From 219e234c19b42ab93afac1e3ab519da2302e4d24 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Wed, 15 Jan 2025 15:41:07 -0800 Subject: [PATCH 1/3] Deprecate and add new encoding and decoding functions --- src/josepy/json_util.py | 89 ++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 15 deletions(-) diff --git a/src/josepy/json_util.py b/src/josepy/json_util.py index ed4e4811..7eae6015 100644 --- a/src/josepy/json_util.py +++ b/src/josepy/json_util.py @@ -10,6 +10,7 @@ import abc import binascii import logging +import warnings from typing import ( Any, Callable, @@ -20,8 +21,11 @@ Optional, Type, TypeVar, + Union, ) +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding from OpenSSL import crypto from josepy import b64, errors, interfaces, util @@ -426,26 +430,41 @@ 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` + or `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :rtype: unicode - """ - if isinstance(cert.wrapped, crypto.X509Req): - raise ValueError("Error input is actually a certificate request.") - - return encode_b64jose(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) + if isinstance(cert, util.ComparableX509): + # DEPRECATED; remove this in major release + warnings.warn( + "`josepy.json_util.encode_cert` has deprecated support for accepting " + "util.ComparableX509 objects, and support will be dropped in the next major release. " + "Please use `cryptography.x509.Certificate` objects instead.", + DeprecationWarning, + ) + if isinstance(cert.wrapped, crypto.X509Req): + raise ValueError("Error input is actually a certificate request.") + return encode_b64jose(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert.wrapped)) + assert isinstance(cert, x509.Certificate) + return encode_b64jose(cert.public_bytes(Encoding.DER)) def decode_cert(b64der: str) -> util.ComparableX509: - """Decode JOSE Base-64 DER-encoded certificate. + """Decode JOSE Base-64 DER-encoded certificate. Deprecated as of 1.15.0 and will be + removed in 2.0.0. :param unicode b64der: :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ + warnings.warn( + "`josepy.json_util.decode_cert` is deprecated, and will be removed in the next major " + "release. Please use `josepy.json_util.decode_cert_cryptography instead.", + DeprecationWarning, + ) try: return util.ComparableX509( crypto.load_certificate(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) @@ -454,26 +473,53 @@ def decode_cert(b64der: str) -> util.ComparableX509: 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: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + :type csr: `cryptography.x509.CertificateSigningRequest` + or `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` :rtype: unicode """ - if isinstance(csr.wrapped, crypto.X509): - raise ValueError("Error input is actually a certificate.") - - return encode_b64jose(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, csr.wrapped)) + if isinstance(csr, util.ComparableX509): + # DEPRECATED; remove this in major release + warnings.warn( + "`josepy.json_util.encode_csr` has deprecated support for accepting " + "util.ComparableX509 objects, and support will be dropped in the next major release. " + "Please use `cryptography.x509.CertificateSigningRequest` objects instead.", + DeprecationWarning, + ) + if isinstance(csr.wrapped, crypto.X509): + raise ValueError("Error input is actually a certificate.") + return encode_b64jose(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, csr.wrapped)) + assert isinstance(csr, x509.CertificateSigningRequest) + return encode_b64jose(csr.public_bytes(Encoding.DER)) def decode_csr(b64der: str) -> util.ComparableX509: - """Decode JOSE Base-64 DER-encoded CSR. + """Decode JOSE Base-64 DER-encoded CSR. Deprecated as of 1.15.0 and will be removed in 2.0.0. :param unicode b64der: :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ + warnings.warn( + "`josepy.json_util.decode_csr` is deprecated, and will be removed in the next major " + "release. Please use `josepy.json_util.decode_csr_cryptography instead.", + DeprecationWarning, + ) try: return util.ComparableX509( crypto.load_certificate_request(crypto.FILETYPE_ASN1, decode_b64jose(b64der)) @@ -482,6 +528,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" ) From 1c2de2e90850bc78c92008aa017fc0b6f28c9708 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Wed, 15 Jan 2025 15:51:44 -0800 Subject: [PATCH 2/3] Add unit tests --- tests/json_util_test.py | 57 +++++++++++++++++++++++++++++++++-------- tests/test_util.py | 15 +++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/tests/json_util_test.py b/tests/json_util_test.py index 9e53876c..55aa17d6 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") +CERT_CRYPTOGRAPHY = test_util.load_cert_cryptography("cert.pem") +CSR_CRYPTOGRAPHY = test_util.load_csr_cryptography("csr.pem") class FieldTest(unittest.TestCase): @@ -321,30 +325,61 @@ def test_decode_hex16_odd_length(self) -> None: def test_encode_cert(self) -> None: from josepy.json_util import encode_cert - assert self.b64_cert == encode_cert(CERT) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + assert self.b64_cert == encode_cert(CERT) + + assert self.b64_cert == encode_cert(CERT_CRYPTOGRAPHY) def test_decode_cert(self) -> None: - from josepy.json_util import decode_cert + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + from josepy.json_util import decode_cert + + cert = decode_cert(self.b64_cert) + assert isinstance(cert, util.ComparableX509) + assert cert == CERT + with pytest.raises(errors.DeserializationError): + decode_cert("") - cert = decode_cert(self.b64_cert) - assert isinstance(cert, util.ComparableX509) - assert cert == CERT + def test_decode_cert_cryptography(self) -> None: + from josepy.json_util import decode_cert_cryptography + + cert = decode_cert_cryptography(self.b64_cert) + assert isinstance(cert, x509.Certificate) + assert cert == CERT_CRYPTOGRAPHY with pytest.raises(errors.DeserializationError): - decode_cert("") + decode_cert_cryptography("") def test_encode_csr(self) -> None: from josepy.json_util import encode_csr - assert self.b64_csr == encode_csr(CSR) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + assert self.b64_csr == encode_csr(CSR) + + assert self.b64_csr == encode_csr(CSR_CRYPTOGRAPHY) def test_decode_csr(self) -> None: from josepy.json_util import decode_csr - csr = decode_csr(self.b64_csr) - assert isinstance(csr, util.ComparableX509) - assert csr == CSR + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + + csr = decode_csr(self.b64_csr) + assert isinstance(csr, util.ComparableX509) + assert csr == CSR + with pytest.raises(errors.DeserializationError): + decode_csr("") + + def test_decode_csr_cryptography(self) -> None: + from josepy.json_util import decode_csr_cryptography + + csr = decode_csr_cryptography(self.b64_csr) + assert isinstance(csr, x509.CertificateSigningRequest) + assert csr == CSR_CRYPTOGRAPHY with pytest.raises(errors.DeserializationError): - decode_csr("") + decode_csr_cryptography("") class TypedJSONObjectWithFieldsTest(unittest.TestCase): diff --git a/tests/test_util.py b/tests/test_util.py index 1bd60974..ae2c5762 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,6 +6,7 @@ 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 @@ -57,6 +58,14 @@ def load_cert(*names: str) -> crypto.X509: return crypto.load_certificate(loader, load_vector(*names)) +def load_cert_cryptography(*names: str) -> x509.Certificate: + """Load certificate using cryptography API.""" + 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)) @@ -68,6 +77,12 @@ def load_csr(*names: str) -> crypto.X509Req: return crypto.load_certificate_request(loader, load_vector(*names)) +def load_csr_cryptography(*names: str) -> x509.CertificateSigningRequest: + """Load certificate request.""" + 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)) From d990223991a868c17834bbd9612921ac66f16705 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Wed, 15 Jan 2025 16:14:38 -0800 Subject: [PATCH 3/3] Add new methods to __init__ --- src/josepy/__init__.py | 2 ++ 1 file changed, 2 insertions(+) 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,