Deprecate functions using acme.crypto_util.Formatto be able to soon remove OpenSSL (#10485)

A few largely unused functions/types have been deprecated in our effort
to remove our pyOpenSSL dependency:
    * Deprecated: `certbot.crypto_util.get_sans_from_cert`
    * Deprecated: `certbot.crypto_util.get_names_from_cert`
    * Deprecated: `certbot.crypto_util.get_names_from_req`
* Deprecated: `certbot.crypto_util.import_csr_file` (and replaced by
`certbot.crypto_util.read_csr_file`)
    * Deprecated: `acme.crypto_util.Format`

`read_csr_file` now always returns a PEM formatted CSR, since that's
what was happening in practice, and therefore lets us stop having to
return a `Format`, so we will be able to stop importing it.

first half of #10433

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
Will Greenberg
2026-02-02 16:25:13 -08:00
committed by GitHub
co-authored by Brad Warren
parent 5cc2f6b4af
commit e7c539d3e9
7 changed files with 183 additions and 52 deletions
@@ -16,9 +16,10 @@ from acme._internal.tests import test_util
class FormatTest(unittest.TestCase): class FormatTest(unittest.TestCase):
def test_to_cryptography_encoding(self): def test_to_cryptography_encoding(self):
from acme.crypto_util import Format with pytest.warns(DeprecationWarning, match='Format is deprecated'):
assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER from acme.crypto_util import Format
assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER
assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM
class MiscTests(unittest.TestCase): class MiscTests(unittest.TestCase):
+35 -2
View File
@@ -3,11 +3,14 @@ import enum
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import ipaddress import ipaddress
import logging import logging
from types import ModuleType
import typing import typing
from typing import Any
from typing import Literal from typing import Literal
from typing import Optional from typing import Optional
from typing import Union from typing import Union
import warnings import warnings
import sys
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
@@ -18,8 +21,38 @@ from OpenSSL import crypto
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# https://github.com/pyca/cryptography/blob/1eab7a67dcc34b568e3c0df64e6222a2ac74b1ee/src/cryptography/utils.py#L66-L113
class _ClientDeprecationModule(ModuleType):
"""
Internal class delegating to a module, and displaying warnings when attributes
related to deprecated attributes in the acme.client module.
"""
def __init__(self, module: ModuleType) -> None:
super().__init__(module.__name__)
self.__dict__['_module'] = module
def __getattr__(self, attr: str) -> Any:
if attr == 'Format':
warnings.warn("acme.crypto_util.Format is deprecated and will be removed in "
"the next major release.", DeprecationWarning)
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__] = _ClientDeprecationModule(sys.modules[__name__])
class Format(enum.IntEnum): class Format(enum.IntEnum):
"""File format to be used when parsing or serializing X.509 structures. """File format to be used when parsing or serializing X.509 structures. Deprecated.
Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants
from pyOpenSSL. from pyOpenSSL.
@@ -36,7 +69,7 @@ class Format(enum.IntEnum):
return Encoding.PEM return Encoding.PEM
# Even *more* annoyingly, due to a mypy bug, we can't use Union[] types in # Annoyingly, due to a mypy bug, we can't use Union[] types in
# isinstance expressions without causing false mypy errors. So we have to # isinstance expressions without causing false mypy errors. So we have to
# recreate the type collection as a tuple here. And no, typing.get_args doesn't # recreate the type collection as a tuple here. And no, typing.get_args doesn't
# work due to another mypy bug. # work due to another mypy bug.
+2 -2
View File
@@ -330,7 +330,7 @@ class HelpfulArgumentParser:
raise errors.Error("--allow-subset-of-names cannot be used with --csr") raise errors.Error("--allow-subset-of-names cannot be used with --csr")
csrfile, contents = config.csr[0:2] csrfile, contents = config.csr[0:2]
typ, util_csr, _ = crypto_util.import_csr_file(csrfile, contents) util_csr = crypto_util.read_csr_file(csrfile, contents)
x509_req = x509.load_pem_x509_csr(util_csr.data) x509_req = x509.load_pem_x509_csr(util_csr.data)
domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions) domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions)
@@ -344,7 +344,7 @@ class HelpfulArgumentParser:
f"Unfortunately, your CSR {config.csr[0]} needs to have a SubjectAltName for " + f"Unfortunately, your CSR {config.csr[0]} needs to have a SubjectAltName for " +
"every domain or IP address") "every domain or IP address")
config.actual_csr = (util_csr, typ) config.actual_csr = util_csr
# Check that the original values for --domain and --ip-address set by the user were # Check that the original values for --domain and --ip-address set by the user were
# a subset of the domains listed in the CSR. # a subset of the domains listed in the CSR.
+1 -1
View File
@@ -1501,7 +1501,7 @@ def _csr_get_and_save_cert(config: configuration.NamespaceConfig,
:rtype: `tuple` of `str` :rtype: `tuple` of `str`
""" """
util_csr, _ = config.actual_csr util_csr = config.actual_csr
x509_req = x509.load_pem_x509_csr(util_csr.data) x509_req = x509.load_pem_x509_csr(util_csr.data)
domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions) domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions)
display_util.notify( display_util.notify(
@@ -128,35 +128,31 @@ class CSRMatchesPubkeyTest(unittest.TestCase):
test_util.load_vector('csr_512.pem'), P256_KEY) test_util.load_vector('csr_512.pem'), P256_KEY)
class ImportCSRFileTest(unittest.TestCase): class ReadCSRFileTest(unittest.TestCase):
"""Tests for certbot.certbot_util.import_csr_file.""" """Tests for certbot.certbot_util.read_csr_file."""
@classmethod @classmethod
def _call(cls, *args, **kwargs): def _call(cls, *args, **kwargs):
from certbot.crypto_util import import_csr_file from certbot.crypto_util import read_csr_file
return import_csr_file(*args, **kwargs) return read_csr_file(*args, **kwargs)
def test_der_csr(self): def test_der_csr(self):
csrfile = test_util.vector_path('csr_512.der') csrfile = test_util.vector_path('csr_512.der')
data = test_util.load_vector('csr_512.der') data = test_util.load_vector('csr_512.der')
data_pem = test_util.load_vector('csr_512.pem') data_pem = test_util.load_vector('csr_512.pem')
assert (acme_crypto_util.Format.PEM, assert util.CSR(file=csrfile,
util.CSR(file=csrfile,
data=data_pem, data=data_pem,
form="pem"), form="pem") == \
["Example.com"]) == \
self._call(csrfile, data) self._call(csrfile, data)
def test_pem_csr(self): def test_pem_csr(self):
csrfile = test_util.vector_path('csr_512.pem') csrfile = test_util.vector_path('csr_512.pem')
data = test_util.load_vector('csr_512.pem') data = test_util.load_vector('csr_512.pem')
assert (acme_crypto_util.Format.PEM, assert util.CSR(file=csrfile,
util.CSR(file=csrfile,
data=data, data=data,
form="pem"), form="pem") == \
["Example.com"],) == \
self._call(csrfile, data) self._call(csrfile, data)
def test_bad_csr(self): def test_bad_csr(self):
@@ -165,6 +161,46 @@ class ImportCSRFileTest(unittest.TestCase):
test_util.load_vector('cert_512.pem')) test_util.load_vector('cert_512.pem'))
class ImportCSRFileTest(unittest.TestCase):
"""Tests for certbot.certbot_util.import_csr_file."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.crypto_util import import_csr_file
with pytest.warns(DeprecationWarning, match='import_csr_file is deprecated'):
return import_csr_file(*args, **kwargs)
def test_der_csr(self):
csrfile = test_util.vector_path('csr_512.der')
data = test_util.load_vector('csr_512.der')
data_pem = test_util.load_vector('csr_512.pem')
with pytest.warns(DeprecationWarning, match='Format is deprecated'):
assert (acme_crypto_util.Format.PEM,
util.CSR(file=csrfile,
data=data_pem,
form="pem"),
["Example.com"]) == \
self._call(csrfile, data)
def test_pem_csr(self):
csrfile = test_util.vector_path('csr_512.pem')
data = test_util.load_vector('csr_512.pem')
with pytest.warns(DeprecationWarning, match='Format is deprecated'):
assert (acme_crypto_util.Format.PEM,
util.CSR(file=csrfile,
data=data,
form="pem"),
["Example.com"],) == \
self._call(csrfile, data)
def test_bad_csr(self):
with pytest.raises(errors.Error):
self._call(test_util.vector_path('cert_512.pem'),
test_util.load_vector('cert_512.pem'))
class MakeKeyTest(unittest.TestCase): class MakeKeyTest(unittest.TestCase):
"""Tests for certbot.crypto_util.make_key.""" """Tests for certbot.crypto_util.make_key."""
@@ -335,7 +371,8 @@ class GetSANsFromCertTest(unittest.TestCase):
@classmethod @classmethod
def _call(cls, *args, **kwargs): def _call(cls, *args, **kwargs):
from certbot.crypto_util import get_sans_from_cert from certbot.crypto_util import get_sans_from_cert
return get_sans_from_cert(*args, **kwargs) with pytest.warns(DeprecationWarning, match='get_sans_from_cert is deprecated'):
return get_sans_from_cert(*args, **kwargs)
def test_single(self): def test_single(self):
assert [] == self._call(test_util.load_vector('cert_512.pem')) assert [] == self._call(test_util.load_vector('cert_512.pem'))
@@ -351,7 +388,8 @@ class GetNamesFromCertTest(unittest.TestCase):
@classmethod @classmethod
def _call(cls, *args, **kwargs): def _call(cls, *args, **kwargs):
from certbot.crypto_util import get_names_from_cert from certbot.crypto_util import get_names_from_cert
return get_names_from_cert(*args, **kwargs) with pytest.warns(DeprecationWarning, match='get_names_from_cert is deprecated'):
return get_names_from_cert(*args, **kwargs)
def test_single(self): def test_single(self):
assert ['example.com'] == \ assert ['example.com'] == \
@@ -378,7 +416,8 @@ class GetNamesFromReqTest(unittest.TestCase):
@classmethod @classmethod
def _call(cls, *args, **kwargs): def _call(cls, *args, **kwargs):
from certbot.crypto_util import get_names_from_req from certbot.crypto_util import get_names_from_req
return get_names_from_req(*args, **kwargs) with pytest.warns(DeprecationWarning, match='get_names_from_req is deprecated'):
return get_names_from_req(*args, **kwargs)
def test_nonames(self): def test_nonames(self):
assert [] == \ assert [] == \
@@ -394,8 +433,9 @@ class GetNamesFromReqTest(unittest.TestCase):
self._call(test_util.load_vector('csr-6sans_512.pem')) self._call(test_util.load_vector('csr-6sans_512.pem'))
def test_der(self): def test_der(self):
assert ['Example.com'] == \ with pytest.warns(DeprecationWarning, match='Format is deprecated'):
self._call(test_util.load_vector('csr_512.der'), typ=acme_crypto_util.Format.DER) assert ['Example.com'] == \
self._call(test_util.load_vector('csr_512.der'), typ=acme_crypto_util.Format.DER)
class NotBeforeTest(unittest.TestCase): class NotBeforeTest(unittest.TestCase):
+78 -27
View File
@@ -12,6 +12,7 @@ import re
from typing import Optional from typing import Optional
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import warnings
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
@@ -171,9 +172,36 @@ def csr_matches_pubkey(csr: bytes, privkey: bytes) -> bool:
return req.is_signature_valid and req.public_key() == pkey.public_key() return req.is_signature_valid and req.public_key() == pkey.public_key()
def read_csr_file(
csrfile: str, data: bytes
) -> util.CSR:
"""Reads a CSR file, which can be either PEM or DER, and returns a
`certbot.util.CSR` object.
:param str csrfile: CSR filename
:param bytes data: contents of the CSR file
:returns: object representing the CSR
:rtype: util.CSR
"""
try:
# Try to parse as DER first, then fall back to PEM.
csr = x509.load_der_x509_csr(data)
except ValueError:
try:
csr = x509.load_pem_x509_csr(data)
except ValueError:
raise errors.Error("Failed to parse CSR file: {0}".format(csrfile))
# Internally we always use PEM, so re-encode as PEM before returning.
data_pem = csr.public_bytes(serialization.Encoding.PEM)
return util.CSR(file=csrfile, data=data_pem, form="pem")
def import_csr_file( def import_csr_file(
csrfile: str, data: bytes csrfile: str, data: bytes
) -> tuple[acme_crypto_util.Format, util.CSR, list[str]]: ) -> tuple['acme_crypto_util.Format', util.CSR, list[str]]:
"""Import a CSR file, which can be either PEM or DER. """Import a CSR file, which can be either PEM or DER.
:param str csrfile: CSR filename :param str csrfile: CSR filename
@@ -185,6 +213,9 @@ def import_csr_file(
:rtype: tuple :rtype: tuple
""" """
warnings.warn("certbot.crypto_util.import_csr_file is deprecated and "
"will be removed in the next major release. Please use "
"certbot.crypto_util.read_csr_file instead.", DeprecationWarning)
try: try:
# Try to parse as DER first, then fall back to PEM. # Try to parse as DER first, then fall back to PEM.
csr = x509.load_der_x509_csr(data) csr = x509.load_der_x509_csr(data)
@@ -197,11 +228,13 @@ def import_csr_file(
domains = acme_crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions) domains = acme_crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions)
# Internally we always use PEM, so re-encode as PEM before returning. # Internally we always use PEM, so re-encode as PEM before returning.
data_pem = csr.public_bytes(serialization.Encoding.PEM) data_pem = csr.public_bytes(serialization.Encoding.PEM)
return ( with warnings.catch_warnings():
acme_crypto_util.Format.PEM, warnings.filterwarnings("ignore", "acme.crypto_util.Format is deprecated")
util.CSR(file=csrfile, data=data_pem, form="pem"), return (
domains, acme_crypto_util.Format.PEM,
) util.CSR(file=csrfile, data=data_pem, form="pem"),
domains,
)
def make_key(bits: int = 2048, key_type: str = "rsa", def make_key(bits: int = 2048, key_type: str = "rsa",
@@ -389,7 +422,7 @@ def verify_fullchain(renewable_cert: interfaces.RenewableCert) -> None:
def get_sans_from_cert( def get_sans_from_cert(
cert: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM cert: bytes, typ: 'acme_crypto_util.Format | int | None' = None
) -> list[str]: ) -> list[str]:
"""Get a list of Subject Alternative Names from a certificate. """Get a list of Subject Alternative Names from a certificate.
@@ -400,12 +433,18 @@ def get_sans_from_cert(
:rtype: list :rtype: list
""" """
typ = acme_crypto_util.Format(typ) warnings.warn("get_sans_from_cert is deprecated and will be removed in the next "
if typ == acme_crypto_util.Format.PEM: "major release.", DeprecationWarning)
x509_cert = x509.load_pem_x509_certificate(cert) with warnings.catch_warnings():
else: warnings.filterwarnings("ignore", "acme.crypto_util.Format is deprecated")
assert typ == acme_crypto_util.Format.DER if typ is None:
x509_cert = x509.load_der_x509_certificate(cert) typ = acme_crypto_util.Format.PEM
typ = acme_crypto_util.Format(typ)
if typ == acme_crypto_util.Format.PEM:
x509_cert = x509.load_pem_x509_certificate(cert)
else:
assert typ == acme_crypto_util.Format.DER
x509_cert = x509.load_der_x509_certificate(cert)
try: try:
san_ext = x509_cert.extensions.get_extension_for_class( san_ext = x509_cert.extensions.get_extension_for_class(
@@ -418,7 +457,7 @@ def get_sans_from_cert(
def get_names_from_cert( def get_names_from_cert(
cert: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM cert: bytes, typ: 'acme_crypto_util.Format | int | None' = None
) -> list[str]: ) -> list[str]:
"""Get a list of domains from a cert, including the CN if it is set. """Get a list of domains from a cert, including the CN if it is set.
@@ -429,19 +468,25 @@ def get_names_from_cert(
:rtype: list :rtype: list
""" """
typ = acme_crypto_util.Format(typ) warnings.warn("get_names_from_cert is deprecated and will be removed in the next "
if typ == acme_crypto_util.Format.PEM: "major release.", DeprecationWarning)
x509_cert = x509.load_pem_x509_certificate(cert) with warnings.catch_warnings():
else: warnings.filterwarnings("ignore", "acme.crypto_util.Format is deprecated")
assert typ == acme_crypto_util.Format.DER if typ is None:
x509_cert = x509.load_der_x509_certificate(cert) typ = acme_crypto_util.Format.PEM
typ = acme_crypto_util.Format(typ)
if typ == acme_crypto_util.Format.PEM:
x509_cert = x509.load_pem_x509_certificate(cert)
else:
assert typ == acme_crypto_util.Format.DER
x509_cert = x509.load_der_x509_certificate(cert)
return acme_crypto_util.get_names_from_subject_and_extensions( return acme_crypto_util.get_names_from_subject_and_extensions(
x509_cert.subject, x509_cert.extensions x509_cert.subject, x509_cert.extensions
) )
def get_names_from_req( def get_names_from_req(
csr: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM csr: bytes, typ: 'acme_crypto_util.Format | int | None' = None
) -> list[str]: ) -> list[str]:
"""Get a list of domains from a CSR, including the CN if it is set. """Get a list of domains from a CSR, including the CN if it is set.
@@ -451,12 +496,18 @@ def get_names_from_req(
:rtype: list :rtype: list
""" """
typ = acme_crypto_util.Format(typ) warnings.warn("get_names_from_req is deprecated and will be removed in the next "
if typ == acme_crypto_util.Format.PEM: "major release.", DeprecationWarning)
x509_req = x509.load_pem_x509_csr(csr) with warnings.catch_warnings():
else: warnings.filterwarnings("ignore", "acme.crypto_util.Format is deprecated")
assert typ == acme_crypto_util.Format.DER if typ is None:
x509_req = x509.load_der_x509_csr(csr) typ = acme_crypto_util.Format.PEM
typ = acme_crypto_util.Format(typ)
if typ == acme_crypto_util.Format.PEM:
x509_req = x509.load_pem_x509_csr(csr)
else:
assert typ == acme_crypto_util.Format.DER
x509_req = x509.load_der_x509_csr(csr)
return acme_crypto_util.get_names_from_subject_and_extensions( return acme_crypto_util.get_names_from_subject_and_extensions(
x509_req.subject, x509_req.extensions x509_req.subject, x509_req.extensions
) )
+6
View File
@@ -0,0 +1,6 @@
A few largely unused functions/types have been deprecated in our effort to remove our pyOpenSSL dependency:
* Deprecated: `certbot.crypto_util.get_sans_from_cert`
* Deprecated: `certbot.crypto_util.get_names_from_cert`
* Deprecated: `certbot.crypto_util.get_names_from_req`
* Deprecated: `certbot.crypto_util.import_csr_file` (and replaced by `certbot.crypto_util.read_csr_file`)
* Deprecated: `acme.crypto_util.Format`