diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index a774bb96..7b751678 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -11,13 +11,12 @@ on: jobs: build: - name: test-${{ matrix.python }}-${{ matrix.build-type }} + name: test-${{ matrix.python }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - build-type: ["noopenssl", "legacy"] steps: - uses: actions/checkout@v3 - name: Setup Python @@ -36,7 +35,7 @@ jobs: - name: Install Poetry & Tox run: pip install poetry>1.0.0 tox>3.3.0 - name: Run tox - run: tox -e github-${{ matrix.build-type }} + run: tox # This job runs our tests like external parties such as packagers. external: runs-on: ubuntu-latest diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6a4479f8..95f284e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,27 +1,29 @@ 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. * Dropped support for Python 3.7. * Support for Python 3.8 has been deprecated and will be removed in the next scheduled release. -* Deprecated pyOpenSSL in favor of Cryptography and removed the required - dependency. The underlying storage format of the `josepy.util.ComparableX509` - has been switched to `cryptography.x509` objects, and the - `josepy.util.ComparableX509.wrapped` attribute will now be either a - `cryptography.x509.Certificate` or `cryptography.x509.CertificateSigningRequest` - - objects from the `opensssl.crypto` package will be automatically transcoded - to their Cryptography counterparts on initialization. A new convenience - attribute, `josepy.util.ComparableX509.wrapped_legacy` will return an - `opensssl.crypto` object for affected projects that are unable to immediately - migrate code to the Cryptography objects. This is offered as a minimally - breaking change to aid in migration to Cryptography. Affected projects should - either pin to `1.14.0` or utilize the new attribute in a "hotfix" release. - Please note, due to the removal of `X509_V_FLAG_NOTIFY_POLICY` in pyOpenSSL - `23.2.0`, projects migrating to the new backend may experience a version - conflict during the code transition. 1.14.0 (2023-11-01) ------------------- diff --git a/poetry.lock b/poetry.lock index 26e5f07b..290c4c33 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1618,9 +1618,8 @@ type = ["pytest-mypy"] [extras] docs = ["sphinx", "sphinx-rtd-theme"] -legacy = ["pyopenssl"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e561f96799ce63a00112b0911daa292b6f80bff61e9f1fd762da17c944561eb4" +content-hash = "ba9a13bdc8bce224e4fd5fd61af862f4f8e40106f35dd922093c38a6870854c8" diff --git a/pyproject.toml b/pyproject.toml index 8f726915..0b91ac4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ python = "^3.8" cryptography = ">=42.0.0" # Connection.set_tlsext_host_name (>=0.13) # incompatibilities with cryptography; X509_V_FLAG_NOTIFY_POLICY removed (>=23.2.0) -pyopenssl = {version = ">=23.2.0", optional=true} +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} @@ -75,17 +75,10 @@ docs = [ "sphinx", "sphinx-rtd-theme", ] -legacy = [ - "pyopenssl", -] [tool.poetry.scripts] jws = "josepy.jws:CLI.run" -# pyopenssl is only needed for testing -[tool.poetry.group.dev.dependencies] -pyopenssl = ">=23.2.0" - # Black tooling configuration [tool.black] line-length = 100 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 193a5aae..98f24892 100644 --- a/src/josepy/json_util.py +++ b/src/josepy/json_util.py @@ -20,6 +20,7 @@ Optional, Type, TypeVar, + Union, ) from cryptography import x509 @@ -427,43 +428,79 @@ 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: `x509.Certificate` wrapped in `.ComparableX509` + :type cert: `cryptography.x509.Certificate` :rtype: unicode """ - if isinstance(cert._wrapped_new, x509.CertificateSigningRequest): + 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(cert._wrapped_new.public_bytes(Encoding.DER)) + 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: `x509.Certificate` 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(x509.load_der_x509_certificate(decode_b64jose(b64der))) except Exception as error: raise errors.DeserializationError(error) -def encode_csr(csr: util.ComparableX509) -> str: +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: Union[util.ComparableX509, x509.CertificateSigningRequest]) -> str: """Encode CSR as JOSE Base-64 DER. - :type csr: `x509.CertificateSigningRequest` wrapped in `.ComparableX509` + :type csr: `cryptography.x509.CertificateSigningRequest` :rtype: unicode """ - if isinstance(csr._wrapped_new, x509.Certificate): + 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(csr._wrapped_new.public_bytes(Encoding.DER)) + return encode_b64jose(csr.public_bytes(Encoding.DER)) def decode_csr(b64der: str) -> util.ComparableX509: @@ -479,6 +516,19 @@ def decode_csr(b64der: str) -> util.ComparableX509: 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) + + GenericTypedJSONObjectWithFields = TypeVar( "GenericTypedJSONObjectWithFields", bound="TypedJSONObjectWithFields" ) diff --git a/src/josepy/jws.py b/src/josepy/jws.py index 617b70f8..eb0c87ae 100644 --- a/src/josepy/jws.py +++ b/src/josepy/jws.py @@ -12,6 +12,7 @@ Optional, Tuple, Type, + Union, cast, ) @@ -81,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 @@ -139,15 +143,20 @@ def crit(unused_value: Any) -> Any: @x5c.encoder # type: ignore def x5c(value): - return [base64.b64encode(cert._wrapped_new.public_bytes(Encoding.DER)) for cert in value] + # DEPRECATE `cert.wrapped_new` + return [ + ( + 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(x509.load_der_x509_certificate(base64.b64decode(cert))) - for cert in value - ) + 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 c2b9d608..225ba097 100644 --- a/src/josepy/util.py +++ b/src/josepy/util.py @@ -1,7 +1,6 @@ """JOSE utilities.""" import abc -import datetime import sys import warnings from collections.abc import Hashable, Mapping @@ -12,7 +11,6 @@ Callable, Iterator, List, - Optional, Tuple, TypeVar, Union, @@ -22,18 +20,7 @@ from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.hazmat.primitives.serialization import Encoding - -# support this as an optional import -# use an alternate name, as the dev environment will always need typing -crypto: Optional[ModuleType] = None -try: - from OpenSSL import crypto -except (ImportError, ModuleNotFoundError): - pass - -if TYPE_CHECKING: - # use the full path for typing - import OpenSSL.crypto +from OpenSSL import crypto def warn_deprecated(message: str) -> None: @@ -41,142 +28,107 @@ def warn_deprecated(message: str) -> None: warnings.warn(message, DeprecationWarning, stacklevel=2) -# compatability against pyopenssl; -# ensured by unittest: ComparableX509LegacyTest.test_filetype_compat -FILETYPE_ASN1 = 2 -FILETYPE_PEM = 1 -FILETYPE_TEXT = 65535 - - # Deprecated. Please use built-in decorators @classmethod and abc.abstractmethod together instead. def abstractclassmethod(func: Callable) -> classmethod: return classmethod(abc.abstractmethod(func)) class ComparableX509: - """Originally a wrapper for OpenSSL.crypto.X509** objects that supports __eq__. - - :ivar wrapped: Wrapped certificate or certificate request. - :type wrapped: `Cryptography.x509.Certificate` or - `Cryptography.x509.CertificateSigningRequest` - :ivar wrapped_legacy: Legacy Wrapped certificate or certificate request. - This attribute will be removed when `OpenSSL.crypto` support is fully - dropped. This attribute is only meant to aid in migration to the - new Cryptography backend. + """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 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` """ - _wrapped_new: Union[x509.Certificate, x509.CertificateSigningRequest] - _wrapped_legacy: Union["OpenSSL.crypto.X509", "OpenSSL.crypto.X509Req", None] = None + _wrapped_new: Union[x509.Certificate, x509.CertificateSigningRequest, None] = None + _wrapped_legacy: Union[crypto.X509, crypto.X509Req, None] = None def __init__( self, wrapped: Union[ - "OpenSSL.crypto.X509", - "OpenSSL.crypto.X509Req", + crypto.X509, + crypto.X509Req, x509.Certificate, x509.CertificateSigningRequest, ], ) -> None: - # conditional runtime inputs - if crypto: - # if pyOpenSSL is installed, we expect 4 potential classes: - assert isinstance( - wrapped, - (x509.Certificate, x509.CertificateSigningRequest, crypto.X509, crypto.X509Req), - ) + """__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: - # if pyOpenSSL is not installed, there are 2 potential classes: - assert isinstance(wrapped, (x509.Certificate, x509.CertificateSigningRequest)) - # conditional compatibility layer - if crypto: - if isinstance(wrapped, (crypto.X509, crypto.X509Req)): - warn_deprecated( - "`OpenSSL.crypto` objects are deprecated and support will be " - "removed in a future verison of josepy. The `wrapped` attribute " - "now contains a `Cryptography.x509` object." - ) - # stash for legacy operations - self._wrapped_legacy = wrapped - # convert to Cryptography.x509 - der: bytes - if isinstance(wrapped, crypto.X509): - der = crypto.dump_certificate(crypto.FILETYPE_ASN1, wrapped) - wrapped = x509.load_der_x509_certificate(der) - - elif isinstance(wrapped, crypto.X509Req): - der = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, wrapped) - wrapped = x509.load_der_x509_csr(der) - else: - # this is a x509 AND we have pyOpenSSL installed - # as an interim bridge, create a legacy version - _wrapped_legacy: Union["OpenSSL.crypto.X509", "OpenSSL.crypto.X509Req"] - if isinstance(wrapped, x509.Certificate): - _wrapped_legacy = crypto.load_certificate( - crypto.FILETYPE_ASN1, wrapped.public_bytes(Encoding.DER) - ) - elif isinstance(wrapped, x509.CertificateSigningRequest): - _wrapped_legacy = crypto.load_certificate_request( - crypto.FILETYPE_ASN1, wrapped.public_bytes(Encoding.DER) - ) - self._wrapped_legacy = _wrapped_legacy - self._wrapped_new = wrapped + # this is a x509 AND we have pyOpenSSL + self._wrapped_new = wrapped @property - def wrapped( + def wrapped_new( self, ) -> Union[ x509.Certificate, x509.CertificateSigningRequest, - "OpenSSL.crypto.X509", - "OpenSSL.crypto.X509Req", ]: - # prefer returning the legacy if it is defined, otherwise the new - return self._wrapped_legacy or self._wrapped_new - - @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_legacy(self) -> Union["OpenSSL.crypto.X509", "OpenSSL.crypto.X509Req"]: - # migration layer to the new Cryptography backend - # this function is deprecated on release, and will be removed - if crypto is None: - raise ValueError("`OpenSSL.crypto` must be installed for compatability") + 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): + if isinstance(self.wrapped_new, x509.Certificate): self._wrapped_legacy = crypto.load_certificate( - crypto.FILETYPE_ASN1, self._wrapped_new.public_bytes(Encoding.DER) + crypto.FILETYPE_ASN1, self.wrapped_new.public_bytes(Encoding.DER) ) - elif isinstance(self._wrapped_new, x509.CertificateSigningRequest): + elif isinstance(self.wrapped_new, x509.CertificateSigningRequest): self._wrapped_legacy = crypto.load_certificate_request( - crypto.FILETYPE_ASN1, self._wrapped_new.public_bytes(Encoding.DER) + crypto.FILETYPE_ASN1, self.wrapped_new.public_bytes(Encoding.DER) ) - else: - raise ValueError("no compatible legacy object") + if TYPE_CHECKING: + assert isinstance(self._wrapped_legacy, (crypto.X509, crypto.X509Req)) return self._wrapped_legacy def __getattr__(self, name: str) -> Any: - if self._wrapped_legacy: - try: - return getattr(self._wrapped_legacy, name) - except Exception: - pass - # fallback to compat and _wrapped_new - if name == "has_expired": - # a unittest addresses this attribute - # x509.CertificateSigningRequest does not have this attribute - # ideally this function would be deprecated and users should - # address the `wrapped` item directly. - if isinstance(self._wrapped_new, x509.Certificate): - return ( - lambda: datetime.datetime.now(datetime.timezone.utc) - > self._wrapped_new.not_valid_after_utc - ) - return getattr(self._wrapped_new, name) + return getattr(self.wrapped, name) - def _dump(self, filetype: int = FILETYPE_ASN1) -> bytes: + def _dump(self, filetype: int = crypto.FILETYPE_ASN1) -> bytes: """Dumps the object into a buffer with the specified encoding. :param int filetype: The desired encoding. Should be one of @@ -188,11 +140,11 @@ def _dump(self, filetype: int = FILETYPE_ASN1) -> bytes: """ # deprecated `FILETYPE_TEXT` - if filetype not in (FILETYPE_ASN1, FILETYPE_PEM): + if filetype not in (crypto.FILETYPE_ASN1, crypto.FILETYPE_PEM): raise ValueError("filetype `%s` is deprecated") - if filetype == FILETYPE_ASN1: - return self._wrapped_new.public_bytes(Encoding.DER) - return self._wrapped_new.public_bytes(Encoding.PEM) + 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__): @@ -203,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_new) + 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..9f52ca2f 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 = decode_cert(self.b64_cert) + assert isinstance(cert, util.ComparableX509) + assert cert == 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 = decode_csr(self.b64_csr) + assert isinstance(csr, util.ComparableX509) + assert csr == 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 33fe05b1..4e106027 100644 --- a/tests/jws_test.py +++ b/tests/jws_test.py @@ -3,6 +3,7 @@ import base64 import sys import unittest +import warnings from unittest import mock import pytest @@ -12,7 +13,8 @@ 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")) @@ -71,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_new, x509.Certificate) - cert_asn1 = CERT._wrapped_new.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) + 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 8e8bea5d..2c742e37 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,23 +4,17 @@ import contextlib import os import sys -from types import ModuleType -from typing import Any, Optional +import warnings +from typing import Any from cryptography import x509 from cryptography.hazmat.primitives import serialization +from OpenSSL import crypto import josepy.util from josepy import ComparableRSAKey, ComparableX509 from josepy.util import ComparableECKey -# conditional import -crypto: Optional[ModuleType] = None -try: - from OpenSSL import crypto -except (ImportError, ModuleNotFoundError): - pass - # This approach is based on the recommendation at # https://github.com/python/mypy/issues/1153#issuecomment-1207333806. if sys.version_info >= (3, 9): @@ -68,7 +62,10 @@ def load_cert(*names: str) -> x509.Certificate: 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) -> x509.CertificateSigningRequest: @@ -79,7 +76,10 @@ def load_csr(*names: str) -> x509.CertificateSigningRequest: 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: @@ -98,24 +98,27 @@ def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: return ComparableECKey(loader(load_vector(*names), password=None)) -if crypto: +# 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_certificate(loader, load_vector(*names)) + - # legacy testing support +def load_comparable_cert__pyopenssl(*names: str) -> josepy.util.ComparableX509: + """Load ComparableX509 cert.""" + return ComparableX509(load_cert__pyopenssl(*names)) - def load_cert__pyopenssl(*names: str) -> crypto.X509: - """Load certificate.""" - loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - 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_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)) +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 84140895..09456538 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,28 +1,14 @@ """Tests for josepy.util.""" import functools -import os import sys import unittest import warnings -from types import ModuleType -from typing import Optional import pytest import test_util from cryptography import x509 - -import josepy.util - -# conditional import -crypto: Optional[ModuleType] = None -try: - from OpenSSL import crypto -except (ImportError, ModuleNotFoundError): - pass - - -JOSEPY_EXPECT_OPENSSL = bool(int(os.getenv("JOSEPY_EXPECT_OPENSSL", "0"))) +from OpenSSL import crypto class ComparableX509Test(unittest.TestCase): @@ -68,35 +54,21 @@ def test_hash(self) -> None: def test_repr(self) -> None: for cert in (self.req1, self.cert1): - assert repr(cert) == "".format(cert._wrapped_new) + assert repr(cert) == "".format(cert.wrapped_new) def test_legacy_cert(self) -> None: - if crypto: - # check explicit - assert isinstance(self.cert1.wrapped_legacy, crypto.X509) - assert isinstance(self.cert1.wrapped_new, x509.Certificate) - # check default - assert isinstance(self.cert1.wrapped, crypto.X509) - else: - # check explicit - assert self.cert1.wrapped_legacy is None - assert isinstance(self.cert1.wrapped_new, x509.Certificate) - # check default - assert isinstance(self.cert1.wrapped, x509.Certificate) + # 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: - if crypto: - # check explicit - assert isinstance(self.req1.wrapped_legacy, crypto.X509Req) - assert isinstance(self.req1.wrapped_new, x509.CertificateSigningRequest) - # check default - assert isinstance(self.req1.wrapped, crypto.X509Req) - else: - # check explicit - assert self.cert1.wrapped_legacy is None - assert isinstance(self.req1.wrapped_new, x509.CertificateSigningRequest) - # check default - assert isinstance(self.req1.wrapped, x509.CertificateSigningRequest) + # 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): @@ -106,39 +78,45 @@ def _check_loading_warns(self, warnlist: list) -> bool: for w in warnlist: if isinstance(w.message, DeprecationWarning): if isinstance(w.message.args[0], str): - if w.message.args[0].startswith("`OpenSSL.crypto` objects are deprecated"): + if w.message.args[0].startswith( + "`josepy.util.ComparableX509` objects are deprecated" + ): _found = True break return _found """Legacy tests for josepy.util.ComparableX509.""" - @unittest.skipUnless(JOSEPY_EXPECT_OPENSSL, "only run in legacy mode") 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 - # check explicit - assert isinstance(cert1.wrapped_legacy, crypto.X509) - # check default 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 - # check explicit - assert isinstance(csr1.wrapped_legacy, crypto.X509Req) - # check default assert isinstance(csr1.wrapped, crypto.X509Req) + assert isinstance(csr1.wrapped_new, x509.CertificateSigningRequest) - @unittest.skipUnless(JOSEPY_EXPECT_OPENSSL, "only run in legacy mode") - def test_filetype_compat(self) -> None: - assert josepy.util.FILETYPE_ASN1 == crypto.FILETYPE_ASN1 - assert josepy.util.FILETYPE_PEM == crypto.FILETYPE_PEM - assert josepy.util.FILETYPE_TEXT == crypto.FILETYPE_TEXT + 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): diff --git a/tox.ini b/tox.ini index 8fc63d7a..c22a3fa6 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ commands = false [testenv:py] -commands = poetry run pytest -v --cov-report xml --cov-report=term-missing --cov=josepy {env:JOSEPY_EXPECT_OPENSSL} {posargs} +commands = poetry run pytest -v --cov-report xml --cov-report=term-missing --cov=josepy {posargs} [testenv:py3{,8,9,10,11,12,13}] commands = {[testenv:py]commands} @@ -36,17 +36,3 @@ commands = poetry run pre-commit run --all extras = legacy commands_pre = poetry install -v -E legacy commands = poetry run mypy src tests - -# runs the py test configured for `github-${{build-type}}` -[testenv:github-legacy] -extras = legacy -setenv = - JOSEPY_EXPECT_OPENSSL = 1 -commands_pre = poetry install -v -E legacy -commands = {[testenv:py]commands} - -# runs the py test configured for `github-${{build-type}}` -[testenv:github-noopenssl] -setenv = - JOSEPY_EXPECT_OPENSSL = 0 -commands = {[testenv:py]commands}