diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 8842704f..7b751678 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -11,6 +11,7 @@ on: jobs: build: + name: test-${{ matrix.python }} runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 218c6746..95f284e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,23 @@ Changelog ========= +This PR +---------- +* Deprecated pyOpenSSL in favor of Cryptography. +* Deprecated the following for removal in the next scheduled release: + `josepy.util.ComparableX509` +* The following functions now accept a `josepy.util.ComparableX509` OR a + `cryptography.x509.Certificate`: + - `josepy.json_util.encode_cert` +* The following functions now accept a `josepy.util.ComparableX509` OR a + `cryptography.x509.CertificateSigningRequest`: + - `josepy.json_util.encode_csr` +* Added the following functions: + - `josepy.json_util.decode_cert_cryptography` + - `josepy.json_util.decode_csr_cryptography` +* x5c headers are now raw Cryptography objects + + 1.15.0 (master) --------------- * Added support for Python 3.13. diff --git a/poetry.lock b/poetry.lock index 29c29b2a..290c4c33 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "alabaster" @@ -1622,4 +1622,4 @@ docs = ["sphinx", "sphinx-rtd-theme"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "7fa22898e575497d4261ebca5256b36bd6576e1cca2d01bd7e5d050a0d43fd3e" +content-hash = "ba9a13bdc8bce224e4fd5fd61af862f4f8e40106f35dd922093c38a6870854c8" diff --git a/pyproject.toml b/pyproject.toml index 61bb818d..0b91ac4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,11 @@ python = "^3.8" # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) # add sign() and verify() to asymetric keys (RSA >=1.4, ECDSA >=1.5) -cryptography = ">=1.5" +# add not_valid_after_utc() in (>=42.0.0) +cryptography = ">=42.0.0" # Connection.set_tlsext_host_name (>=0.13) -pyopenssl = ">=0.13" +# incompatibilities with cryptography; X509_V_FLAG_NOTIFY_POLICY removed (>=23.2.0) +pyopenssl = ">=23.2.0" # >=4.3.0 is needed for Python 3.10 support sphinx = {version = ">=4.3.0", optional = true} sphinx-rtd-theme = {version = ">=1.0", optional = true} diff --git a/src/josepy/__init__.py b/src/josepy/__init__.py index dde2fb6e..b259d225 100644 --- a/src/josepy/__init__.py +++ b/src/josepy/__init__.py @@ -44,7 +44,9 @@ TypedJSONObjectWithFields, decode_b64jose, decode_cert, + decode_cert_cryptography, decode_csr, + decode_csr_cryptography, decode_hex16, encode_b64jose, encode_cert, diff --git a/src/josepy/json_util.py b/src/josepy/json_util.py index ed4e4811..98f24892 100644 --- a/src/josepy/json_util.py +++ b/src/josepy/json_util.py @@ -20,9 +20,11 @@ Optional, Type, TypeVar, + Union, ) -from OpenSSL import crypto +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding from josepy import b64, errors, interfaces, util @@ -426,59 +428,104 @@ def decode_hex16(value: str, size: Optional[int] = None, minimum: bool = False) raise errors.DeserializationError(error) -def encode_cert(cert: util.ComparableX509) -> str: +def encode_cert(cert: Union[util.ComparableX509, x509.Certificate]) -> str: """Encode certificate as JOSE Base-64 DER. - :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` + :type cert: `cryptography.x509.Certificate` :rtype: unicode """ - if isinstance(cert.wrapped, crypto.X509Req): + if isinstance(cert, util.ComparableX509): + # DEPRECATED; remove in next release + util.warn_deprecated( + """`josepy.json_util.encode_cert` has deprecated support for""" + """util.ComparableX509 objects. Please use """ + """`cryptography.x509.Certificate` objects instead.""" + ) + if isinstance(cert.wrapped_new, x509.CertificateSigningRequest): + raise ValueError("Error input is actually a certificate request.") + return encode_b64jose(cert.wrapped_new.public_bytes(Encoding.DER)) + if isinstance(cert, x509.CertificateSigningRequest): raise ValueError("Error input is actually a certificate request.") - - return encode_b64jose(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) + return encode_b64jose(cert.public_bytes(Encoding.DER)) def decode_cert(b64der: str) -> util.ComparableX509: """Decode JOSE Base-64 DER-encoded certificate. :param unicode b64der: - :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` + :rtype: `cryptography.x509.Certificate` wrapped in `.ComparableX509` """ + util.warn_deprecated( + """`josepy.json_util.decode_cert` has been deprecated and will be removed""" + """in a future release. please use `josepy.json_util.decode_cert_cryptography`""" + """instead.""" + ) try: - return util.ComparableX509( - crypto.load_certificate(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) - ) - except crypto.Error as error: + return util.ComparableX509(x509.load_der_x509_certificate(decode_b64jose(b64der))) + except Exception as error: + raise errors.DeserializationError(error) + + +def decode_cert_cryptography(b64der: str) -> x509.Certificate: + """Decode JOSE Base-64 DER-encoded certificate. + + :param unicode b64der: + :rtype: `cryptography.x509.Certificate` + + """ + try: + return x509.load_der_x509_certificate(decode_b64jose(b64der)) + except Exception as error: raise errors.DeserializationError(error) -def encode_csr(csr: util.ComparableX509) -> str: +def encode_csr(csr: Union[util.ComparableX509, x509.CertificateSigningRequest]) -> str: """Encode CSR as JOSE Base-64 DER. - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + :type csr: `cryptography.x509.CertificateSigningRequest` :rtype: unicode """ - if isinstance(csr.wrapped, crypto.X509): + if isinstance(csr, util.ComparableX509): + # DEPRECATED; remove in next release + util.warn_deprecated( + """`josepy.json_util.encode_csr` has deprecated support for""" + """util.ComparableX509 objects. Please use """ + """`cryptography.x509.CertificateSigningRequest` objects instead.""" + ) + if isinstance(csr.wrapped_new, x509.Certificate): + raise ValueError("Error input is actually a certificate.") + return encode_b64jose(csr.wrapped_new.public_bytes(Encoding.DER)) + if isinstance(csr, x509.Certificate): raise ValueError("Error input is actually a certificate.") - - return encode_b64jose(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, csr.wrapped)) + return encode_b64jose(csr.public_bytes(Encoding.DER)) def decode_csr(b64der: str) -> util.ComparableX509: """Decode JOSE Base-64 DER-encoded CSR. :param unicode b64der: - :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + :rtype: `cryptography.x509.CertificateSigningRequest` wrapped in `.ComparableX509` """ try: - return util.ComparableX509( - crypto.load_certificate_request(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) - ) - except crypto.Error as error: + return util.ComparableX509(x509.load_der_x509_csr(decode_b64jose(b64der))) + except Exception as error: + raise errors.DeserializationError(error) + + +def decode_csr_cryptography(b64der: str) -> x509.CertificateSigningRequest: + """Decode JOSE Base-64 DER-encoded CSR. + + :param unicode b64der: + :rtype: `cryptography.x509.CertificateSigningRequest` + + """ + try: + return x509.load_der_x509_csr(decode_b64jose(b64der)) + except Exception as error: raise errors.DeserializationError(error) diff --git a/src/josepy/jws.py b/src/josepy/jws.py index 6bfe9ce9..eb0c87ae 100644 --- a/src/josepy/jws.py +++ b/src/josepy/jws.py @@ -12,10 +12,12 @@ Optional, Tuple, Type, + Union, cast, ) -from OpenSSL import crypto +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding import josepy from josepy import b64, errors, json_util, jwa @@ -80,7 +82,10 @@ class Header(json_util.JSONObjectWithFields): ) kid: Optional[str] = json_util.field("kid", omitempty=True) x5u: Optional[bytes] = json_util.field("x5u", omitempty=True) - x5c: Tuple[util.ComparableX509, ...] = json_util.field("x5c", omitempty=True, default=()) + x5c: Union[ + Tuple[util.ComparableX509, ...], + Tuple[x509.Certificate, ...], + ] = json_util.field("x5c", omitempty=True, default=()) x5t: Optional[bytes] = json_util.field("x5t", decoder=json_util.decode_b64jose, omitempty=True) x5tS256: Optional[bytes] = json_util.field( "x5t#S256", decoder=json_util.decode_b64jose, omitempty=True @@ -138,21 +143,21 @@ def crit(unused_value: Any) -> Any: @x5c.encoder # type: ignore def x5c(value): + # DEPRECATE `cert.wrapped_new` return [ - base64.b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) + ( + base64.b64encode(cert.wrapped_new.public_bytes(Encoding.DER)) + if isinstance(cert, util.ComparableX509) + else base64.b64encode(cert.public_bytes(Encoding.DER)) + ) for cert in value ] @x5c.decoder # type: ignore def x5c(value): try: - return tuple( - util.ComparableX509( - crypto.load_certificate(crypto.FILETYPE_ASN1, base64.b64decode(cert)) - ) - for cert in value - ) - except crypto.Error as error: + return tuple(x509.load_der_x509_certificate(base64.b64decode(cert)) for cert in value) + except Exception as error: raise errors.DeserializationError(error) diff --git a/src/josepy/util.py b/src/josepy/util.py index 3af88e75..225ba097 100644 --- a/src/josepy/util.py +++ b/src/josepy/util.py @@ -5,28 +5,125 @@ import warnings from collections.abc import Hashable, Mapping from types import ModuleType -from typing import Any, Callable, Iterator, List, Tuple, TypeVar, Union, cast - +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterator, + List, + Tuple, + TypeVar, + Union, + cast, +) + +from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.serialization import Encoding from OpenSSL import crypto +def warn_deprecated(message: str) -> None: + # used to warn for deprecation + warnings.warn(message, DeprecationWarning, stacklevel=2) + + # Deprecated. Please use built-in decorators @classmethod and abc.abstractmethod together instead. def abstractclassmethod(func: Callable) -> classmethod: return classmethod(abc.abstractmethod(func)) class ComparableX509: - """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__. + """A wrapper for OpenSSL.crypto.X509** objects that supports __eq__. + + ComparableX509 objects may be instantiated with a legacy OpenSSL.crypto + object OR their modern cryptography.x509 equivalents. + + This class is deprecated and will be removed in a future release. Projects + should migrate to using raw cryptography.x509 objects instead. + - :ivar wrapped: Wrapped certificate or certificate request. - :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. + :ivar wrapped: Wrapped certificate or certificate request as a + `OpenSSL.crypto` object. + :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509` + :ivar wrapped_legacy: actual storage for wrapped pyopenssl objects + :type wrapped_legacy: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req` + :ivar _wrapped_new: actual storage for wrapped cryptography objects + :type _wrapped_new: `cryptography.x509.Certificate` or + `cryptography.x509.CertificateSigningRequest` """ - def __init__(self, wrapped: Union[crypto.X509, crypto.X509Req]) -> None: - assert isinstance(wrapped, crypto.X509) or isinstance(wrapped, crypto.X509Req) - self.wrapped = wrapped + _wrapped_new: Union[x509.Certificate, x509.CertificateSigningRequest, None] = None + _wrapped_legacy: Union[crypto.X509, crypto.X509Req, None] = None + + def __init__( + self, + wrapped: Union[ + crypto.X509, + crypto.X509Req, + x509.Certificate, + x509.CertificateSigningRequest, + ], + ) -> None: + """__init__ has been expanded to accept `Cryptogarphy.x509` objects to + help users migrate. + """ + warn_deprecated( + "`josepy.util.ComparableX509` objects are deprecated and will be " + "removed in a future verison. Please use `Cryptogatphy.x509` objects " + "directly when utilizing functions in this library." + ) + if isinstance(wrapped, (crypto.X509, crypto.X509Req)): + # stash for legacy operations + self._wrapped_legacy = wrapped + else: + # this is a x509 AND we have pyOpenSSL + self._wrapped_new = wrapped + + @property + def wrapped_new( + self, + ) -> Union[ + x509.Certificate, + x509.CertificateSigningRequest, + ]: + if self._wrapped_new is None: + # convert to Cryptography.x509 + der: bytes + if isinstance(self._wrapped_legacy, crypto.X509): + der = crypto.dump_certificate(crypto.FILETYPE_ASN1, self._wrapped_legacy) + self._wrapped_new = x509.load_der_x509_certificate(der) + + elif isinstance(self._wrapped_legacy, crypto.X509Req): + der = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, self._wrapped_legacy) + self._wrapped_new = x509.load_der_x509_csr(der) + if TYPE_CHECKING: + assert isinstance(self._wrapped_new, (x509.Certificate, x509.CertificateSigningRequest)) + return self._wrapped_new + + @property + def wrapped( + self, + ) -> Union[ + crypto.X509, + crypto.X509Req, + ]: + """For backwards compatibility returns a `pyOpenSSL` object, + even if this object was instantiated with a `Cryptogrphy` object. + """ + if self._wrapped_legacy is None: + if isinstance(self.wrapped_new, x509.Certificate): + self._wrapped_legacy = crypto.load_certificate( + crypto.FILETYPE_ASN1, self.wrapped_new.public_bytes(Encoding.DER) + ) + elif isinstance(self.wrapped_new, x509.CertificateSigningRequest): + self._wrapped_legacy = crypto.load_certificate_request( + crypto.FILETYPE_ASN1, self.wrapped_new.public_bytes(Encoding.DER) + ) + if TYPE_CHECKING: + assert isinstance(self._wrapped_legacy, (crypto.X509, crypto.X509Req)) + return self._wrapped_legacy def __getattr__(self, name: str) -> Any: return getattr(self.wrapped, name) @@ -36,18 +133,18 @@ def _dump(self, filetype: int = crypto.FILETYPE_ASN1) -> bytes: :param int filetype: The desired encoding. Should be one of `OpenSSL.crypto.FILETYPE_ASN1`, - `OpenSSL.crypto.FILETYPE_PEM`, or - `OpenSSL.crypto.FILETYPE_TEXT`. + `OpenSSL.crypto.FILETYPE_PEM` :returns: Encoded X509 object. :rtype: bytes """ - if isinstance(self.wrapped, crypto.X509): - return crypto.dump_certificate(filetype, self.wrapped) - - # assert in __init__ makes sure this is X509Req - return crypto.dump_certificate_request(filetype, self.wrapped) + # deprecated `FILETYPE_TEXT` + if filetype not in (crypto.FILETYPE_ASN1, crypto.FILETYPE_PEM): + raise ValueError("filetype `%s` is deprecated") + if filetype == crypto.FILETYPE_ASN1: + return self.wrapped_new.public_bytes(Encoding.DER) + return self.wrapped_new.public_bytes(Encoding.PEM) def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): @@ -58,7 +155,7 @@ def __hash__(self) -> int: return hash((self.__class__, self._dump())) def __repr__(self) -> str: - return "<{0}({1!r})>".format(self.__class__.__name__, self.wrapped) + return "<{0}({1!r})>".format(self.__class__.__name__, self.wrapped_new) class ComparableKey: diff --git a/tests/json_util_test.py b/tests/json_util_test.py index 9e53876c..d65b607f 100644 --- a/tests/json_util_test.py +++ b/tests/json_util_test.py @@ -3,16 +3,20 @@ import itertools import sys import unittest +import warnings from typing import Any, Dict, Mapping from unittest import mock import pytest import test_util +from cryptography import x509 from josepy import errors, interfaces, util -CERT = test_util.load_comparable_cert("cert.pem") -CSR = test_util.load_comparable_csr("csr.pem") +COMPARABLE_CERT = test_util.load_comparable_cert("cert.pem") +COMPARABLE_CSR = test_util.load_comparable_csr("csr.pem") +CERT = test_util.load_cert("cert.pem") +CSR = test_util.load_csr("csr.pem") class FieldTest(unittest.TestCase): @@ -321,30 +325,58 @@ def test_decode_hex16_odd_length(self) -> None: def test_encode_cert(self) -> None: from josepy.json_util import encode_cert + with warnings.catch_warnings(): + # DEPRECATED + warnings.filterwarnings("ignore", category=DeprecationWarning) + assert self.b64_cert == encode_cert(COMPARABLE_CERT) + assert self.b64_cert == encode_cert(CERT) def test_decode_cert(self) -> None: - from josepy.json_util import decode_cert + from josepy.json_util import decode_cert, decode_cert_cryptography + + with warnings.catch_warnings(): + # DEPRECATED + warnings.filterwarnings("ignore", category=DeprecationWarning) + cert_legacy = decode_cert(self.b64_cert) + assert isinstance(cert_legacy, util.ComparableX509) + assert cert_legacy == COMPARABLE_CERT + with pytest.raises(errors.DeserializationError): + decode_cert("") - cert = decode_cert(self.b64_cert) - assert isinstance(cert, util.ComparableX509) + cert = decode_cert_cryptography(self.b64_cert) + assert isinstance(cert, x509.Certificate) assert cert == CERT with pytest.raises(errors.DeserializationError): - decode_cert("") + decode_cert_cryptography("") def test_encode_csr(self) -> None: from josepy.json_util import encode_csr + with warnings.catch_warnings(): + # DEPRECATED + warnings.filterwarnings("ignore", category=DeprecationWarning) + assert self.b64_csr == encode_csr(COMPARABLE_CSR) + assert self.b64_csr == encode_csr(CSR) def test_decode_csr(self) -> None: - from josepy.json_util import decode_csr + from josepy.json_util import decode_csr, decode_csr_cryptography + + with warnings.catch_warnings(): + # DEPRECATED + warnings.filterwarnings("ignore", category=DeprecationWarning) + csr_legacy = decode_csr(self.b64_csr) + assert isinstance(csr_legacy, util.ComparableX509) + assert csr_legacy == COMPARABLE_CSR + with pytest.raises(errors.DeserializationError): + decode_csr("") - csr = decode_csr(self.b64_csr) - assert isinstance(csr, util.ComparableX509) + csr = decode_csr_cryptography(self.b64_csr) + assert isinstance(csr, x509.CertificateSigningRequest) assert csr == CSR with pytest.raises(errors.DeserializationError): - decode_csr("") + decode_csr_cryptography("") class TypedJSONObjectWithFieldsTest(unittest.TestCase): diff --git a/tests/jws_test.py b/tests/jws_test.py index c6608cd2..4e106027 100644 --- a/tests/jws_test.py +++ b/tests/jws_test.py @@ -3,15 +3,18 @@ import base64 import sys import unittest +import warnings from unittest import mock -import OpenSSL import pytest import test_util +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding from josepy import errors, json_util, jwa, jwk -CERT = test_util.load_comparable_cert("cert.pem") +COMPARABLE_CERT = test_util.load_comparable_cert("cert.pem") +CERT = test_util.load_cert("cert.pem") KEY = jwk.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) @@ -70,16 +73,33 @@ def test_crit_decode_always_errors(self) -> None: def test_x5c_decoding(self) -> None: from josepy.jws import Header - header = Header(x5c=(CERT, CERT)) - jobj = header.to_partial_json() - assert isinstance(CERT.wrapped, OpenSSL.crypto.X509) - cert_asn1 = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped) - cert_b64 = base64.b64encode(cert_asn1) - assert jobj == {"x5c": [cert_b64, cert_b64]} - assert header == Header.from_json(jobj) - jobj["x5c"][0] = base64.b64encode(b"xxx" + cert_asn1) - with pytest.raises(errors.DeserializationError): - Header.from_json(jobj) + with warnings.catch_warnings(record=True) as warns: # noqa: F841 + warnings.simplefilter("always") + + # the new way + header = Header(x5c=(CERT, CERT)) + jobj = header.to_partial_json() + assert isinstance(CERT, x509.Certificate) + cert_asn1 = CERT.public_bytes(Encoding.DER) + cert_b64 = base64.b64encode(cert_asn1) + assert jobj == {"x5c": [cert_b64, cert_b64]} + assert header == Header.from_json(jobj) + jobj["x5c"][0] = base64.b64encode(b"xxx" + cert_asn1) + with pytest.raises(errors.DeserializationError): + Header.from_json(jobj) + + # legacy + header_legacy = Header(x5c=(COMPARABLE_CERT, COMPARABLE_CERT)) + jobj = header_legacy.to_partial_json() + assert isinstance(COMPARABLE_CERT.wrapped_new, x509.Certificate) + cert_asn1 = COMPARABLE_CERT.wrapped_new.public_bytes(Encoding.DER) + cert_b64 = base64.b64encode(cert_asn1) + assert jobj == {"x5c": [cert_b64, cert_b64]} + # comparing the NEW header to the object + assert header == Header.from_json(jobj) + jobj["x5c"][0] = base64.b64encode(b"xxx" + cert_asn1) + with pytest.raises(errors.DeserializationError): + Header.from_json(jobj) def test_find_key(self) -> None: assert "foo" == self.header1.find_key() diff --git a/tests/test_util.py b/tests/test_util.py index 1bd60974..2c742e37 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,9 +4,10 @@ import contextlib import os import sys +import warnings from typing import Any -from cryptography.hazmat.backends import default_backend +from cryptography import x509 from cryptography.hazmat.primitives import serialization from OpenSSL import crypto @@ -51,26 +52,34 @@ def _guess_loader(filename: str, loader_pem: Any, loader_der: Any) -> Any: raise ValueError("Loader could not be recognized based on extension") -def load_cert(*names: str) -> crypto.X509: +def load_cert(*names: str) -> x509.Certificate: """Load certificate.""" - loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate(loader, load_vector(*names)) + loader = _guess_loader( + names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate + ) + return loader(load_vector(*names)) def load_comparable_cert(*names: str) -> josepy.util.ComparableX509: """Load ComparableX509 cert.""" - return ComparableX509(load_cert(*names)) + # DEPRECATED + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return ComparableX509(load_cert(*names)) -def load_csr(*names: str) -> crypto.X509Req: +def load_csr(*names: str) -> x509.CertificateSigningRequest: """Load certificate request.""" - loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate_request(loader, load_vector(*names)) + loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr) + return loader(load_vector(*names)) def load_comparable_csr(*names: str) -> josepy.util.ComparableX509: """Load ComparableX509 certificate request.""" - return ComparableX509(load_csr(*names)) + # DEPRECATED + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return ComparableX509(load_csr(*names)) def load_rsa_private_key(*names: str) -> josepy.util.ComparableRSAKey: @@ -78,7 +87,7 @@ def load_rsa_private_key(*names: str) -> josepy.util.ComparableRSAKey: loader = _guess_loader( names[-1], serialization.load_pem_private_key, serialization.load_der_private_key ) - return ComparableRSAKey(loader(load_vector(*names), password=None, backend=default_backend())) + return ComparableRSAKey(loader(load_vector(*names), password=None)) def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: @@ -86,10 +95,30 @@ def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: loader = _guess_loader( names[-1], serialization.load_pem_private_key, serialization.load_der_private_key ) - return ComparableECKey(loader(load_vector(*names), password=None, backend=default_backend())) + return ComparableECKey(loader(load_vector(*names), password=None)) -def load_pyopenssl_private_key(*names: str) -> crypto.PKey: - """Load pyOpenSSL private key.""" +# legacy testing support +# DEPRECATED + + +def load_cert__pyopenssl(*names: str) -> crypto.X509: + """Load certificate.""" loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_privatekey(loader, load_vector(*names)) + return crypto.load_certificate(loader, load_vector(*names)) + + +def load_comparable_cert__pyopenssl(*names: str) -> josepy.util.ComparableX509: + """Load ComparableX509 cert.""" + return ComparableX509(load_cert__pyopenssl(*names)) + + +def load_csr__pyopenssl(*names: str) -> crypto.X509Req: + """Load certificate request.""" + loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) + return crypto.load_certificate_request(loader, load_vector(*names)) + + +def load_comparable_csr__pyopenssl(*names: str) -> josepy.util.ComparableX509: + """Load ComparableX509 certificate request.""" + return ComparableX509(load_csr__pyopenssl(*names)) diff --git a/tests/util_test.py b/tests/util_test.py index 09edab03..09456538 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -7,6 +7,8 @@ import pytest import test_util +from cryptography import x509 +from OpenSSL import crypto class ComparableX509Test(unittest.TestCase): @@ -51,8 +53,70 @@ def test_hash(self) -> None: assert hash(self.cert1) != hash(self.cert_other) def test_repr(self) -> None: - for x509 in self.req1, self.cert1: - assert repr(x509) == "".format(x509.wrapped) + for cert in (self.req1, self.cert1): + assert repr(cert) == "".format(cert.wrapped_new) + + def test_legacy_cert(self) -> None: + # check default + assert isinstance(self.cert1.wrapped, crypto.X509) + # check explicit + assert isinstance(self.cert1._wrapped_legacy, crypto.X509) + assert isinstance(self.cert1.wrapped_new, x509.Certificate) + + def test_legacy_csr(self) -> None: + # check default first, as it will populate + assert isinstance(self.req1.wrapped, crypto.X509Req) + # check explicit + assert isinstance(self.req1._wrapped_legacy, crypto.X509Req) + assert isinstance(self.req1.wrapped_new, x509.CertificateSigningRequest) + + +class ComparableX509LegacyTest(unittest.TestCase): + + def _check_loading_warns(self, warnlist: list) -> bool: + _found = False + for w in warnlist: + if isinstance(w.message, DeprecationWarning): + if isinstance(w.message.args[0], str): + if w.message.args[0].startswith( + "`josepy.util.ComparableX509` objects are deprecated" + ): + _found = True + break + return _found + + """Legacy tests for josepy.util.ComparableX509.""" + + def test_legacy(self) -> None: + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + + # load pyopenssl + cert1 = test_util.load_comparable_cert__pyopenssl("cert.pem") + assert self._check_loading_warns(warns) is True + assert isinstance(cert1.wrapped, crypto.X509) + assert isinstance(cert1.wrapped_new, x509.Certificate) + + # load cryptography + cert2 = test_util.load_comparable_cert("cert.pem") + assert self._check_loading_warns(warns) is True + assert isinstance(cert2.wrapped, crypto.X509) + assert isinstance(cert2.wrapped_new, x509.Certificate) + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + + # load pyopenssl + csr1 = test_util.load_comparable_csr__pyopenssl("csr.pem") + assert self._check_loading_warns(warns) is True + assert isinstance(csr1.wrapped, crypto.X509Req) + assert isinstance(csr1.wrapped_new, x509.CertificateSigningRequest) + + csr2 = test_util.load_comparable_csr("csr.pem") + assert self._check_loading_warns(warns) is True + assert isinstance(csr2.wrapped, crypto.X509Req) + assert isinstance(csr2.wrapped_new, x509.CertificateSigningRequest) class ComparableRSAKeyTest(unittest.TestCase):