diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py index e67bd13db..94eca1439 100644 --- a/OpenSSL/SSL.py +++ b/OpenSSL/SSL.py @@ -231,6 +231,36 @@ def wrapper(ok, store_ctx): "int (*)(int, X509_STORE_CTX *)", wrapper) +class _RawVerifyHelper(_CallbackExceptionHelper): + """ + Wrap a callback such that it can be used as a certificate verification + callback without the extra wrapping of ``_VerifyHelper``. + """ + def __init__(self, callback): + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ok, store_ctx): + index = _lib.SSL_get_ex_data_X509_STORE_CTX_idx() + ssl = _lib.X509_STORE_CTX_get_ex_data(store_ctx, index) + connection = Connection._reverse_mapping[ssl] + + try: + result = callback(connection, store_ctx) + except Exception as e: + self._problems.append(e) + return 0 + else: + if result: + _lib.X509_STORE_CTX_set_error(store_ctx, _lib.X509_V_OK) + return 1 + else: + return 0 + + self.callback = _ffi.callback( + "int (*)(int, X509_STORE_CTX *)", wrapper) + + class _NpnAdvertiseHelper(_CallbackExceptionHelper): """ Wrap a callback such that it can be used as an NPN advertisement callback. @@ -753,6 +783,33 @@ def set_verify(self, mode, callback): _lib.SSL_CTX_set_verify(self._context, mode, self._verify_callback) + def set_raw_verify(self, mode, callback): + """ + Set the verify mode and verify callback. Unlike set_verify, this method + does not provide you with any extra help to pull out useful features: + you are exposed directly to OpenSSL objects and functions. + + This is an advanced function, and should almost never be used: instead, + use set_verify. + + :param mode: The verify mode, this is either VERIFY_NONE or + VERIFY_PEER combined with possible other flags + :param callback: The Python callback to use + :return: None + + See SSL_CTX_set_verify(3SSL) for further details. + """ + if not isinstance(mode, integer_types): + raise TypeError("mode must be an integer") + + if not callable(callback): + raise TypeError("callback must be callable") + + self._verify_helper = _RawVerifyHelper(callback) + self._verify_callback = self._verify_helper.callback + _lib.SSL_CTX_set_verify(self._context, mode, self._verify_callback) + + def set_verify_depth(self, depth): """ Set the verify depth diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py index 1f231c9c0..a5e31915d 100644 --- a/OpenSSL/test/test_ssl.py +++ b/OpenSSL/test/test_ssl.py @@ -2017,6 +2017,97 @@ def test_alpn_not_implemented(self): +class RawVerifyTests(TestCase, _LoopbackMixin): + """ + Unit tests for the raw verification helper. + """ + def test_raw_verify_success(self): + """ + Test that returning True from the verify helper allows verification to + succeed. + """ + call_args = [] + def verify(conn, context): + call_args.append((conn, context)) + return True + + serverContext = Context(TLSv1_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) + server = Connection(serverContext, None) + server.set_accept_state() + + # Create the client + clientContext = Context(TLSv1_METHOD) + clientContext.set_raw_verify(VERIFY_PEER, verify) + client = Connection(clientContext, None) + client.set_connect_state() + + self._interactInMemory(client, server) + + # The verify function is called once per cert. + self.assertEqual(len(call_args), 2) + # The first argument is the connection object. + self.assertEqual(client, call_args[0][0]) + + + def test_raw_verify_fail(self): + """ + Returning False from the verify helper destroys the connection. + """ + call_args = [] + def verify(conn, context): + call_args.append((conn, context)) + return False + + serverContext = Context(TLSv1_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) + server = Connection(serverContext, None) + server.set_accept_state() + + # Create the client + clientContext = Context(TLSv1_METHOD) + clientContext.set_raw_verify(VERIFY_PEER, verify) + client = Connection(clientContext, None) + client.set_connect_state() + + self.assertRaises(Error, self._interactInMemory, server, client) + + # The verify function is called only once here. + self.assertEqual(len(call_args), 1) + + + def test_raw_verify_exception(self): + """ + Throwing exceptions from the verify helper destroys the connection. + """ + def verify(conn, context): + raise ValueError("Whoops!") + return True + + serverContext = Context(TLSv1_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) + server = Connection(serverContext, None) + server.set_accept_state() + + # Create the client + clientContext = Context(TLSv1_METHOD) + clientContext.set_raw_verify(VERIFY_PEER, verify) + client = Connection(clientContext, None) + client.set_connect_state() + + self.assertRaises(ValueError, self._interactInMemory, server, client) + + + class SessionTests(TestCase): """ Unit tests for :py:obj:`OpenSSL.SSL.Session`. diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst index 292930506..5b42f55ec 100644 --- a/doc/api/ssl.rst +++ b/doc/api/ssl.rst @@ -421,6 +421,26 @@ Context objects have the following methods: and false otherwise. +.. py:method:: Context.set_raw_verify(mode, callback) + + A version of :py:meth:`set_verify` that provides OpenSSL objects directly + to *callback*, rather than unwrapping them. This is an expert-level + function and should only be used when you know what you're doing: in almost + all cases prefer :py:meth:`set_verify`. + + Set the verification flags for this Context object to *mode* and specify + that *callback* should be used for verification callbacks. *mode* should be + one of :py:const:`VERIFY_NONE` and :py:const:`VERIFY_PEER`. If + :py:const:`VERIFY_PEER` is used, *mode* can be OR:ed with + :py:const:`VERIFY_FAIL_IF_NO_PEER_CERT` and :py:const:`VERIFY_CLIENT_ONCE` + to further control the behaviour. + + *callback* should take two arguments: A Connection object, and an OpenSSL + X509 Store Context object. This second object can only be manipulated using + functions provided by the ``cryptography`` project. *callback* should + return true if verification passes and false otherwise. + + .. py:method:: Context.set_verify_depth(depth) Set the maximum depth for the certificate chain verification that shall be