d9bda15afea264015291f926a68333a0995ed354
[sdc/sdc-distribution-client.git] /
1 '''SSL with SNI_-support for Python 2.
2
3 This needs the following packages installed:
4
5 * pyOpenSSL (tested with 0.13)
6 * ndg-httpsclient (tested with 0.3.2)
7 * pyasn1 (tested with 0.1.6)
8
9 To activate it call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3`.
10 This can be done in a ``sitecustomize`` module, or at any other time before
11 your application begins using ``urllib3``, like this::
12
13     try:
14         import urllib3.contrib.pyopenssl
15         urllib3.contrib.pyopenssl.inject_into_urllib3()
16     except ImportError:
17         pass
18
19 Now you can use :mod:`urllib3` as you normally would, and it will support SNI
20 when the required modules are installed.
21
22 Activating this module also has the positive side effect of disabling SSL/TLS
23 encryption in Python 2 (see `CRIME attack`_).
24
25 If you want to configure the default list of supported cipher suites, you can
26 set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
27
28 Module Variables
29 ----------------
30
31 :var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites.
32     Default: ``EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA256
33     EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EDH+aRSA EECDH RC4 !aNULL !eNULL !LOW !3DES
34     !MD5 !EXP !PSK !SRP !DSS'``
35
36 .. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
37 .. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
38
39 '''
40
41 from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT
42 from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName
43 import OpenSSL.SSL
44 from pyasn1.codec.der import decoder as der_decoder
45 from pyasn1.type import univ, constraint
46 from socket import _fileobject
47 import ssl
48 import select
49 from cStringIO import StringIO
50
51 from .. import connection
52 from .. import util
53
54 __all__ = ['inject_into_urllib3', 'extract_from_urllib3']
55
56 # SNI only *really* works if we can read the subjectAltName of certificates.
57 HAS_SNI = SUBJ_ALT_NAME_SUPPORT
58
59 # Map from urllib3 to PyOpenSSL compatible parameter-values.
60 _openssl_versions = {
61     ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
62     ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD,
63     ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
64 }
65 _openssl_verify = {
66     ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
67     ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
68     ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER
69                        + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
70 }
71
72 # Default SSL/TLS cipher list.
73 # Recommendation by https://community.qualys.com/blogs/securitylabs/2013/08/05/
74 # configuring-apache-nginx-and-openssl-for-forward-secrecy
75 DEFAULT_SSL_CIPHER_LIST = 'EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM ' + \
76         'EECDH+ECDSA+SHA256 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EDH+aRSA ' + \
77         'EECDH RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS'
78
79
80 orig_util_HAS_SNI = util.HAS_SNI
81 orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket
82
83
84 def inject_into_urllib3():
85     'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.'
86
87     connection.ssl_wrap_socket = ssl_wrap_socket
88     util.HAS_SNI = HAS_SNI
89
90
91 def extract_from_urllib3():
92     'Undo monkey-patching by :func:`inject_into_urllib3`.'
93
94     connection.ssl_wrap_socket = orig_connection_ssl_wrap_socket
95     util.HAS_SNI = orig_util_HAS_SNI
96
97
98 ### Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
99 class SubjectAltName(BaseSubjectAltName):
100     '''ASN.1 implementation for subjectAltNames support'''
101
102     # There is no limit to how many SAN certificates a certificate may have,
103     #   however this needs to have some limit so we'll set an arbitrarily high
104     #   limit.
105     sizeSpec = univ.SequenceOf.sizeSpec + \
106         constraint.ValueSizeConstraint(1, 1024)
107
108
109 ### Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
110 def get_subj_alt_name(peer_cert):
111     # Search through extensions
112     dns_name = []
113     if not SUBJ_ALT_NAME_SUPPORT:
114         return dns_name
115
116     general_names = SubjectAltName()
117     for i in range(peer_cert.get_extension_count()):
118         ext = peer_cert.get_extension(i)
119         ext_name = ext.get_short_name()
120         if ext_name != 'subjectAltName':
121             continue
122
123         # PyOpenSSL returns extension data in ASN.1 encoded form
124         ext_dat = ext.get_data()
125         decoded_dat = der_decoder.decode(ext_dat,
126                                          asn1Spec=general_names)
127
128         for name in decoded_dat:
129             if not isinstance(name, SubjectAltName):
130                 continue
131             for entry in range(len(name)):
132                 component = name.getComponentByPosition(entry)
133                 if component.getName() != 'dNSName':
134                     continue
135                 dns_name.append(str(component.getComponent()))
136
137     return dns_name
138
139
140 class fileobject(_fileobject):
141
142     def read(self, size=-1):
143         # Use max, disallow tiny reads in a loop as they are very inefficient.
144         # We never leave read() with any leftover data from a new recv() call
145         # in our internal buffer.
146         rbufsize = max(self._rbufsize, self.default_bufsize)
147         # Our use of StringIO rather than lists of string objects returned by
148         # recv() minimizes memory usage and fragmentation that occurs when
149         # rbufsize is large compared to the typical return value of recv().
150         buf = self._rbuf
151         buf.seek(0, 2)  # seek end
152         if size < 0:
153             # Read until EOF
154             self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
155             while True:
156                 try:
157                     data = self._sock.recv(rbufsize)
158                 except OpenSSL.SSL.WantReadError:
159                     continue
160                 if not data:
161                     break
162                 buf.write(data)
163             return buf.getvalue()
164         else:
165             # Read until size bytes or EOF seen, whichever comes first
166             buf_len = buf.tell()
167             if buf_len >= size:
168                 # Already have size bytes in our buffer?  Extract and return.
169                 buf.seek(0)
170                 rv = buf.read(size)
171                 self._rbuf = StringIO()
172                 self._rbuf.write(buf.read())
173                 return rv
174
175             self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
176             while True:
177                 left = size - buf_len
178                 # recv() will malloc the amount of memory given as its
179                 # parameter even though it often returns much less data
180                 # than that.  The returned data string is short lived
181                 # as we copy it into a StringIO and free it.  This avoids
182                 # fragmentation issues on many platforms.
183                 try:
184                     data = self._sock.recv(left)
185                 except OpenSSL.SSL.WantReadError:
186                     continue
187                 if not data:
188                     break
189                 n = len(data)
190                 if n == size and not buf_len:
191                     # Shortcut.  Avoid buffer data copies when:
192                     # - We have no data in our buffer.
193                     # AND
194                     # - Our call to recv returned exactly the
195                     #   number of bytes we were asked to read.
196                     return data
197                 if n == left:
198                     buf.write(data)
199                     del data  # explicit free
200                     break
201                 assert n <= left, "recv(%d) returned %d bytes" % (left, n)
202                 buf.write(data)
203                 buf_len += n
204                 del data  # explicit free
205                 #assert buf_len == buf.tell()
206             return buf.getvalue()
207
208     def readline(self, size=-1):
209         buf = self._rbuf
210         buf.seek(0, 2)  # seek end
211         if buf.tell() > 0:
212             # check if we already have it in our buffer
213             buf.seek(0)
214             bline = buf.readline(size)
215             if bline.endswith('\n') or len(bline) == size:
216                 self._rbuf = StringIO()
217                 self._rbuf.write(buf.read())
218                 return bline
219             del bline
220         if size < 0:
221             # Read until \n or EOF, whichever comes first
222             if self._rbufsize <= 1:
223                 # Speed up unbuffered case
224                 buf.seek(0)
225                 buffers = [buf.read()]
226                 self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
227                 data = None
228                 recv = self._sock.recv
229                 while True:
230                     try:
231                         while data != "\n":
232                             data = recv(1)
233                             if not data:
234                                 break
235                             buffers.append(data)
236                     except OpenSSL.SSL.WantReadError:
237                         continue
238                     break
239                 return "".join(buffers)
240
241             buf.seek(0, 2)  # seek end
242             self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
243             while True:
244                 try:
245                     data = self._sock.recv(self._rbufsize)
246                 except OpenSSL.SSL.WantReadError:
247                     continue
248                 if not data:
249                     break
250                 nl = data.find('\n')
251                 if nl >= 0:
252                     nl += 1
253                     buf.write(data[:nl])
254                     self._rbuf.write(data[nl:])
255                     del data
256                     break
257                 buf.write(data)
258             return buf.getvalue()
259         else:
260             # Read until size bytes or \n or EOF seen, whichever comes first
261             buf.seek(0, 2)  # seek end
262             buf_len = buf.tell()
263             if buf_len >= size:
264                 buf.seek(0)
265                 rv = buf.read(size)
266                 self._rbuf = StringIO()
267                 self._rbuf.write(buf.read())
268                 return rv
269             self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
270             while True:
271                 try:
272                     data = self._sock.recv(self._rbufsize)
273                 except OpenSSL.SSL.WantReadError:
274                         continue
275                 if not data:
276                     break
277                 left = size - buf_len
278                 # did we just receive a newline?
279                 nl = data.find('\n', 0, left)
280                 if nl >= 0:
281                     nl += 1
282                     # save the excess data to _rbuf
283                     self._rbuf.write(data[nl:])
284                     if buf_len:
285                         buf.write(data[:nl])
286                         break
287                     else:
288                         # Shortcut.  Avoid data copy through buf when returning
289                         # a substring of our first recv().
290                         return data[:nl]
291                 n = len(data)
292                 if n == size and not buf_len:
293                     # Shortcut.  Avoid data copy through buf when
294                     # returning exactly all of our first recv().
295                     return data
296                 if n >= left:
297                     buf.write(data[:left])
298                     self._rbuf.write(data[left:])
299                     break
300                 buf.write(data)
301                 buf_len += n
302                 #assert buf_len == buf.tell()
303             return buf.getvalue()
304
305
306 class WrappedSocket(object):
307     '''API-compatibility wrapper for Python OpenSSL's Connection-class.'''
308
309     def __init__(self, connection, socket):
310         self.connection = connection
311         self.socket = socket
312
313     def fileno(self):
314         return self.socket.fileno()
315
316     def makefile(self, mode, bufsize=-1):
317         return fileobject(self.connection, mode, bufsize)
318
319     def settimeout(self, timeout):
320         return self.socket.settimeout(timeout)
321
322     def sendall(self, data):
323         return self.connection.sendall(data)
324
325     def close(self):
326         return self.connection.shutdown()
327
328     def getpeercert(self, binary_form=False):
329         x509 = self.connection.get_peer_certificate()
330
331         if not x509:
332             return x509
333
334         if binary_form:
335             return OpenSSL.crypto.dump_certificate(
336                 OpenSSL.crypto.FILETYPE_ASN1,
337                 x509)
338
339         return {
340             'subject': (
341                 (('commonName', x509.get_subject().CN),),
342             ),
343             'subjectAltName': [
344                 ('DNS', value)
345                 for value in get_subj_alt_name(x509)
346             ]
347         }
348
349
350 def _verify_callback(cnx, x509, err_no, err_depth, return_code):
351     return err_no == 0
352
353
354 def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
355                     ca_certs=None, server_hostname=None,
356                     ssl_version=None):
357     ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version])
358     if certfile:
359         ctx.use_certificate_file(certfile)
360     if keyfile:
361         ctx.use_privatekey_file(keyfile)
362     if cert_reqs != ssl.CERT_NONE:
363         ctx.set_verify(_openssl_verify[cert_reqs], _verify_callback)
364     if ca_certs:
365         try:
366             ctx.load_verify_locations(ca_certs, None)
367         except OpenSSL.SSL.Error as e:
368             raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e)
369
370     # Disable TLS compression to migitate CRIME attack (issue #309)
371     OP_NO_COMPRESSION = 0x20000
372     ctx.set_options(OP_NO_COMPRESSION)
373
374     # Set list of supported ciphersuites.
375     ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST)
376
377     cnx = OpenSSL.SSL.Connection(ctx, sock)
378     cnx.set_tlsext_host_name(server_hostname)
379     cnx.set_connect_state()
380     while True:
381         try:
382             cnx.do_handshake()
383         except OpenSSL.SSL.WantReadError:
384             select.select([sock], [], [])
385             continue
386         except OpenSSL.SSL.Error as e:
387             raise ssl.SSLError('bad handshake', e)
388         break
389
390     return WrappedSocket(cnx, sock)