diff --git a/src/OpenSSL/crypto.py b/src/OpenSSL/crypto.py index 366007e8..dcee9d61 100644 --- a/src/OpenSSL/crypto.py +++ b/src/OpenSSL/crypto.py @@ -66,6 +66,7 @@ def deprecated(msg: str, **kwargs: object) -> Callable[[_T], _T]: "X509Extension", "X509Name", "X509Req", + "X509Purpose", "X509Store", "X509StoreContext", "X509StoreContextError", @@ -1709,6 +1710,28 @@ class X509StoreFlags: PARTIAL_CHAIN: int = _lib.X509_V_FLAG_PARTIAL_CHAIN +class X509Purpose: + """ + Enumeration of X509 purposes, e.g. used to set the purpose of a + :class:`X509Store`. + + See `OpenSSL check purpose`_ for details. + + .. _OpenSSL check purpose: + https://www.openssl.org/docs/manmaster/man3/X509_check_purpose.html + """ + + X509_PURPOSE_SSL_CLIENT = _lib.X509_PURPOSE_SSL_CLIENT + X509_PURPOSE_SSL_SERVER = _lib.X509_PURPOSE_SSL_SERVER + X509_PURPOSE_NS_SSL_SERVER = _lib.X509_PURPOSE_NS_SSL_SERVER + X509_PURPOSE_SMIME_SIGN = _lib.X509_PURPOSE_SMIME_SIGN + X509_PURPOSE_SMIME_ENCRYPT = _lib.X509_PURPOSE_SMIME_ENCRYPT + X509_PURPOSE_CRL_SIGN = _lib.X509_PURPOSE_CRL_SIGN + X509_PURPOSE_ANY = _lib.X509_PURPOSE_ANY + X509_PURPOSE_OCSP_HELPER = _lib.X509_PURPOSE_OCSP_HELPER + X509_PURPOSE_TIMESTAMP_SIGN = _lib.X509_PURPOSE_TIMESTAMP_SIGN + + class X509Store: """ An X.509 store. @@ -1827,6 +1850,18 @@ def set_time(self, vfy_time: datetime.datetime) -> None: ) _openssl_assert(_lib.X509_STORE_set1_param(self._store, param) != 0) + def set_purpose(self, purpose): + """ + Set purpose of this store. + + .. versionadded:: 26.0.0 + + :param int flags: The verification flags to set on this store. + See :class:`X509StorePurposes` for available constants. + :return: ``None`` if the verification flags were successfully set. + """ + _openssl_assert(_lib.X509_STORE_set_purpose(self._store, purpose) != 0) + def load_locations( self, cafile: StrOrBytesPath | None, diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 7b07b441..b602ed82 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -38,6 +38,7 @@ PKey, X509Extension, X509Name, + X509Purpose, X509Req, X509Store, X509StoreContext, @@ -3007,6 +3008,7 @@ class TestCRL: intermediate_server_key = load_privatekey( FILETYPE_PEM, intermediate_server_key_pem ) + server_cert = load_certificate(FILETYPE_PEM, server_cert_pem) @staticmethod def _make_test_crl_cryptography( @@ -3069,7 +3071,33 @@ def test_verify_with_revoked(self) -> None: store_ctx.verify_certificate() assert str(err.value) == "certificate revoked" - def test_verify_with_missing_crl(self) -> None: + def test_verify_with_correct_purpose(self): + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store.set_purpose(X509Purpose.X509_PURPOSE_SSL_SERVER) + + store_ctx = X509StoreContext(store, self.server_cert) + store_ctx.verify_certificate() + + # The intermediate server certificate has no EKU and so it is fit + # for any purpose + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + store_ctx.verify_certificate() + + def test_verify_with_incorrect_purpose(self): + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store.set_purpose(X509Purpose.X509_PURPOSE_SSL_CLIENT) + + store_ctx = X509StoreContext(store, self.server_cert) + with pytest.raises(X509StoreContextError) as err: + store_ctx.verify_certificate() + + assert err.value.args[0] == "unsupported certificate purpose" + + def test_verify_with_missing_crl(self): """ `verify_certificate` raises error when an intermediate certificate's CRL is missing.