Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
51 changes: 2 additions & 49 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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 = "*"
Expand Down Expand Up @@ -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",
]
Expand Down
31 changes: 14 additions & 17 deletions src/josepy/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)


Expand Down
15 changes: 6 additions & 9 deletions src/josepy/jws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
49 changes: 32 additions & 17 deletions src/josepy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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__):
Expand Down
7 changes: 4 additions & 3 deletions tests/jws_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 9 additions & 13 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,21 +51,23 @@ 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))


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:
Expand All @@ -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))
2 changes: 1 addition & 1 deletion tests/util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading