From 2a6e3b8998c1c7f0c2c62b71c7e31c94465583cb Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 6 Sep 2017 14:49:50 -0700 Subject: [PATCH] bpo-31372: Expose SSL verify result Signed-off-by: Christian Heimes --- Doc/library/ssl.rst | 25 ++++ Lib/ssl.py | 18 +++ Lib/test/test_ssl.py | 43 +++++++ .../2018-02-25-09-38-22.bpo-31372.m_3hY5.rst | 1 + Modules/_ssl.c | 113 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-02-25-09-38-22.bpo-31372.m_3hY5.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 20f5724447164d..f91b707b5742d4 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -640,6 +640,15 @@ Constants .. versionadded:: 3.6 +.. class:: VerifyResult + + :class:`enum.IntEnum` collection of verify results used by + :attr:`SSLSocket.verify_result`. Verify result ``V_OK`` means success, + all other values are errors. Some results are only available with + OpenSSL 1.1.0 or newer. + + .. versionadded:: 3.7 + .. data:: PROTOCOL_TLS Selects the highest protocol version that both the client and server support. @@ -1391,6 +1400,22 @@ SSL sockets also have the following additional methods and attributes: .. versionadded:: 3.6 +.. attribute:: SSLSocket.verify_result + + The result of chain verification as data:`VerifyResult`, message tuple. + The property raises an :exc:`SSLError` exception if TLS connection hasn't + been established yet or the peer hasn't send a certificate. The property + can be used in combination with :data:`CERT_NONE` to check if OpenSSL has + successfully validated the certificate:: + + >>> ctx = ssl.SSLContext(ssl.PROTOCOL_TLS) + >>> ctx.verify_mode = ssl.CERT_NONE + >>> sock = ctx.wrap_socket(conn, server_hostname='www.example.org') + >>> sock.verify_result + (, 'ok') + + .. versionadded:: 3.7 + SSL Contexts ------------ diff --git a/Lib/ssl.py b/Lib/ssl.py index 793ed496c77af4..26234862ff67ac 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -149,6 +149,12 @@ lambda name: name.startswith('CERT_'), source=_ssl) +_IntEnum._convert( + 'VerifyResult', __name__, + lambda name: name.startswith('V_'), + source=_ssl) + + PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS _PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()} @@ -695,6 +701,11 @@ def server_hostname(self): server hostame is set.""" return self._sslobj.server_hostname + @property + def verify_result(self): + errcode, msg = self._sslobj.verify_result + return VerifyResult(errcode), msg + def read(self, len=1024, buffer=None): """Read up to 'len' bytes from the SSL object and return them. @@ -890,6 +901,13 @@ def session_reused(self): if self._sslobj is not None: return self._sslobj.session_reused + @property + def verify_result(self): + if self._sslobj is not None: + return self._sslobj.verify_result + else: + raise SSLError("Connection is not established yet.") + def dup(self): raise NotImplementedError("Can't dup() %s instances" % self.__class__.__name__) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index d48d6e5569fc3e..27d25df193843a 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -3075,6 +3075,49 @@ def test_ssl_cert_verify_error(self): self.assertIn(msg, repr(e)) self.assertIn('certificate verify failed', repr(e)) + def test_ssl_cert_verify_result(self): + if support.verbose: + sys.stdout.write("\n") + + server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + server_context.load_cert_chain(SIGNED_CERTFILE) + + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + server = ThreadedEchoServer(context=server_context, chatty=True) + with server: + with context.wrap_socket( + socket.socket(), + do_handshake_on_connect=False, + server_hostname=SIGNED_CERTFILE_HOSTNAME) as s: + with self.assertRaises(ssl.SSLError): + s.verify_result + s.connect((HOST, server.port)) + with self.assertRaises(ssl.SSLError): + s.verify_result + + s.do_handshake() # no error, CERT_NONE + self.assertEqual( + s.verify_result, + (ssl.VerifyResult.V_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, + 'unable to get local issuer certificate') + ) + + context.load_verify_locations(SIGNING_CA) + + server = ThreadedEchoServer(context=server_context, chatty=True) + with server: + with context.wrap_socket( + socket.socket(), + server_hostname=SIGNED_CERTFILE_HOSTNAME) as s: + s.connect((HOST, server.port)) + self.assertEqual( + s.verify_result, + (ssl.VerifyResult.V_OK, 'ok') + ) + @unittest.skipUnless(hasattr(ssl, 'PROTOCOL_SSLv2'), "OpenSSL is compiled without SSLv2 support") def test_protocol_sslv2(self): diff --git a/Misc/NEWS.d/next/Library/2018-02-25-09-38-22.bpo-31372.m_3hY5.rst b/Misc/NEWS.d/next/Library/2018-02-25-09-38-22.bpo-31372.m_3hY5.rst new file mode 100644 index 00000000000000..e70ccd5256b8da --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-02-25-09-38-22.bpo-31372.m_3hY5.rst @@ -0,0 +1 @@ +Expose OpenSSL verify result diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 390a1af1e59de7..60e04997d54ee9 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2833,6 +2833,34 @@ PySSL_get_session_reused(PySSLSocket *self, void *closure) { PyDoc_STRVAR(PySSL_get_session_reused_doc, "Was the client session reused during handshake?"); +static PyObject * +PySSL_get_verify_result(PySSLSocket *self, void *closure) { + long result; + const char *msg; + + if (!SSL_is_init_finished(self->ssl)) { + _setSSLError("Connection is not established yet.", 0, __FILE__, + __LINE__); + return NULL; + } + + if (SSL_get_peer_certificate(self->ssl) == NULL) { + _setSSLError("Peer did not sent a certificate.", 0, __FILE__, + __LINE__); + return NULL; + } + result = SSL_get_verify_result(self->ssl); + msg = X509_verify_cert_error_string(result); + return Py_BuildValue("ls", result, msg); +} + +PyDoc_STRVAR(PySSL_get_verify_result_doc, +"Get certificate validation result\n\ +\n\ +Returns (error_code, error_message) or raises an exception if connection\n\ +has't been established yet. Error code 0 means success."); + + static PyGetSetDef ssl_getsetlist[] = { {"context", (getter) PySSL_get_context, (setter) PySSL_set_context, PySSL_set_context_doc}, @@ -2846,6 +2874,8 @@ static PyGetSetDef ssl_getsetlist[] = { (setter) PySSL_set_session, PySSL_set_session_doc}, {"session_reused", (getter) PySSL_get_session_reused, NULL, PySSL_get_session_reused_doc}, + {"verify_result", (getter) PySSL_get_verify_result, NULL, + PySSL_get_verify_result_doc}, {NULL}, /* sentinel */ }; @@ -6142,6 +6172,89 @@ PyInit__ssl(void) addbool(m, "HAS_TLSv1_3", 0); #endif + /* Verify result */ + PyModule_AddIntConstant(m, "V_OK", X509_V_OK); +#ifdef X509_V_ERR_UNSPECIFIED + PyModule_AddIntConstant(m, "V_UNSPECIFIED", X509_V_ERR_UNSPECIFIED); +#endif + PyModule_AddIntConstant(m, "V_UNABLE_TO_GET_ISSUER_CERT", X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT); + PyModule_AddIntConstant(m, "V_UNABLE_TO_GET_CRL", X509_V_ERR_UNABLE_TO_GET_CRL); + PyModule_AddIntConstant(m, "V_UNABLE_TO_DECRYPT_CERT_SIGNATURE", X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE); + PyModule_AddIntConstant(m, "V_UNABLE_TO_DECRYPT_CRL_SIGNATURE", X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE); + PyModule_AddIntConstant(m, "V_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY); + PyModule_AddIntConstant(m, "V_CERT_SIGNATURE_FAILURE", X509_V_ERR_CERT_SIGNATURE_FAILURE); + PyModule_AddIntConstant(m, "V_CRL_SIGNATURE_FAILURE", X509_V_ERR_CRL_SIGNATURE_FAILURE); + PyModule_AddIntConstant(m, "V_CERT_NOT_YET_VALID", X509_V_ERR_CERT_NOT_YET_VALID); + PyModule_AddIntConstant(m, "V_CERT_HAS_EXPIRED", X509_V_ERR_CERT_HAS_EXPIRED); + PyModule_AddIntConstant(m, "V_CRL_NOT_YET_VALID", X509_V_ERR_CRL_NOT_YET_VALID); + PyModule_AddIntConstant(m, "V_CRL_HAS_EXPIRED", X509_V_ERR_CRL_HAS_EXPIRED); + PyModule_AddIntConstant(m, "V_ERROR_IN_CERT_NOT_BEFORE_FIELD", X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD); + PyModule_AddIntConstant(m, "V_ERROR_IN_CERT_NOT_AFTER_FIELD", X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD); + PyModule_AddIntConstant(m, "V_ERROR_IN_CRL_LAST_UPDATE_FIELD", X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD); + PyModule_AddIntConstant(m, "V_ERROR_IN_CRL_NEXT_UPDATE_FIELD", X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD); + PyModule_AddIntConstant(m, "V_OUT_OF_MEM", X509_V_ERR_OUT_OF_MEM); + PyModule_AddIntConstant(m, "V_DEPTH_ZERO_SELF_SIGNED_CERT", X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT); + PyModule_AddIntConstant(m, "V_SELF_SIGNED_CERT_IN_CHAIN", X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN); + PyModule_AddIntConstant(m, "V_UNABLE_TO_GET_ISSUER_CERT_LOCALLY", X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY); + PyModule_AddIntConstant(m, "V_UNABLE_TO_VERIFY_LEAF_SIGNATURE", X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE); + PyModule_AddIntConstant(m, "V_CERT_CHAIN_TOO_LONG", X509_V_ERR_CERT_CHAIN_TOO_LONG); + PyModule_AddIntConstant(m, "V_CERT_REVOKED", X509_V_ERR_CERT_REVOKED); + PyModule_AddIntConstant(m, "V_INVALID_CA", X509_V_ERR_INVALID_CA); + PyModule_AddIntConstant(m, "V_PATH_LENGTH_EXCEEDED", X509_V_ERR_PATH_LENGTH_EXCEEDED); + PyModule_AddIntConstant(m, "V_INVALID_PURPOSE", X509_V_ERR_INVALID_PURPOSE); + PyModule_AddIntConstant(m, "V_CERT_UNTRUSTED", X509_V_ERR_CERT_UNTRUSTED); + PyModule_AddIntConstant(m, "V_CERT_REJECTED", X509_V_ERR_CERT_REJECTED); + PyModule_AddIntConstant(m, "V_SUBJECT_ISSUER_MISMATCH", X509_V_ERR_SUBJECT_ISSUER_MISMATCH); + PyModule_AddIntConstant(m, "V_AKID_SKID_MISMATCH", X509_V_ERR_AKID_SKID_MISMATCH); + PyModule_AddIntConstant(m, "V_AKID_ISSUER_SERIAL_MISMATCH", X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH); + PyModule_AddIntConstant(m, "V_KEYUSAGE_NO_CERTSIGN", X509_V_ERR_KEYUSAGE_NO_CERTSIGN); + PyModule_AddIntConstant(m, "V_UNABLE_TO_GET_CRL_ISSUER", X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER); + PyModule_AddIntConstant(m, "V_UNHANDLED_CRITICAL_EXTENSION", X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION); + PyModule_AddIntConstant(m, "V_KEYUSAGE_NO_CRL_SIGN", X509_V_ERR_KEYUSAGE_NO_CRL_SIGN); + PyModule_AddIntConstant(m, "V_UNHANDLED_CRITICAL_CRL_EXTENSION", X509_V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION); + PyModule_AddIntConstant(m, "V_INVALID_NON_CA", X509_V_ERR_INVALID_NON_CA); + PyModule_AddIntConstant(m, "V_PROXY_PATH_LENGTH_EXCEEDED", X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED); + PyModule_AddIntConstant(m, "V_KEYUSAGE_NO_DIGITAL_SIGNATURE", X509_V_ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE); + PyModule_AddIntConstant(m, "V_PROXY_CERTIFICATES_NOT_ALLOWED", X509_V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED); + PyModule_AddIntConstant(m, "V_INVALID_EXTENSION", X509_V_ERR_INVALID_EXTENSION); + PyModule_AddIntConstant(m, "V_INVALID_POLICY_EXTENSION", X509_V_ERR_INVALID_POLICY_EXTENSION); + PyModule_AddIntConstant(m, "V_NO_EXPLICIT_POLICY", X509_V_ERR_NO_EXPLICIT_POLICY); + PyModule_AddIntConstant(m, "V_DIFFERENT_CRL_SCOPE", X509_V_ERR_DIFFERENT_CRL_SCOPE); + PyModule_AddIntConstant(m, "V_UNSUPPORTED_EXTENSION_FEATURE", X509_V_ERR_UNSUPPORTED_EXTENSION_FEATURE); + PyModule_AddIntConstant(m, "V_UNNESTED_RESOURCE", X509_V_ERR_UNNESTED_RESOURCE); + PyModule_AddIntConstant(m, "V_PERMITTED_VIOLATION", X509_V_ERR_PERMITTED_VIOLATION); + PyModule_AddIntConstant(m, "V_EXCLUDED_VIOLATION", X509_V_ERR_EXCLUDED_VIOLATION); + PyModule_AddIntConstant(m, "V_SUBTREE_MINMAX", X509_V_ERR_SUBTREE_MINMAX); + PyModule_AddIntConstant(m, "V_APPLICATION_VERIFICATION", X509_V_ERR_APPLICATION_VERIFICATION); + PyModule_AddIntConstant(m, "V_UNSUPPORTED_CONSTRAINT_TYPE", X509_V_ERR_UNSUPPORTED_CONSTRAINT_TYPE); + PyModule_AddIntConstant(m, "V_UNSUPPORTED_CONSTRAINT_SYNTAX", X509_V_ERR_UNSUPPORTED_CONSTRAINT_SYNTAX); + PyModule_AddIntConstant(m, "V_UNSUPPORTED_NAME_SYNTAX", X509_V_ERR_UNSUPPORTED_NAME_SYNTAX); + PyModule_AddIntConstant(m, "V_CRL_PATH_VALIDATION_ERROR", X509_V_ERR_CRL_PATH_VALIDATION_ERROR); +#ifdef X509_V_ERR_PATH_LOOP + PyModule_AddIntConstant(m, "V_PATH_LOOP", X509_V_ERR_PATH_LOOP); +#endif +#ifdef VERIFY_RESULT_SUITE_B_INVALID_VERSION + PyModule_AddIntConstant(m, "V_SUITE_B_INVALID_VERSION", X509_V_ERR_SUITE_B_INVALID_VERSION); + PyModule_AddIntConstant(m, "V_SUITE_B_INVALID_ALGORITHM", X509_V_ERR_SUITE_B_INVALID_ALGORITHM); + PyModule_AddIntConstant(m, "V_SUITE_B_INVALID_CURVE", X509_V_ERR_SUITE_B_INVALID_CURVE); + PyModule_AddIntConstant(m, "V_SUITE_B_INVALID_SIGNATURE_ALGORITHM", X509_V_ERR_SUITE_B_INVALID_SIGNATURE_ALGORITHM); + PyModule_AddIntConstant(m, "V_SUITE_B_LOS_NOT_ALLOWED", X509_V_ERR_SUITE_B_LOS_NOT_ALLOWED); + PyModule_AddIntConstant(m, "V_SUITE_B_CANNOT_SIGN_P_384_WITH_P_256", X509_V_ERR_SUITE_B_CANNOT_SIGN_P_384_WITH_P_256); +#endif + PyModule_AddIntConstant(m, "V_HOSTNAME_MISMATCH", X509_V_ERR_HOSTNAME_MISMATCH); + PyModule_AddIntConstant(m, "V_EMAIL_MISMATCH", X509_V_ERR_EMAIL_MISMATCH); + PyModule_AddIntConstant(m, "V_IP_ADDRESS_MISMATCH", X509_V_ERR_IP_ADDRESS_MISMATCH); +#ifdef OPENSSL_VERSION_1_1 + PyModule_AddIntConstant(m, "V_DANE_NO_MATCH", X509_V_ERR_DANE_NO_MATCH); + PyModule_AddIntConstant(m, "V_EE_KEY_TOO_SMALL", X509_V_ERR_EE_KEY_TOO_SMALL); + PyModule_AddIntConstant(m, "V_CA_KEY_TOO_SMALL", X509_V_ERR_CA_KEY_TOO_SMALL); + PyModule_AddIntConstant(m, "V_CA_MD_TOO_WEAK", X509_V_ERR_CA_MD_TOO_WEAK); + PyModule_AddIntConstant(m, "V_INVALID_CALL", X509_V_ERR_INVALID_CALL); + PyModule_AddIntConstant(m, "V_STORE_LOOKUP", X509_V_ERR_STORE_LOOKUP); + PyModule_AddIntConstant(m, "V_NO_VALID_SCTS", X509_V_ERR_NO_VALID_SCTS); + PyModule_AddIntConstant(m, "V_PROXY_SUBJECT_NAME_VIOLATION", X509_V_ERR_PROXY_SUBJECT_NAME_VIOLATION); +#endif + /* Mappings for error codes */ err_codes_to_names = PyDict_New(); err_names_to_codes = PyDict_New();