From dae06d1921b3e96b8a2d44b3a67dfef90f37b6d9 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Thu, 7 Sep 2023 22:31:17 +0200 Subject: [PATCH 01/10] Expose retrieving certificate chains in SSL module --- Doc/library/ssl.rst | 23 +++++++++++++++++++---- Lib/ssl.py | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 19225c85ff7624..6e5d9a19c03e2c 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -42,8 +42,10 @@ This module provides a class, :class:`ssl.SSLSocket`, which is derived from the :class:`socket.socket` type, and provides a socket-like wrapper that also encrypts and decrypts the data going over the socket with SSL. It supports additional methods such as :meth:`getpeercert`, which retrieves the -certificate of the other side of the connection, and :meth:`cipher`, which -retrieves the cipher being used for the secure connection. +certificate of the other side of the connection, :meth:`cipher`, which +retrieves the cipher being used for the secure connection or +:meth:`get_verified_chain`, :meth:`get_unverified_chain` which retrieves +certificate chain. For more sophisticated applications, the :class:`ssl.SSLContext` class helps manage settings and certificates, which can then be inherited @@ -1221,6 +1223,16 @@ SSL sockets also have the following additional methods and attributes: .. versionchanged:: 3.9 IPv6 address strings no longer have a trailing new line. +.. method:: SSLSocket.get_verified_chain() + + Returns verified verified certificate chain provided by the other + end of the SSL channel. Return ``None`` if no certificates were provided. + +.. method:: SSLSocket.get_unverified_chain() + + Returns unverified verified certificate chain provided by the other + end of the SSL channel. Return ``None`` if no certificates were provided. + .. method:: SSLSocket.cipher() Returns a three-value tuple containing the name of the cipher being used, the @@ -1670,8 +1682,9 @@ to speed up repeated connections from the same clients. Due to the early negotiation phase of the TLS connection, only limited methods and attributes are usable like :meth:`SSLSocket.selected_alpn_protocol` and :attr:`SSLSocket.context`. - The :meth:`SSLSocket.getpeercert`, - :meth:`SSLSocket.cipher` and :meth:`SSLSocket.compression` methods require that + The :meth:`SSLSocket.getpeercert`, :meth:`SSLSocket.get_verified_chain`, + :meth:`SSLSocket.get_unverified_chain` :meth:`SSLSocket.cipher` + and :meth:`SSLSocket.compression` methods require that the TLS connection has progressed beyond the TLS Client Hello and therefore will not return meaningful values nor can they be called safely. @@ -2428,6 +2441,8 @@ provided. - :meth:`~SSLSocket.read` - :meth:`~SSLSocket.write` - :meth:`~SSLSocket.getpeercert` + - :meth:`~SSLSocket.get_verified_chain` + - :meth:`~SSLSocket.get_unverified_chain` - :meth:`~SSLSocket.selected_alpn_protocol` - :meth:`~SSLSocket.selected_npn_protocol` - :meth:`~SSLSocket.cipher` diff --git a/Lib/ssl.py b/Lib/ssl.py index 1d5873726441e4..e5f4da72ab1b6b 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -876,6 +876,22 @@ def getpeercert(self, binary_form=False): """ return self._sslobj.getpeercert(binary_form) + def get_verified_chain(self): + """Returns verified verified certificate chain provided by the other + end of the SSL channel. + + Return None if no certificates were provided. + """ + return self._sslobj.get_verified_chain() + + def get_unverified_chain(self): + """Returns unverified verified certificate chain provided by the other + end of the SSL channel. + + Return None if no certificates were provided. + """ + return self._sslobj.get_unverified_chain() + def selected_npn_protocol(self): """Return the currently selected NPN protocol as a string, or ``None`` if a next protocol was not negotiated or if NPN is not supported by one @@ -1096,6 +1112,14 @@ def getpeercert(self, binary_form=False): self._check_connected() return self._sslobj.getpeercert(binary_form) + @_sslcopydoc + def get_verified_chain(self): + return self._sslobj.get_verified_chain() + + @_sslcopydoc + def get_unverified_chain(self): + return self._sslobj.get_unverified_chain() + @_sslcopydoc def selected_npn_protocol(self): self._checkClosed() From 1418dd109022cdff4432e332cffba4f19b143cb4 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Fri, 8 Sep 2023 09:53:13 +0200 Subject: [PATCH 02/10] Review remarks applied --- Doc/library/ssl.rst | 14 ++++++++++---- Lib/ssl.py | 14 ++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 58c8f58463b2a8..7288b85c5a8038 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1214,13 +1214,19 @@ SSL sockets also have the following additional methods and attributes: .. method:: SSLSocket.get_verified_chain() - Returns verified verified certificate chain provided by the other - end of the SSL channel. Return ``None`` if no certificates were provided. + Returns verified certificate chain provided by the other + end of the SSL channel as a list of ``_ssl.Certificate``. + Return ``None`` if no certificates were provided. + + .. versionadded:: 3.13 .. method:: SSLSocket.get_unverified_chain() - Returns unverified verified certificate chain provided by the other - end of the SSL channel. Return ``None`` if no certificates were provided. + Returns unverified certificate chain provided by the other + end of the SSL channel as a list of ``_ssl.Certificate``. + Return ``None`` if no certificates were provided. + + .. versionadded:: 3.13 .. method:: SSLSocket.cipher() diff --git a/Lib/ssl.py b/Lib/ssl.py index 1b7f284357f755..d768487d88dccd 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -877,18 +877,16 @@ def getpeercert(self, binary_form=False): return self._sslobj.getpeercert(binary_form) def get_verified_chain(self): - """Returns verified verified certificate chain provided by the other - end of the SSL channel. - - Return None if no certificates were provided. + """Returns verified certificate chain provided by the other + end of the SSL channel as a list of ``_ssl.Certificate``. + Return ``None`` if no certificates were provided. """ return self._sslobj.get_verified_chain() def get_unverified_chain(self): - """Returns unverified verified certificate chain provided by the other - end of the SSL channel. - - Return None if no certificates were provided. + """Returns unverified certificate chain provided by the other + end of the SSL channel as a list of ``_ssl.Certificate``. + Return ``None`` if no certificates were provided. """ return self._sslobj.get_unverified_chain() From 0cdfe19425ba0de60ad23431792ad835267a0213 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Fri, 8 Sep 2023 12:21:04 +0200 Subject: [PATCH 03/10] Trim white spaces --- Doc/library/ssl.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 7288b85c5a8038..4e24ace624bb89 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1218,7 +1218,7 @@ SSL sockets also have the following additional methods and attributes: end of the SSL channel as a list of ``_ssl.Certificate``. Return ``None`` if no certificates were provided. - .. versionadded:: 3.13 + .. versionadded:: 3.13 .. method:: SSLSocket.get_unverified_chain() @@ -1226,7 +1226,7 @@ SSL sockets also have the following additional methods and attributes: end of the SSL channel as a list of ``_ssl.Certificate``. Return ``None`` if no certificates were provided. - .. versionadded:: 3.13 + .. versionadded:: 3.13 .. method:: SSLSocket.cipher() From 4a51db29416e7a5f09fa5394280bc7b20c1c8c56 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Mon, 11 Sep 2023 13:28:47 +0200 Subject: [PATCH 04/10] Review fixes. --- Doc/library/ssl.rst | 6 ++---- Lib/ssl.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 4e24ace624bb89..75c308e57c5c46 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1215,16 +1215,14 @@ SSL sockets also have the following additional methods and attributes: .. method:: SSLSocket.get_verified_chain() Returns verified certificate chain provided by the other - end of the SSL channel as a list of ``_ssl.Certificate``. - Return ``None`` if no certificates were provided. + end of the SSL channel as a list of DER-encoded bytes. .. versionadded:: 3.13 .. method:: SSLSocket.get_unverified_chain() Returns unverified certificate chain provided by the other - end of the SSL channel as a list of ``_ssl.Certificate``. - Return ``None`` if no certificates were provided. + end of the SSL channel as a list of DER-encoded bytes. .. versionadded:: 3.13 diff --git a/Lib/ssl.py b/Lib/ssl.py index d768487d88dccd..ffcf5255e34806 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -878,17 +878,25 @@ def getpeercert(self, binary_form=False): def get_verified_chain(self): """Returns verified certificate chain provided by the other - end of the SSL channel as a list of ``_ssl.Certificate``. - Return ``None`` if no certificates were provided. + end of the SSL channel as a list of DER-encoded bytes. """ - return self._sslobj.get_verified_chain() + chain = self._sslobj.get_verified_chain() + + if chain is None: + return [] + + return [cert.public_bytes(_ssl.ENCODING_DER) for cert in chain] def get_unverified_chain(self): """Returns unverified certificate chain provided by the other - end of the SSL channel as a list of ``_ssl.Certificate``. - Return ``None`` if no certificates were provided. + end of the SSL channel as a list of DER-encoded bytes. """ - return self._sslobj.get_unverified_chain() + chain = self._sslobj.get_verified_chain() + + if chain is None: + return [] + + return [cert.public_bytes(_ssl.ENCODING_DER) for cert in chain] def selected_npn_protocol(self): """Return the currently selected NPN protocol as a string, or ``None`` From 701c62a80af6fa8340a7ee0fedb10b146eaa360d Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Wed, 13 Sep 2023 15:42:01 +0200 Subject: [PATCH 05/10] Update Lib/ssl.py --- Lib/ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index ffcf5255e34806..f5c2491afd5393 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -891,7 +891,7 @@ def get_unverified_chain(self): """Returns unverified certificate chain provided by the other end of the SSL channel as a list of DER-encoded bytes. """ - chain = self._sslobj.get_verified_chain() + chain = self._sslobj.get_unverified_chain() if chain is None: return [] From f925937080d235c8e6dc6b50f0d2d27d23b9dfc1 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Tue, 19 Sep 2023 22:33:40 +0200 Subject: [PATCH 06/10] Review fixes --- Doc/library/ssl.rst | 4 +++- Lib/ssl.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 75c308e57c5c46..92cf3de2a7b4cf 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1216,12 +1216,14 @@ SSL sockets also have the following additional methods and attributes: Returns verified certificate chain provided by the other end of the SSL channel as a list of DER-encoded bytes. + If certificate verification was disabled method acts the same as + :meth:`~SSLSocket.get_unverified_chain`. .. versionadded:: 3.13 .. method:: SSLSocket.get_unverified_chain() - Returns unverified certificate chain provided by the other + Returns raw certificate chain provided by the other end of the SSL channel as a list of DER-encoded bytes. .. versionadded:: 3.13 diff --git a/Lib/ssl.py b/Lib/ssl.py index f5c2491afd5393..62e55857141dfc 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -879,6 +879,9 @@ def getpeercert(self, binary_form=False): def get_verified_chain(self): """Returns verified certificate chain provided by the other end of the SSL channel as a list of DER-encoded bytes. + + If certificate verification was disabled method acts the same as + ``SSLSocket.get_unverified_chain``. """ chain = self._sslobj.get_verified_chain() @@ -888,7 +891,7 @@ def get_verified_chain(self): return [cert.public_bytes(_ssl.ENCODING_DER) for cert in chain] def get_unverified_chain(self): - """Returns unverified certificate chain provided by the other + """Returns raw certificate chain provided by the other end of the SSL channel as a list of DER-encoded bytes. """ chain = self._sslobj.get_unverified_chain() From 5cb139d7c7a80ab3e4a73c429b1d3b21343138a6 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith [Google LLC]" Date: Tue, 19 Sep 2023 17:56:28 -0700 Subject: [PATCH 07/10] NEWS entry. --- .../Library/2023-09-19-17-56-24.gh-issue-109109.WJvvX2.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-09-19-17-56-24.gh-issue-109109.WJvvX2.rst diff --git a/Misc/NEWS.d/next/Library/2023-09-19-17-56-24.gh-issue-109109.WJvvX2.rst b/Misc/NEWS.d/next/Library/2023-09-19-17-56-24.gh-issue-109109.WJvvX2.rst new file mode 100644 index 00000000000000..e741e60ff41a9b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-09-19-17-56-24.gh-issue-109109.WJvvX2.rst @@ -0,0 +1,5 @@ +You can now get the raw TLS certificate chains from TLS connections via +:meth:`ssl.SSLSocket.get_verified_chain` and +:meth:`ssl.SSLSocket.get_unverified_chain` methods. + +Contributed by Mateusz Nowak. From d276aeb49a10471958508c1226061e71e2991b46 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Mon, 6 May 2024 22:01:19 +0200 Subject: [PATCH 08/10] Return consistent types for `get_un/verified_chain` in `SSLObject` and `SSLSocket` --- Lib/ssl.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index cc685c2cc405ab..f248e1404baf44 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1165,11 +1165,21 @@ def getpeercert(self, binary_form=False): @_sslcopydoc def get_verified_chain(self): - return self._sslobj.get_verified_chain() + chain = self._sslobj.get_verified_chain() + + if chain is None: + return [] + + return [cert.public_bytes(_ssl.ENCODING_DER) for cert in chain] @_sslcopydoc def get_unverified_chain(self): - return self._sslobj.get_unverified_chain() + chain = self._sslobj.get_unverified_chain() + + if chain is None: + return [] + + return [cert.public_bytes(_ssl.ENCODING_DER) for cert in chain] @_sslcopydoc def selected_npn_protocol(self): From 19bf5f1447811afc6d2a19a7b93a1a0f1225c284 Mon Sep 17 00:00:00 2001 From: matiuszka Date: Wed, 7 Aug 2024 21:43:18 +0200 Subject: [PATCH 09/10] Simple test for un/verified chain --- Lib/test/test_ssl.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 6ec010d13f9e7e..2c383096ed0227 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4720,6 +4720,28 @@ def test_internal_chain_client(self): ssl.PEM_cert_to_DER_cert(pem), der ) + def test_certificate_chain(self): + client_context, server_context, hostname = testing_context( + server_chain=False + ) + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with client_context.wrap_socket( + socket.socket(), + server_hostname=hostname + ) as s: + s.connect((HOST, server.port)) + vc = s.get_verified_chain() + self.assertEqual(len(vc), 2) + + ee, ca = vc + + uvc = s.get_unverified_chain() + self.assertEqual(len(uvc), 1) + + self.assertEqual(ee, uvc[0]) + self.assertNotEqual(ee, ca) + def test_internal_chain_server(self): client_context, server_context, hostname = testing_context() client_context.load_cert_chain(SIGNED_CERTFILE) From e0a1dc6b6176f4d6b1ad2c739946d356ac66879a Mon Sep 17 00:00:00 2001 From: matiuszka Date: Wed, 14 Aug 2024 14:18:43 +0200 Subject: [PATCH 10/10] Tests improvements --- Lib/test/certdata/cert3.pem | 34 ++++++++++++++++++++++++++++++++++ Lib/test/test_ssl.py | 13 +++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Lib/test/certdata/cert3.pem diff --git a/Lib/test/certdata/cert3.pem b/Lib/test/certdata/cert3.pem new file mode 100644 index 00000000000000..034bc43ff1974e --- /dev/null +++ b/Lib/test/certdata/cert3.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF8TCCBFmgAwIBAgIJAMstgJlaaVJcMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNV +BAYTAlhZMSYwJAYDVQQKDB1QeXRob24gU29mdHdhcmUgRm91bmRhdGlvbiBDQTEW +MBQGA1UEAwwNb3VyLWNhLXNlcnZlcjAeFw0xODA4MjkxNDIzMTZaFw0zNzEwMjgx +NDIzMTZaMF8xCzAJBgNVBAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEj +MCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRpb24xEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKAqKHEL7aDt +3swl8hQF8VaK4zDGDRaF3E/IZTMwCN7FsQ4ejSiOe3E90f0phHCIpEpv2OebNenY +IpOGoFgkh62r/cthmnhu8Mn+FUIv17iOq7WX7B30OSqEpnr1voLX93XYkAq8LlMh +P79vsSCVhTwow3HZY7krEgl5WlfryOfj1i1TODSFPRCJePh66BsOTUvV/33GC+Qd +pVZVDGLowU1Ycmr/FdRvwT+F39Dehp03UFcxaX0/joPhH5gYpBB1kWTAQmxuqKMW +9ZZs6hrPtMXF/yfSrrXrzTdpct9paKR8RcufOcS8qju/ISK+1P/LXg2b5KJHedLo +TTIO3yCZ4d1odyuZBP7JDrI05gMJx95gz6sG685Qc+52MzLSTwr/Qg+MOjQoBy0o +8fRRVvIMEwoN0ZDb4uFEUuwZceUP1vTk/GGpNQt7ct4ropn6K4Zta3BUtovlLjZa +IIBhc1KETUqjRDvC6ACKmlcJ/5pY/dbH1lOux+IMFsh+djmaV90b3QIDAQABo4IB +wDCCAbwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA4GA1UdDwEB/wQEAwIFoDAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E +FgQUP7HpT6C+MGY+ChjID0caTzRqD0IwfQYDVR0jBHYwdIAU8+yUjvKOMMSOaMK/ +jmoZwMGfdmWhUaRPME0xCzAJBgNVBAYTAlhZMSYwJAYDVQQKDB1QeXRob24gU29m +dHdhcmUgRm91bmRhdGlvbiBDQTEWMBQGA1UEAwwNb3VyLWNhLXNlcnZlcoIJAMst +gJlaaVJbMIGDBggrBgEFBQcBAQR3MHUwPAYIKwYBBQUHMAKGMGh0dHA6Ly90ZXN0 +Y2EucHl0aG9udGVzdC5uZXQvdGVzdGNhL3B5Y2FjZXJ0LmNlcjA1BggrBgEFBQcw +AYYpaHR0cDovL3Rlc3RjYS5weXRob250ZXN0Lm5ldC90ZXN0Y2Evb2NzcC8wQwYD +VR0fBDwwOjA4oDagNIYyaHR0cDovL3Rlc3RjYS5weXRob250ZXN0Lm5ldC90ZXN0 +Y2EvcmV2b2NhdGlvbi5jcmwwDQYJKoZIhvcNAQELBQADggGBAMo0usXQzycxMtYN +JzC42xfftzmnu7E7hsQx/fur22MazJCruU6rNEkMXow+cKOnay+nmiV7AVoYlkh2 ++DZ4dPq8fWh/5cqmnXvccr2jJVEXaOjp1wKGLH0WfLXcRLIK4/fJM6NRNoO81HDN +hJGfBrot0gUKZcPZVQmouAlpu5OGwrfCkHR8v/BdvA5jE4zr+g/x+uUScE0M64wu +okJCAAQP/PkfQZxjePBmk7KPLuiTHFDLLX+2uldvUmLXOQsJgqumU03MBT4Z8NTA +zqmtEM65ceSP8lo8Zbrcy+AEkCulFaZ92tyjtbe8oN4wTmTLFw06oFLSZzuiOgDV +OaphdVKf/pvA6KBpr6izox0KQFIE5z3AAJZfKzMGDDD20xhy7jjQZNMAhjfsT+k4 +SeYB/6KafNxq08uoulj7w4Z4R/EGpkXnU96ZHYHmvGN0RnxwI1cpYHCazG8AjsK/ +anN9brBi5twTGrn+D8LRBqF5Yn+2MKkD0EdXJdtIENHP+32sPQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 2c383096ed0227..9c415bd7d1c4e4 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -103,6 +103,7 @@ def data_file(*name): # Two keys and certs signed by the same CA (for SNI tests) SIGNED_CERTFILE = data_file("keycert3.pem") +SINGED_CERTFILE_ONLY = data_file("cert3.pem") SIGNED_CERTFILE_HOSTNAME = 'localhost' SIGNED_CERTFILE_INFO = { @@ -4725,6 +4726,13 @@ def test_certificate_chain(self): server_chain=False ) server = ThreadedEchoServer(context=server_context, chatty=False) + + with open(SIGNING_CA) as f: + expected_ca_cert = ssl.PEM_cert_to_DER_cert(f.read()) + + with open(SINGED_CERTFILE_ONLY) as f: + expected_ee_cert = ssl.PEM_cert_to_DER_cert(f.read()) + with server: with client_context.wrap_socket( socket.socket(), @@ -4735,9 +4743,14 @@ def test_certificate_chain(self): self.assertEqual(len(vc), 2) ee, ca = vc + self.assertIsInstance(ee, bytes) + self.assertIsInstance(ca, bytes) + self.assertEqual(expected_ca_cert, ca) + self.assertEqual(expected_ee_cert, ee) uvc = s.get_unverified_chain() self.assertEqual(len(uvc), 1) + self.assertIsInstance(uvc[0], bytes) self.assertEqual(ee, uvc[0]) self.assertNotEqual(ee, ca)