Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring to drop deprecated usages in pyOpenSSL & drop Python 3.7 support #182

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Setup Python
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
Changelog
=========

1.15.0 (master)
2.0.0 (master)
---------------

* Remove support for Python 3.7.
* Class `josepy.ComparableX509` is removed, please use `cryptography` APIs instead
(`cryptography.x509.Certificate` and `cryptography.x509.CertificateSigningRequest`).
* Methods `josepy.decode_cert` and `josepy.encode_cert` now return/accept an instance
of `cryptography.x509.Certificate`.
* Methods `josepy.decode_csr` and `josepy.encode_csr` now return/accept an instance
of `cryptography.x509.CertificateSigningRequest`.

1.14.0 (2023-11-01)
-------------------

Expand Down
1,102 changes: 533 additions & 569 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -38,13 +37,11 @@ include = [
[tool.poetry.dependencies]
# This should be kept in sync with the value of target-version in our
# configuration for black below.
python = "^3.7"
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"
# >=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}
Expand All @@ -57,7 +54,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 = "*"
Expand All @@ -82,7 +78,7 @@ jws = "josepy.jws:CLI.run"
line-length = 100
# This should be kept in sync with the version of Python specified in poetry's
# dependencies above.
target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312']
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']

# Mypy tooling configuration

Expand All @@ -95,8 +91,6 @@ disallow_untyped_defs = true
# Pytest tooling configuration

[tool.pytest.ini_options]
# We also ignore our own deprecation warning about dropping Python 3.7 support.
filterwarnings = ["error", "ignore:Python 3.7 support will be dropped:DeprecationWarning"]
norecursedirs = "*.egg .eggs dist build docs .tox"

# Isort tooling configuration
Expand Down
8 changes: 0 additions & 8 deletions src/josepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,5 @@
ComparableECKey,
ComparableKey,
ComparableRSAKey,
ComparableX509,
ImmutableMap,
)

if sys.version_info[:2] == (3, 7):
warnings.warn(
"Python 3.7 support will be dropped in the next scheduled release of "
"josepy. Please upgrade your Python version.",
DeprecationWarning,
)
43 changes: 20 additions & 23 deletions src/josepy/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
TypeVar,
)

from OpenSSL import crypto
from cryptography import x509
from cryptography.hazmat.primitives import serialization

from josepy import b64, errors, interfaces, util

Expand Down Expand Up @@ -425,60 +426,56 @@ 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: 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, 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(serialization.Encoding.DER))


def decode_cert(b64der: str) -> util.ComparableX509:
def decode_cert(b64der: str) -> x509.Certificate:
"""Decode JOSE Base-64 DER-encoded certificate.

:param unicode b64der:
:rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
:rtype: `x509.Certificate`

"""
try:
return util.ComparableX509(
crypto.load_certificate(crypto.FILETYPE_ASN1, decode_b64jose(b64der))
)
except crypto.Error as error:
raise errors.DeserializationError(error)
return x509.load_der_x509_certificate(decode_b64jose(b64der))
except Exception as e:
raise errors.DeserializationError(e)


def encode_csr(csr: util.ComparableX509) -> str:
def encode_csr(csr: 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, 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(serialization.Encoding.DER))


def decode_csr(b64der: str) -> util.ComparableX509:
def decode_csr(b64der: str) -> x509.CertificateSigningRequest:
"""Decode JOSE Base-64 DER-encoded CSR.

:param unicode b64der:
:rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
:rtype: `cryptography.x509.CertificateSigningRequest`

"""
try:
return util.ComparableX509(
crypto.load_certificate_request(crypto.FILETYPE_ASN1, decode_b64jose(b64der))
)
except crypto.Error as error:
raise errors.DeserializationError(error)
return x509.load_der_x509_csr(decode_b64jose(b64der))
except Exception as e:
raise errors.DeserializationError(e)


GenericTypedJSONObjectWithFields = TypeVar(
Expand Down
15 changes: 7 additions & 8 deletions src/josepy/jws.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
cast,
)

from OpenSSL import crypto
from cryptography import x509
from cryptography.hazmat.primitives import serialization

import josepy
from josepy import b64, errors, json_util, jwa
Expand Down Expand Up @@ -79,7 +80,7 @@ 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: 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
Expand Down Expand Up @@ -138,21 +139,19 @@ def crit(unused_value: Any) -> Any:
@x5c.encoder # type: ignore
def x5c(value):
return [
base64.b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped))
base64.b64encode(cert.public_bytes(serialization.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))
)
x509.load_der_x509_certificate(base64.b64decode(cert))
for cert in value
)
except crypto.Error as error:
raise errors.DeserializationError(error)
except Exception as e:
raise errors.DeserializationError(e)


class Signature(json_util.JSONObjectWithFields):
Expand Down
110 changes: 9 additions & 101 deletions src/josepy/util.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,9 @@
"""JOSE utilities."""
import abc
import sys
import warnings
from collections.abc import Hashable, Mapping
from types import ModuleType
from typing import Any, Callable, Iterator, List, Tuple, TypeVar, Union, cast
from __future__ import annotations
from collections.abc import Hashable, Iterator, Mapping, Callable
from typing import Any, TypeVar

from cryptography.hazmat.primitives.asymmetric import ec, rsa
from OpenSSL import crypto


# 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__.

:ivar wrapped: Wrapped certificate or certificate request.
:type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.

"""

def __init__(self, wrapped: Union[crypto.X509, crypto.X509Req]) -> None:
assert isinstance(wrapped, crypto.X509) or isinstance(wrapped, crypto.X509Req)
self.wrapped = wrapped

def __getattr__(self, name: str) -> Any:
return getattr(self.wrapped, name)

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
`OpenSSL.crypto.FILETYPE_ASN1`,
`OpenSSL.crypto.FILETYPE_PEM`, or
`OpenSSL.crypto.FILETYPE_TEXT`.

: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)

def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return self._dump() == other._dump()

def __hash__(self) -> int:
return hash((self.__class__, self._dump()))

def __repr__(self) -> str:
return "<{0}({1!r})>".format(self.__class__.__name__, self.wrapped)


class ComparableKey:
Expand All @@ -71,12 +17,11 @@ class ComparableKey:

def __init__(
self,
wrapped: Union[
rsa.RSAPrivateKeyWithSerialization,
rsa.RSAPublicKeyWithSerialization,
ec.EllipticCurvePrivateKeyWithSerialization,
wrapped:
rsa.RSAPrivateKeyWithSerialization |
rsa.RSAPublicKeyWithSerialization |
ec.EllipticCurvePrivateKeyWithSerialization |
ec.EllipticCurvePublicKeyWithSerialization,
],
):
self._wrapped = wrapped

Expand Down Expand Up @@ -163,7 +108,7 @@ def __hash__(self) -> int:
class ImmutableMap(Mapping, Hashable):
"""Immutable key to value mapping with attribute access."""

__slots__: Tuple[str, ...] = ()
__slots__: tuple[str, ...] = ()
"""Must be overridden in subclasses."""

def __init__(self, **kwargs: Any) -> None:
Expand Down Expand Up @@ -234,7 +179,7 @@ def __iter__(self) -> Iterator[str]:
def __len__(self) -> int:
return len(self._items)

def _sorted_items(self) -> Tuple[Tuple[str, Any], ...]:
def _sorted_items(self) -> tuple[tuple[str, Any], ...]:
return tuple((key, self[key]) for key in self._keys)

def __hash__(self) -> int:
Expand All @@ -253,40 +198,3 @@ def __repr__(self) -> str:
return "frozendict({0})".format(
", ".join("{0}={1!r}".format(key, value) for key, value in self._sorted_items())
)


# This class takes a similar approach to the cryptography project to deprecate attributes
# in public modules. See the _ModuleWithDeprecation class here:
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
class _UtilDeprecationModule:
"""
Internal class delegating to a module, and displaying warnings when attributes
related to the deprecated "abstractclassmethod" attributes in the josepy.util module.
"""

def __init__(self, module: ModuleType) -> None:
self.__dict__["_module"] = module

def __getattr__(self, attr: str) -> Any:
if attr == "abstractclassmethod":
warnings.warn(
"The abstractclassmethod attribute in josepy.util is deprecated and will "
"be removed soon. Please use the built-in decorators @classmethod and "
"@abc.abstractmethod together instead.",
DeprecationWarning,
stacklevel=2,
)
return getattr(self._module, attr)

def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover
setattr(self._module, attr, value)

def __delattr__(self, attr: str) -> None: # pragma: no cover
delattr(self._module, attr)

def __dir__(self) -> List[str]: # pragma: no cover
return ["_module"] + dir(self._module)


# Patching ourselves to warn about deprecation and planned removal of some elements in the module.
sys.modules[__name__] = cast(ModuleType, _UtilDeprecationModule(sys.modules[__name__]))
25 changes: 0 additions & 25 deletions tests/init_test.py

This file was deleted.

Loading
Loading