diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 218c6746..6e3fc410 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Changelog ========= +2.0.0 (master) +* Dropped support for pyOpenSSL in favor of Cryptography + 1.15.0 (master) --------------- * Added support for Python 3.13. diff --git a/poetry.lock b/poetry.lock index 29c29b2a..10646941 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" @@ -989,24 +989,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pyopenssl" -version = "24.3.0" -description = "Python wrapper module around the OpenSSL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"}, - {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"}, -] - -[package.dependencies] -cryptography = ">=41.0.5,<45" - -[package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] -test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] - [[package]] name = "pyproject-api" version = "1.8.0" @@ -1484,35 +1466,6 @@ urllib3 = ">=1.26.0" [package.extras] keyring = ["keyring (>=15.1)"] -[[package]] -name = "types-cffi" -version = "1.16.0.20240331" -description = "Typing stubs for cffi" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, - {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, -] - -[package.dependencies] -types-setuptools = "*" - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -description = "Typing stubs for pyOpenSSL" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, - {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, -] - -[package.dependencies] -cryptography = ">=35.0.0" -types-cffi = "*" - [[package]] name = "types-pyrfc3339" version = "2.0.1.20241107" @@ -1622,4 +1575,4 @@ docs = ["sphinx", "sphinx-rtd-theme"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "7fa22898e575497d4261ebca5256b36bd6576e1cca2d01bd7e5d050a0d43fd3e" +content-hash = "4fedbf154eae8b6303acfc9ec4b2bcca791bc8023093532692a12bb1e43e3006" diff --git a/pyproject.toml b/pyproject.toml index 61bb818d..33f0ef8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,8 @@ 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" -# Connection.set_tlsext_host_name (>=0.13) -pyopenssl = ">=0.13" +# add not_valid_after_utc() ">=42.0.0" +cryptography = ">=42.0.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} @@ -57,7 +56,6 @@ coverage = {version = ">=4.0", extras = ["toml"]} # https://github.com/python/importlib_resources/tree/7f4fbb5ee026d7610636d5ece18b09c64aa0c893#compatibility. importlib_resources = {version = ">=1.3", python = "<3.9"} mypy = "*" -types-pyOpenSSL = "*" types-pyRFC3339 = "*" types-requests = "*" types-setuptools = "*" @@ -97,7 +95,6 @@ disallow_untyped_defs = true [tool.pytest.ini_options] filterwarnings = [ "error", - "ignore:CSR support in pyOpenSSL is deprecated:DeprecationWarning", # We ignore our own warning about dropping Python 3.8 support. "ignore:Python 3.8 support will be dropped:DeprecationWarning", ] diff --git a/src/josepy/json_util.py b/src/josepy/json_util.py index ed4e4811..6aad2505 100644 --- a/src/josepy/json_util.py +++ b/src/josepy/json_util.py @@ -22,7 +22,8 @@ TypeVar, ) -from OpenSSL import crypto +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding from josepy import b64, errors, interfaces, util @@ -429,56 +430,52 @@ def decode_hex16(value: str, size: Optional[int] = None, minimum: bool = False) def encode_cert(cert: util.ComparableX509) -> str: """Encode certificate as JOSE Base-64 DER. - :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` + :type cert: `x509.Certificate` wrapped in `.ComparableX509` :rtype: unicode """ - if isinstance(cert.wrapped, crypto.X509Req): + if isinstance(cert.wrapped, 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.wrapped.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: `x509.Certificate` wrapped in `.ComparableX509` """ 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 encode_csr(csr: util.ComparableX509) -> str: """Encode CSR as JOSE Base-64 DER. - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + :type csr: `x509.CertificateSigningRequest` wrapped in `.ComparableX509` :rtype: unicode """ - if isinstance(csr.wrapped, crypto.X509): + if isinstance(csr.wrapped, 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.wrapped.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) diff --git a/src/josepy/jws.py b/src/josepy/jws.py index 6bfe9ce9..02df3722 100644 --- a/src/josepy/jws.py +++ b/src/josepy/jws.py @@ -15,7 +15,8 @@ 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 @@ -138,21 +139,17 @@ def crit(unused_value: Any) -> Any: @x5c.encoder # type: ignore def x5c(value): - return [ - base64.b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) - for cert in value - ] + return [base64.b64encode(cert.wrapped.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)) - ) + util.ComparableX509(x509.load_der_x509_certificate(base64.b64decode(cert))) for cert in value ) - except crypto.Error as error: + + except Exception as error: raise errors.DeserializationError(error) diff --git a/src/josepy/util.py b/src/josepy/util.py index 3af88e75..cc924393 100644 --- a/src/josepy/util.py +++ b/src/josepy/util.py @@ -5,10 +5,21 @@ 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 ( + Any, + Callable, + Iterator, + List, + Literal, + Tuple, + TypeVar, + Union, + cast, +) + +from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import ec, rsa -from OpenSSL import crypto +from cryptography.hazmat.primitives.serialization import Encoding # Deprecated. Please use built-in decorators @classmethod and abc.abstractmethod together instead. @@ -17,37 +28,41 @@ def abstractclassmethod(func: Callable) -> classmethod: class ComparableX509: - """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__. + """Wraps cryptography.x509 objects objects to support __eq__ operations. :ivar wrapped: Wrapped certificate or certificate request. - :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. - + :type wrapped: `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) + # + wrapped: Union[x509.Certificate, x509.CertificateSigningRequest] + + def __init__( + self, + wrapped: Union[x509.Certificate, x509.CertificateSigningRequest], + ) -> None: + # conditional runtime inputs + assert isinstance(wrapped, (x509.Certificate, x509.CertificateSigningRequest)) self.wrapped = wrapped def __getattr__(self, name: str) -> Any: return getattr(self.wrapped, name) - def _dump(self, filetype: int = crypto.FILETYPE_ASN1) -> bytes: + def _dump(self, filetype: Literal[Encoding.DER, Encoding.PEM] = Encoding.DER) -> bytes: """Dumps the object into a buffer with the specified encoding. :param int filetype: The desired encoding. Should be one of - `OpenSSL.crypto.FILETYPE_ASN1`, - `OpenSSL.crypto.FILETYPE_PEM`, or - `OpenSSL.crypto.FILETYPE_TEXT`. + `Encoding.DER`, + `Encoding.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) + if filetype == Encoding.DER: + return self.wrapped.public_bytes(Encoding.DER) + return self.wrapped.public_bytes(Encoding.PEM) def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): diff --git a/tests/jws_test.py b/tests/jws_test.py index c6608cd2..d8a4e49d 100644 --- a/tests/jws_test.py +++ b/tests/jws_test.py @@ -5,9 +5,10 @@ import unittest 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 @@ -72,8 +73,8 @@ def test_x5c_decoding(self) -> None: 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) + assert isinstance(CERT.wrapped, x509.Certificate) + cert_asn1 = CERT.wrapped.public_bytes(Encoding.DER) cert_b64 = base64.b64encode(cert_asn1) assert jobj == {"x5c": [cert_b64, cert_b64]} assert header == Header.from_json(jobj) diff --git a/tests/test_util.py b/tests/test_util.py index 1bd60974..609bf1ee 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,9 +6,9 @@ import sys from typing import Any +from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization -from OpenSSL import crypto import josepy.util from josepy import ComparableRSAKey, ComparableX509 @@ -51,10 +51,12 @@ 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: @@ -62,10 +64,10 @@ def load_comparable_cert(*names: str) -> josepy.util.ComparableX509: 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: @@ -87,9 +89,3 @@ def load_ec_private_key(*names: str) -> josepy.util.ComparableECKey: names[-1], serialization.load_pem_private_key, serialization.load_der_private_key ) return ComparableECKey(loader(load_vector(*names), password=None, backend=default_backend())) - - -def load_pyopenssl_private_key(*names: str) -> crypto.PKey: - """Load pyOpenSSL private key.""" - loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_privatekey(loader, load_vector(*names)) diff --git a/tests/util_test.py b/tests/util_test.py index cee8b0a6..d46a21f8 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -22,7 +22,7 @@ def setUp(self) -> None: self.cert_other = test_util.load_comparable_cert("cert-san.pem") def test_getattr_proxy(self) -> None: - assert self.cert1.has_expired() is True + assert self.cert1.not_valid_after_utc is not None def test_eq(self) -> None: assert self.req1 == self.req2