]> granicus.if.org Git - python/commitdiff
Issue #25114, asyncio: add ssl_object extra info to SSL transports
authorVictor Stinner <victor.stinner@gmail.com>
Mon, 21 Sep 2015 16:06:17 +0000 (18:06 +0200)
committerVictor Stinner <victor.stinner@gmail.com>
Mon, 21 Sep 2015 16:06:17 +0000 (18:06 +0200)
This info is required on Python 3.5 and newer to get specific information on
the SSL object, like getting the binary peer certificate (instead of getting
it as text).

Doc/library/asyncio-protocol.rst
Lib/asyncio/selector_events.py
Lib/asyncio/sslproto.py
Lib/test/test_asyncio/test_events.py

index 02a1b68c536a2d289df2ce0bb47a898a3f0cdf35..656816f75b020cfa752f280c7b63b4df5aba0d5e 100644 (file)
@@ -71,6 +71,8 @@ BaseTransport
         - ``'peercert'``: peer certificate; result of
           :meth:`ssl.SSLSocket.getpeercert`
         - ``'sslcontext'``: :class:`ssl.SSLContext` instance
+        - ``'ssl_object'``: :class:`ssl.SSLObject` or :class:`ssl.SSLSocket`
+          instance
 
       * pipe:
 
@@ -80,6 +82,9 @@ BaseTransport
 
         - ``'subprocess'``: :class:`subprocess.Popen` instance
 
+   .. versionchanged:: 3.4.4
+      ``'ssl_object'`` info was added to SSL sockets.
+
 
 ReadTransport
 -------------
index 4a9965849e19d0f8845d91df2858299f6515c162..0060912219e9cf8f6c6142797559ef6410e2826c 100644 (file)
@@ -843,6 +843,7 @@ class _SelectorSslTransport(_SelectorTransport):
         self._extra.update(peercert=peercert,
                            cipher=self._sock.cipher(),
                            compression=self._sock.compression(),
+                           ssl_object=self._sock,
                            )
 
         self._read_wants_write = False
index e5ae49a5df92da6e769ce7f4b676f8196058c232..0a8c0900f81f59705d2702856fa2c9a23bfa512c 100644 (file)
@@ -295,6 +295,7 @@ class _SSLProtocolTransport(transports._FlowControlMixin,
 
     def __init__(self, loop, ssl_protocol, app_protocol):
         self._loop = loop
+        # SSLProtocol instance
         self._ssl_protocol = ssl_protocol
         self._app_protocol = app_protocol
         self._closed = False
@@ -425,10 +426,12 @@ class SSLProtocol(protocols.Protocol):
         self._app_protocol = app_protocol
         self._app_transport = _SSLProtocolTransport(self._loop,
                                                     self, self._app_protocol)
+        # _SSLPipe instance (None until the connection is made)
         self._sslpipe = None
         self._session_established = False
         self._in_handshake = False
         self._in_shutdown = False
+        # transport, ex: SelectorSocketTransport
         self._transport = None
 
     def _wakeup_waiter(self, exc=None):
@@ -591,6 +594,7 @@ class SSLProtocol(protocols.Protocol):
         self._extra.update(peercert=peercert,
                            cipher=sslobj.cipher(),
                            compression=sslobj.compression(),
+                           ssl_object=sslobj,
                            )
         self._app_protocol.connection_made(self._app_transport)
         self._wakeup_waiter()
index 8fbba8fe0fc571f5f075c95e6f56eb62837e9be8..ba1fa5dd0ca7367fc5ceb3a109a974cd97425c4d 100644 (file)
@@ -57,6 +57,17 @@ ONLYCERT = data_file('ssl_cert.pem')
 ONLYKEY = data_file('ssl_key.pem')
 SIGNED_CERTFILE = data_file('keycert3.pem')
 SIGNING_CA = data_file('pycacert.pem')
+PEERCERT = {'serialNumber': 'B09264B1F2DA21D1',
+            'version': 1,
+            'subject': ((('countryName', 'XY'),),
+                    (('localityName', 'Castle Anthrax'),),
+                    (('organizationName', 'Python Software Foundation'),),
+                    (('commonName', 'localhost'),)),
+            'issuer': ((('countryName', 'XY'),),
+                    (('organizationName', 'Python Software Foundation CA'),),
+                    (('commonName', 'our-ca-server'),)),
+            'notAfter': 'Nov 13 19:47:07 2022 GMT',
+            'notBefore': 'Jan  4 19:47:07 2013 GMT'}
 
 
 class MyBaseProto(asyncio.Protocol):
@@ -596,22 +607,56 @@ class EventLoopTestsMixin:
             self.assertGreater(pr.nbytes, 0)
             tr.close()
 
+    def check_ssl_extra_info(self, client, check_sockname=True,
+                             peername=None, peercert={}):
+        if check_sockname:
+            self.assertIsNotNone(client.get_extra_info('sockname'))
+        if peername:
+            self.assertEqual(peername,
+                             client.get_extra_info('peername'))
+        else:
+            self.assertIsNotNone(client.get_extra_info('peername'))
+        self.assertEqual(peercert,
+                         client.get_extra_info('peercert'))
+
+        # Python disables compression to prevent CRIME attacks by default
+        self.assertIsNone(client.get_extra_info('compression'))
+
+        # test SSL cipher
+        cipher = client.get_extra_info('cipher')
+        self.assertIsInstance(cipher, tuple)
+        self.assertEqual(len(cipher), 3, cipher)
+        self.assertIsInstance(cipher[0], str)
+        self.assertIsInstance(cipher[1], str)
+        self.assertIsInstance(cipher[2], int)
+
+        # test SSL object
+        sslobj = client.get_extra_info('ssl_object')
+        self.assertIsNotNone(sslobj)
+        self.assertEqual(sslobj.compression(),
+                         client.get_extra_info('compression'))
+        self.assertEqual(sslobj.cipher(),
+                         client.get_extra_info('cipher'))
+        self.assertEqual(sslobj.getpeercert(),
+                         client.get_extra_info('peercert'))
+
     def _basetest_create_ssl_connection(self, connection_fut,
-                                        check_sockname=True):
+                                        check_sockname=True,
+                                        peername=None):
         tr, pr = self.loop.run_until_complete(connection_fut)
         self.assertIsInstance(tr, asyncio.Transport)
         self.assertIsInstance(pr, asyncio.Protocol)
         self.assertTrue('ssl' in tr.__class__.__name__.lower())
-        if check_sockname:
-            self.assertIsNotNone(tr.get_extra_info('sockname'))
+        self.check_ssl_extra_info(tr, check_sockname, peername)
         self.loop.run_until_complete(pr.done)
         self.assertGreater(pr.nbytes, 0)
         tr.close()
 
     def _test_create_ssl_connection(self, httpd, create_connection,
-                                    check_sockname=True):
+                                    check_sockname=True, peername=None):
         conn_fut = create_connection(ssl=test_utils.dummy_ssl_context())
-        self._basetest_create_ssl_connection(conn_fut, check_sockname)
+        self._basetest_create_ssl_connection(conn_fut, check_sockname,
+                                             peername)
 
         # ssl.Purpose was introduced in Python 3.4
         if hasattr(ssl, 'Purpose'):
@@ -629,7 +674,8 @@ class EventLoopTestsMixin:
             with mock.patch('ssl.create_default_context',
                             side_effect=_dummy_ssl_create_context) as m:
                 conn_fut = create_connection(ssl=True)
-                self._basetest_create_ssl_connection(conn_fut, check_sockname)
+                self._basetest_create_ssl_connection(conn_fut, check_sockname,
+                                                     peername)
                 self.assertEqual(m.call_count, 1)
 
         # With the real ssl.create_default_context(), certificate
@@ -638,7 +684,8 @@ class EventLoopTestsMixin:
             conn_fut = create_connection(ssl=True)
             # Ignore the "SSL handshake failed" log in debug mode
             with test_utils.disable_logger():
-                self._basetest_create_ssl_connection(conn_fut, check_sockname)
+                self._basetest_create_ssl_connection(conn_fut, check_sockname,
+                                                     peername)
 
         self.assertEqual(cm.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
 
@@ -649,7 +696,8 @@ class EventLoopTestsMixin:
                 self.loop.create_connection,
                 lambda: MyProto(loop=self.loop),
                 *httpd.address)
-            self._test_create_ssl_connection(httpd, create_connection)
+            self._test_create_ssl_connection(httpd, create_connection,
+                                             peername=httpd.address)
 
     def test_legacy_create_ssl_connection(self):
         with test_utils.force_legacy_ssl_support():
@@ -669,7 +717,8 @@ class EventLoopTestsMixin:
                 server_hostname='127.0.0.1')
 
             self._test_create_ssl_connection(httpd, create_connection,
-                                             check_sockname)
+                                             check_sockname,
+                                             peername=httpd.address)
 
     def test_legacy_create_ssl_unix_connection(self):
         with test_utils.force_legacy_ssl_support():
@@ -819,9 +868,7 @@ class EventLoopTestsMixin:
         self.assertEqual(3, proto.nbytes)
 
         # extra info is available
-        self.assertIsNotNone(proto.transport.get_extra_info('sockname'))
-        self.assertEqual('127.0.0.1',
-                         proto.transport.get_extra_info('peername')[0])
+        self.check_ssl_extra_info(client, peername=(host, port))
 
         # close connection
         proto.transport.close()
@@ -1023,6 +1070,10 @@ class EventLoopTestsMixin:
                                           server_hostname='localhost')
         client, pr = self.loop.run_until_complete(f_c)
 
+        # extra info is available
+        self.check_ssl_extra_info(client,peername=(host, port),
+                                  peercert=PEERCERT)
+
         # close connection
         proto.transport.close()
         client.close()