ed3b9cc3423482af46468d856dae420a9dc023c3
[sdc/sdc-distribution-client.git] /
1 '''SSL with SNI_-support for Python 2. Follow these instructions if you would
2 like to verify SSL certificates in Python 2. Note, the default libraries do
3 *not* do certificate checking; you need to do additional work to validate
4 certificates yourself.
5
6 This needs the following packages installed:
7
8 * pyOpenSSL (tested with 0.13)
9 * ndg-httpsclient (tested with 0.3.2)
10 * pyasn1 (tested with 0.1.6)
11
12 You can install them with the following command:
13
14     pip install pyopenssl ndg-httpsclient pyasn1
15
16 To activate certificate checking, call
17 :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code
18 before you begin making HTTP requests. This can be done in a ``sitecustomize``
19 module, or at any other time before your application begins using ``urllib3``,
20 like this::
21
22     try:
23         import urllib3.contrib.pyopenssl
24         urllib3.contrib.pyopenssl.inject_into_urllib3()
25     except ImportError:
26         pass
27
28 Now you can use :mod:`urllib3` as you normally would, and it will support SNI
29 when the required modules are installed.
30
31 Activating this module also has the positive side effect of disabling SSL/TLS
32 compression in Python 2 (see `CRIME attack`_).
33
34 If you want to configure the default list of supported cipher suites, you can
35 set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
36
37 Module Variables
38 ----------------
39
40 :var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites.
41
42 .. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
43 .. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
44
45 '''
46 from __future__ import absolute_import
47
48 try:
49     from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT
50     from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName
51 except SyntaxError as e:
52     raise ImportError(e)
53
54 import OpenSSL.SSL
55 from pyasn1.codec.der import decoder as der_decoder
56 from pyasn1.type import univ, constraint
57 from socket import timeout, error as SocketError
58
59 try:  # Platform-specific: Python 2
60     from socket import _fileobject
61 except ImportError:  # Platform-specific: Python 3
62     _fileobject = None
63     from urllib3.packages.backports.makefile import backport_makefile
64
65 import ssl
66 import select
67 import six
68
69 from .. import connection
70 from .. import util
71
72 __all__ = ['inject_into_urllib3', 'extract_from_urllib3']
73
74 # SNI only *really* works if we can read the subjectAltName of certificates.
75 HAS_SNI = SUBJ_ALT_NAME_SUPPORT
76
77 # Map from urllib3 to PyOpenSSL compatible parameter-values.
78 _openssl_versions = {
79     ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
80     ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
81 }
82
83 if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'):
84     _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
85
86 if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'):
87     _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD
88
89 try:
90     _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
91 except AttributeError:
92     pass
93
94 _openssl_verify = {
95     ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
96     ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
97     ssl.CERT_REQUIRED:
98         OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
99 }
100
101 DEFAULT_SSL_CIPHER_LIST = util.ssl_.DEFAULT_CIPHERS.encode('ascii')
102
103 # OpenSSL will only write 16K at a time
104 SSL_WRITE_BLOCKSIZE = 16384
105
106 orig_util_HAS_SNI = util.HAS_SNI
107 orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket
108
109
110 def inject_into_urllib3():
111     'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.'
112
113     connection.ssl_wrap_socket = ssl_wrap_socket
114     util.HAS_SNI = HAS_SNI
115     util.IS_PYOPENSSL = True
116
117
118 def extract_from_urllib3():
119     'Undo monkey-patching by :func:`inject_into_urllib3`.'
120
121     connection.ssl_wrap_socket = orig_connection_ssl_wrap_socket
122     util.HAS_SNI = orig_util_HAS_SNI
123     util.IS_PYOPENSSL = False
124
125
126 # Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
127 class SubjectAltName(BaseSubjectAltName):
128     '''ASN.1 implementation for subjectAltNames support'''
129
130     # There is no limit to how many SAN certificates a certificate may have,
131     #   however this needs to have some limit so we'll set an arbitrarily high
132     #   limit.
133     sizeSpec = univ.SequenceOf.sizeSpec + \
134         constraint.ValueSizeConstraint(1, 1024)
135
136
137 # Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
138 def get_subj_alt_name(peer_cert):
139     # Search through extensions
140     dns_name = []
141     if not SUBJ_ALT_NAME_SUPPORT:
142         return dns_name
143
144     general_names = SubjectAltName()
145     for i in range(peer_cert.get_extension_count()):
146         ext = peer_cert.get_extension(i)
147         ext_name = ext.get_short_name()
148         if ext_name != b'subjectAltName':
149             continue
150
151         # PyOpenSSL returns extension data in ASN.1 encoded form
152         ext_dat = ext.get_data()
153         decoded_dat = der_decoder.decode(ext_dat,
154                                          asn1Spec=general_names)
155
156         for name in decoded_dat:
157             if not isinstance(name, SubjectAltName):
158                 continue
159             for entry in range(len(name)):
160                 component = name.getComponentByPosition(entry)
161                 if component.getName() != 'dNSName':
162                     continue
163                 dns_name.append(str(component.getComponent()))
164
165     return dns_name
166
167
168 class WrappedSocket(object):
169     '''API-compatibility wrapper for Python OpenSSL's Connection-class.
170
171     Note: _makefile_refs, _drop() and _reuse() are needed for the garbage
172     collector of pypy.
173     '''
174
175     def __init__(self, connection, socket, suppress_ragged_eofs=True):
176         self.connection = connection
177         self.socket = socket
178         self.suppress_ragged_eofs = suppress_ragged_eofs
179         self._makefile_refs = 0
180         self._closed = False
181
182     def fileno(self):
183         return self.socket.fileno()
184
185     # Copy-pasted from Python 3.5 source code
186     def _decref_socketios(self):
187         if self._makefile_refs > 0:
188             self._makefile_refs -= 1
189         if self._closed:
190             self.close()
191
192     def recv(self, *args, **kwargs):
193         try:
194             data = self.connection.recv(*args, **kwargs)
195         except OpenSSL.SSL.SysCallError as e:
196             if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
197                 return b''
198             else:
199                 raise SocketError(str(e))
200         except OpenSSL.SSL.ZeroReturnError as e:
201             if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
202                 return b''
203             else:
204                 raise
205         except OpenSSL.SSL.WantReadError:
206             rd, wd, ed = select.select(
207                 [self.socket], [], [], self.socket.gettimeout())
208             if not rd:
209                 raise timeout('The read operation timed out')
210             else:
211                 return self.recv(*args, **kwargs)
212         else:
213             return data
214
215     def recv_into(self, *args, **kwargs):
216         try:
217             return self.connection.recv_into(*args, **kwargs)
218         except OpenSSL.SSL.SysCallError as e:
219             if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'):
220                 return 0
221             else:
222                 raise SocketError(str(e))
223         except OpenSSL.SSL.ZeroReturnError as e:
224             if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
225                 return 0
226             else:
227                 raise
228         except OpenSSL.SSL.WantReadError:
229             rd, wd, ed = select.select(
230                 [self.socket], [], [], self.socket.gettimeout())
231             if not rd:
232                 raise timeout('The read operation timed out')
233             else:
234                 return self.recv_into(*args, **kwargs)
235
236     def settimeout(self, timeout):
237         return self.socket.settimeout(timeout)
238
239     def _send_until_done(self, data):
240         while True:
241             try:
242                 return self.connection.send(data)
243             except OpenSSL.SSL.WantWriteError:
244                 _, wlist, _ = select.select([], [self.socket], [],
245                                             self.socket.gettimeout())
246                 if not wlist:
247                     raise timeout()
248                 continue
249
250     def sendall(self, data):
251         total_sent = 0
252         while total_sent < len(data):
253             sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE])
254             total_sent += sent
255
256     def shutdown(self):
257         # FIXME rethrow compatible exceptions should we ever use this
258         self.connection.shutdown()
259
260     def close(self):
261         if self._makefile_refs < 1:
262             try:
263                 self._closed = True
264                 return self.connection.close()
265             except OpenSSL.SSL.Error:
266                 return
267         else:
268             self._makefile_refs -= 1
269
270     def getpeercert(self, binary_form=False):
271         x509 = self.connection.get_peer_certificate()
272
273         if not x509:
274             return x509
275
276         if binary_form:
277             return OpenSSL.crypto.dump_certificate(
278                 OpenSSL.crypto.FILETYPE_ASN1,
279                 x509)
280
281         return {
282             'subject': (
283                 (('commonName', x509.get_subject().CN),),
284             ),
285             'subjectAltName': [
286                 ('DNS', value)
287                 for value in get_subj_alt_name(x509)
288             ]
289         }
290
291     def _reuse(self):
292         self._makefile_refs += 1
293
294     def _drop(self):
295         if self._makefile_refs < 1:
296             self.close()
297         else:
298             self._makefile_refs -= 1
299
300
301 if _fileobject:  # Platform-specific: Python 2
302     def makefile(self, mode, bufsize=-1):
303         self._makefile_refs += 1
304         return _fileobject(self, mode, bufsize, close=True)
305 else:  # Platform-specific: Python 3
306     makefile = backport_makefile
307
308 WrappedSocket.makefile = makefile
309
310
311 def _verify_callback(cnx, x509, err_no, err_depth, return_code):
312     return err_no == 0
313
314
315 def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
316                     ca_certs=None, server_hostname=None,
317                     ssl_version=None, ca_cert_dir=None):
318     ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version])
319     if certfile:
320         keyfile = keyfile or certfile  # Match behaviour of the normal python ssl library
321         ctx.use_certificate_file(certfile)
322     if keyfile:
323         ctx.use_privatekey_file(keyfile)
324     if cert_reqs != ssl.CERT_NONE:
325         ctx.set_verify(_openssl_verify[cert_reqs], _verify_callback)
326     if ca_certs or ca_cert_dir:
327         try:
328             ctx.load_verify_locations(ca_certs, ca_cert_dir)
329         except OpenSSL.SSL.Error as e:
330             raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e)
331     else:
332         ctx.set_default_verify_paths()
333
334     # Disable TLS compression to mitigate CRIME attack (issue #309)
335     OP_NO_COMPRESSION = 0x20000
336     ctx.set_options(OP_NO_COMPRESSION)
337
338     # Set list of supported ciphersuites.
339     ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST)
340
341     cnx = OpenSSL.SSL.Connection(ctx, sock)
342     if isinstance(server_hostname, six.text_type):  # Platform-specific: Python 3
343         server_hostname = server_hostname.encode('utf-8')
344     cnx.set_tlsext_host_name(server_hostname)
345     cnx.set_connect_state()
346     while True:
347         try:
348             cnx.do_handshake()
349         except OpenSSL.SSL.WantReadError:
350             rd, _, _ = select.select([sock], [], [], sock.gettimeout())
351             if not rd:
352                 raise timeout('select timed out')
353             continue
354         except OpenSSL.SSL.Error as e:
355             raise ssl.SSLError('bad handshake: %r' % e)
356         break
357
358     return WrappedSocket(cnx, sock)