--- Begin Message ---
Package: release.debian.org
Severity: normal
X-Debbugs-Cc: python-acme@packages.debian.org, hlieberman@debian.org
Control: affects -1 + src:python-acme
User: release.debian.org@packages.debian.org
Usertags: unblock
User: hlieberman@debian.org
Usertags: trixie-certbot
Please unblock package python-acme
[ Reason ]
The certbot 2.x series is end of life and will not receive further updates or
backports of changes.
(https://github.com/certbot/certbot/wiki/Architectural-Decision-Records-2025#-update-to-certbots-version-policy-and-end-of-life-support-on-previous-major-versions)
By far and away, the primary purpose of certbot is to receive certificates from
Let's Encrypt, and the Let's Encrypt team are planning API changes in 2025 which
will break the issuance of TLS certificates for people using the Certbot 2.x
series.
[ Impact ]
If the unblock is not granted, certbot will suddenly stop working at
some point in the future and users' TLS certificates will expire. Because
certbot tends to be used as a set-it-and-forget-it system, and Let's Encrypt has
recently disabled their email notifications, users' websites and applications
may suddenly be unavailable to users and/or vulnerable to MitM.
[ Tests ]
Certbot's two primary plugins (python-certbot-apache, python-certbot-nginx) and
the main utility (python-certbot) have a test harness which exercises the entire
process of getting a certificate against a test environment. This provides very
high confidence that those packages are still working, and that the libraries
which they depend on (python-josepy, python-acme) are in good health. These
tests pass cleanly on ci.d.n for all three invocations.
The dns plugin packages (python-certbot-dns-*) are substantially less
complicated than the other certbot packages and primarily handle communication
with various companies' API layers. Those are unlikely to have broken because of
the changes to certbot's internals; the primary way in which those packages
break are due to API changes on the providers' ends.
[ Risks ]
Upgrading the packages across major versions comes with risks, certainly, but
there is little in the way of alternative. The changes are too complex for me to
be willing to attempt to backport, and in a security critical application, I am
even more reticent than I normally would be. I recognize the late application
introduces even more risk --- and rightfully, I'm sure no small amount of
annoyance --- but it is where we've ended up.
[ Checklist ]
[X] all changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in testing
[ Other info ]
This is one of a series of identical unblock requests with only
package names and debdiffs differing.
unblock python-acme/4.0.0-1
diff -Nru python-acme-2.11.0/acme/challenges.py python-acme-4.0.0/acme/challenges.py
--- python-acme-2.11.0/acme/challenges.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/challenges.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,5 @@
"""ACME Identifier Validation Challenges."""
import abc
-import codecs
import functools
import hashlib
import logging
@@ -15,6 +14,7 @@
from typing import TypeVar
from typing import Union
+from cryptography import x509
from cryptography.hazmat.primitives import hashes
import josepy as jose
from OpenSSL import crypto
@@ -89,7 +89,7 @@
:ivar bytes token:
"""
- TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
+ TOKEN_SIZE = 128 // 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
# TODO: acme-spec doesn't specify token as base64-encoded value
@@ -419,7 +419,7 @@
return hashlib.sha256(self.key_authorization.encode('utf-8')).digest()
def gen_cert(self, domain: str, key: Optional[crypto.PKey] = None, bits: int = 2048
- ) -> Tuple[crypto.X509, crypto.PKey]:
+ ) -> Tuple[x509.Certificate, crypto.PKey]:
"""Generate tls-alpn-01 certificate.
:param str domain: Domain verified by the challenge.
@@ -428,22 +428,32 @@
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
- :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
+ :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey`
"""
if key is None:
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, bits)
- der_value = b"DER:" + codecs.encode(self.h, 'hex')
- acme_extension = crypto.X509Extension(self.ID_PE_ACME_IDENTIFIER_V1,
- critical=True, value=der_value)
-
- return crypto_util.gen_ss_cert(key, [domain], force_san=True,
- extensions=[acme_extension]), key
+ oid = x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode())
+ acme_extension = x509.Extension(
+ oid,
+ critical=True,
+ value=x509.UnrecognizedExtension(oid, self.h)
+ )
+
+ cryptography_key = key.to_cryptography_key()
+ assert isinstance(cryptography_key, crypto_util.CertificateIssuerPrivateKeyTypesTpl)
+ cert = crypto_util.make_self_signed_cert(
+ cryptography_key,
+ [domain],
+ force_san=True,
+ extensions=[acme_extension]
+ )
+ return cert, key
def probe_cert(self, domain: str, host: Optional[str] = None,
- port: Optional[int] = None) -> crypto.X509:
+ port: Optional[int] = None) -> x509.Certificate:
"""Probe tls-alpn-01 challenge certificate.
:param str domain: domain being validated, required.
@@ -460,38 +470,40 @@
return crypto_util.probe_sni(host=host.encode(), port=port, name=domain.encode(),
alpn_protocols=[self.ACME_TLS_1_PROTOCOL])
- def verify_cert(self, domain: str, cert: crypto.X509) -> bool:
+ def verify_cert(self, domain: str, cert: x509.Certificate, ) -> bool:
"""Verify tls-alpn-01 challenge certificate.
:param str domain: Domain name being validated.
- :param OpensSSL.crypto.X509 cert: Challenge certificate.
+ :param cert: Challenge certificate.
+ :type cert: `cryptography.x509.Certificate`
:returns: Whether the certificate was successfully verified.
:rtype: bool
"""
- # pylint: disable=protected-access
- names = crypto_util._pyopenssl_cert_or_req_all_names(cert)
- # Type ignore needed due to
- # https://github.com/pyca/pyopenssl/issues/730.
- logger.debug('Certificate %s. SANs: %s',
- cert.digest('sha256'), names)
+ names = crypto_util.get_names_from_subject_and_extensions(
+ cert.subject, cert.extensions
+ )
+ logger.debug(
+ "Certificate %s. SANs: %s", cert.fingerprint(hashes.SHA256()), names
+ )
if len(names) != 1 or names[0].lower() != domain.lower():
return False
- for i in range(cert.get_extension_count()):
- ext = cert.get_extension(i)
- # FIXME: assume this is the ACME extension. Currently there is no
- # way to get full OID of an unknown extension from pyopenssl.
- if ext.get_short_name() == b'UNDEF':
- data = ext.get_data()
- return data == self.h
+ try:
+ ext = cert.extensions.get_extension_for_oid(
+ x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode())
+ )
+ except x509.ExtensionNotFound:
+ return False
- return False
+ # This is for the type checker.
+ assert isinstance(ext.value, x509.UnrecognizedExtension)
+ return ext.value.value == self.h
# pylint: disable=too-many-arguments
def simple_verify(self, chall: 'TLSALPN01', domain: str, account_public_key: jose.JWK,
- cert: Optional[crypto.X509] = None, host: Optional[str] = None,
+ cert: Optional[x509.Certificate] = None, host: Optional[str] = None,
port: Optional[int] = None) -> bool:
"""Simple verify.
@@ -501,7 +513,7 @@
:param .challenges.TLSALPN01 chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param JWK account_public_key:
- :param OpenSSL.crypto.X509 cert: Optional certificate. If not
+ :param x509.Certificate cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param string host: IP address used to probe the certificate.
@@ -532,7 +544,8 @@
response_cls = TLSALPN01Response
typ = response_cls.typ
- def validation(self, account_key: jose.JWK, **kwargs: Any) -> Tuple[crypto.X509, crypto.PKey]:
+ def validation(self, account_key: jose.JWK,
+ **kwargs: Any) -> Tuple[x509.Certificate, crypto.PKey]:
"""Generate validation.
:param JWK account_key:
@@ -541,7 +554,7 @@
in certificate generation. If not provided (``None``), then
fresh key will be generated.
- :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
+ :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey`
"""
# TODO: Remove cast when response() is generic.
diff -Nru python-acme-2.11.0/acme/client.py python-acme-4.0.0/acme/client.py
--- python-acme-2.11.0/acme/client.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/client.py 2025-04-07 18:03:33.000000000 -0400
@@ -15,8 +15,9 @@
from typing import Tuple
from typing import Union
+from cryptography import x509
+
import josepy as jose
-import OpenSSL
import requests
from requests.adapters import HTTPAdapter
from requests.utils import parse_header_links
@@ -113,7 +114,7 @@
self.net.account = new_regr
return new_regr
- def new_order(self, csr_pem: bytes) -> messages.OrderResource:
+ def new_order(self, csr_pem: bytes, profile: Optional[str] = None) -> messages.OrderResource:
"""Request a new Order object from the server.
:param bytes csr_pem: A CSR in PEM format.
@@ -121,19 +122,24 @@
:returns: The newly created order.
:rtype: OrderResource
"""
- csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
- # pylint: disable=protected-access
- dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
- ipNames = crypto_util._pyopenssl_cert_or_req_san_ip(csr)
- # ipNames is now []string
+ csr = x509.load_pem_x509_csr(csr_pem)
+ dnsNames = crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions)
+ try:
+ san_ext = csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
+ except x509.ExtensionNotFound:
+ ipNames = []
+ else:
+ ipNames = san_ext.value.get_values_for_type(x509.IPAddress)
identifiers = []
for name in dnsNames:
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
value=name))
- for ips in ipNames:
+ for ip in ipNames:
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP,
- value=ips))
- order = messages.NewOrder(identifiers=identifiers)
+ value=str(ip)))
+ if profile is None:
+ profile = ""
+ order = messages.NewOrder(identifiers=identifiers, profile=profile)
response = self._post(self.directory['newOrder'], order)
body = messages.Order.from_json(response.json())
authorizations = []
@@ -219,9 +225,8 @@
:returns: updated order
:rtype: messages.OrderResource
"""
- csr = OpenSSL.crypto.load_certificate_request(
- OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem)
- wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr))
+ csr = x509.load_pem_x509_csr(orderr.csr_pem)
+ wrapped_csr = messages.CertificateRequest(csr=csr)
res = self._post(orderr.body.finalize, wrapped_csr)
orderr = orderr.update(body=messages.Order.from_json(res.json()))
return orderr
@@ -274,11 +279,10 @@
self.begin_finalization(orderr)
return self.poll_finalization(orderr, deadline, fetch_alternative_chains)
- def revoke(self, cert: jose.ComparableX509, rsn: int) -> None:
+ def revoke(self, cert: x509.Certificate, rsn: int) -> None:
"""Revoke certificate.
- :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
- `.ComparableX509`
+ :param x509.Certificate cert: `x509.Certificate`
:param int rsn: Reason code for certificate revocation.
@@ -466,11 +470,10 @@
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
- def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None:
+ def _revoke(self, cert: x509.Certificate, rsn: int, url: str) -> None:
"""Revoke certificate.
- :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
- `.ComparableX509`
+ :param .x509.Certificate cert: `x509.Certificate`
:param int rsn: Reason code for certificate revocation.
diff -Nru python-acme-2.11.0/acme/crypto_util.py python-acme-4.0.0/acme/crypto_util.py
--- python-acme-2.11.0/acme/crypto_util.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/crypto_util.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,14 +1,15 @@
"""Crypto utilities."""
-import binascii
import contextlib
+import enum
+from datetime import datetime, timedelta, timezone
import ipaddress
import logging
-import os
-import re
import socket
+import typing
from typing import Any
from typing import Callable
from typing import List
+from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Sequence
@@ -16,7 +17,10 @@
from typing import Tuple
from typing import Union
-import josepy as jose
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec, ed25519, ed448, types
+from cryptography.hazmat.primitives.serialization import Encoding
from OpenSSL import crypto
from OpenSSL import SSL
@@ -26,19 +30,43 @@
# Default SSL method selected here is the most compatible, while secure
# SSL method: TLSv1_METHOD is only compatible with
-# TLSv1_METHOD, while SSLv23_METHOD is compatible with all other
+# TLSv1_METHOD, while TLS_method is compatible with all other
# methods, including TLSv2_METHOD (read more at
-# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
+# https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _serve_sni
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
# in case it's used for things other than probing/serving!
-_DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD
+_DEFAULT_SSL_METHOD = SSL.TLS_METHOD
+
+
+class Format(enum.IntEnum):
+ """File format to be used when parsing or serializing X.509 structures.
+
+ Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants
+ from pyOpenSSL.
+ """
+ DER = crypto.FILETYPE_ASN1
+ PEM = crypto.FILETYPE_PEM
+
+ def to_cryptography_encoding(self) -> Encoding:
+ """Converts the Format to the corresponding cryptography `Encoding`.
+ """
+ if self == Format.DER:
+ return Encoding.DER
+ else:
+ return Encoding.PEM
+
+
+_KeyAndCert = Union[
+ Tuple[crypto.PKey, crypto.X509],
+ Tuple[types.CertificateIssuerPrivateKeyTypes, x509.Certificate],
+]
class _DefaultCertSelection:
- def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]]):
+ def __init__(self, certs: Mapping[bytes, _KeyAndCert]):
self.certs = certs
- def __call__(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey, crypto.X509]]:
+ def __call__(self, connection: SSL.Connection) -> Optional[_KeyAndCert]:
server_name = connection.get_servername()
if server_name:
return self.certs.get(server_name, None)
@@ -58,14 +86,19 @@
`certs` parameter would be ignored, and therefore must be empty.
"""
- def __init__(self, sock: socket.socket,
- certs: Optional[Mapping[bytes, Tuple[crypto.PKey, crypto.X509]]] = None,
- method: int = _DEFAULT_SSL_METHOD,
- alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None,
- cert_selection: Optional[Callable[[SSL.Connection],
- Optional[Tuple[crypto.PKey,
- crypto.X509]]]] = None
- ) -> None:
+ def __init__(
+ self,
+ sock: socket.socket,
+ certs: Optional[Mapping[bytes, _KeyAndCert]] = None,
+ method: int = _DEFAULT_SSL_METHOD,
+ alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None,
+ cert_selection: Optional[
+ Callable[
+ [SSL.Connection],
+ Optional[_KeyAndCert],
+ ]
+ ] = None,
+ ) -> None:
self.sock = sock
self.alpn_selection = alpn_selection
self.method = method
@@ -73,13 +106,9 @@
raise ValueError("Neither cert_selection or certs specified.")
if cert_selection and certs:
raise ValueError("Both cert_selection and certs specified.")
- actual_cert_selection: Union[_DefaultCertSelection,
- Optional[Callable[[SSL.Connection],
- Optional[Tuple[crypto.PKey,
- crypto.X509]]]]] = cert_selection
- if actual_cert_selection is None:
- actual_cert_selection = _DefaultCertSelection(certs if certs else {})
- self.cert_selection = actual_cert_selection
+ if cert_selection is None:
+ cert_selection = _DefaultCertSelection(certs if certs else {})
+ self.cert_selection = cert_selection
def __getattr__(self, name: str) -> Any:
return getattr(self.sock, name)
@@ -103,9 +132,10 @@
return
key, cert = pair
new_context = SSL.Context(self.method)
- new_context.set_options(SSL.OP_NO_SSLv2)
- new_context.set_options(SSL.OP_NO_SSLv3)
+ new_context.set_min_proto_version(SSL.TLS1_2_VERSION)
new_context.use_privatekey(key)
+ if isinstance(cert, x509.Certificate):
+ cert = crypto.X509.from_cryptography(cert)
new_context.use_certificate(cert)
if self.alpn_selection is not None:
new_context.set_alpn_select_callback(self.alpn_selection)
@@ -131,7 +161,7 @@
# in the standard library. This is useful when this object is
# used by code which expects a standard socket such as
# socketserver in the standard library.
- raise socket.error(error)
+ raise OSError(error)
def accept(self) -> Tuple[FakeConnection, Any]: # pylint: disable=missing-function-docstring
sock, addr = self.sock.accept()
@@ -155,7 +185,7 @@
except SSL.Error as error:
# _pick_certificate_cb might have returned without
# creating SSL context (wrong server name)
- raise socket.error(error)
+ raise OSError(error)
return ssl_sock, addr
except:
@@ -167,7 +197,7 @@
def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # pylint: disable=too-many-arguments
method: int = _DEFAULT_SSL_METHOD, source_address: Tuple[str, int] = ('', 0),
- alpn_protocols: Optional[Sequence[bytes]] = None) -> crypto.X509:
+ alpn_protocols: Optional[Sequence[bytes]] = None) -> x509.Certificate:
"""Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the
@@ -185,7 +215,7 @@
:raises acme.errors.Error: In case of any problems.
:returns: SSL certificate presented by the server.
- :rtype: OpenSSL.crypto.X509
+ :rtype: cryptography.x509.Certificate
"""
context = SSL.Context(method)
@@ -203,7 +233,7 @@
)
socket_tuple: Tuple[bytes, int] = (host, port)
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore[arg-type]
- except socket.error as error:
+ except OSError as error:
raise errors.Error(error)
with contextlib.closing(sock) as client:
@@ -211,7 +241,7 @@
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
if alpn_protocols is not None:
- client_ssl.set_alpn_protos(alpn_protocols)
+ client_ssl.set_alpn_protos(list(alpn_protocols))
try:
client_ssl.do_handshake()
client_ssl.shutdown()
@@ -219,240 +249,243 @@
raise errors.Error(error)
cert = client_ssl.get_peer_certificate()
assert cert # Appease mypy. We would have crashed out by now if there was no certificate.
- return cert
+ return cert.to_cryptography()
-def make_csr(private_key_pem: bytes, domains: Optional[Union[Set[str], List[str]]] = None,
- must_staple: bool = False,
- ipaddrs: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None
- ) -> bytes:
+# Even *more* annoyingly, due to a mypy bug, we can't use Union[] types in
+# 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
+# work due to another mypy bug.
+#
+# mypy issues:
+# * https://github.com/python/mypy/issues/17680
+# * https://github.com/python/mypy/issues/15106
+CertificateIssuerPrivateKeyTypesTpl = (
+ dsa.DSAPrivateKey,
+ rsa.RSAPrivateKey,
+ ec.EllipticCurvePrivateKey,
+ ed25519.Ed25519PrivateKey,
+ ed448.Ed448PrivateKey,
+)
+
+
+def make_csr(
+ private_key_pem: bytes,
+ domains: Optional[Union[Set[str], List[str]]] = None,
+ must_staple: bool = False,
+ ipaddrs: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None,
+) -> bytes:
"""Generate a CSR containing domains or IPs as subjectAltNames.
+ Parameters are ordered this way for backwards compatibility when called using positional
+ arguments.
+
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
:param list domains: List of DNS names to include in subjectAltNames of CSR.
:param bool must_staple: Whether to include the TLS Feature extension (aka
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
:param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address)
- names to include in subbjectAltNames of CSR.
- params ordered this way for backward competablity when called by positional argument.
+ names to include in subbjectAltNames of CSR.
+
:returns: buffer PEM-encoded Certificate Signing Request.
+
"""
- private_key = crypto.load_privatekey(
- crypto.FILETYPE_PEM, private_key_pem)
- csr = crypto.X509Req()
- sanlist = []
- # if domain or ip list not supplied make it empty list so it's easier to iterate
+ private_key = serialization.load_pem_private_key(private_key_pem, password=None)
+ if not isinstance(private_key, CertificateIssuerPrivateKeyTypesTpl):
+ raise ValueError(f"Invalid private key type: {type(private_key)}")
if domains is None:
domains = []
if ipaddrs is None:
ipaddrs = []
- if len(domains)+len(ipaddrs) == 0:
- raise ValueError("At least one of domains or ipaddrs parameter need to be not empty")
- for address in domains:
- sanlist.append('DNS:' + address)
- for ips in ipaddrs:
- sanlist.append('IP:' + ips.exploded)
- # make sure its ascii encoded
- san_string = ', '.join(sanlist).encode('ascii')
- # for IP san it's actually need to be octet-string,
- # but somewhere downsteam thankfully handle it for us
- extensions = [
- crypto.X509Extension(
- b'subjectAltName',
+ if len(domains) + len(ipaddrs) == 0:
+ raise ValueError(
+ "At least one of domains or ipaddrs parameter need to be not empty"
+ )
+
+ builder = (
+ x509.CertificateSigningRequestBuilder()
+ .subject_name(x509.Name([]))
+ .add_extension(
+ x509.SubjectAlternativeName(
+ [x509.DNSName(d) for d in domains]
+ + [x509.IPAddress(i) for i in ipaddrs]
+ ),
critical=False,
- value=san_string
- ),
- ]
+ )
+ )
if must_staple:
- extensions.append(crypto.X509Extension(
- b"1.3.6.1.5.5.7.1.24",
+ builder = builder.add_extension(
+ # "status_request" is the feature commonly known as OCSP
+ # Must-Staple
+ x509.TLSFeature([x509.TLSFeatureType.status_request]),
critical=False,
- value=b"DER:30:03:02:01:05"))
- csr.add_extensions(extensions)
- csr.set_pubkey(private_key)
- # RFC 2986 Section 4.1 only defines version 0
- csr.set_version(0)
- csr.sign(private_key, 'sha256')
- return crypto.dump_certificate_request(
- crypto.FILETYPE_PEM, csr)
-
-
-def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req]
- ) -> List[str]:
- # unlike its name this only outputs DNS names, other type of idents will ignored
- common_name = loaded_cert_or_req.get_subject().CN
- sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
-
- if common_name is None:
- return sans
- return [common_name] + [d for d in sans if d != common_name]
-
+ )
-def _pyopenssl_cert_or_req_san(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
- """Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
+ csr = builder.sign(private_key, hashes.SHA256())
+ return csr.public_bytes(Encoding.PEM)
- .. todo:: Implement directly in PyOpenSSL!
- .. note:: Although this is `acme` internal API, it is used by
- `letsencrypt`.
+def get_names_from_subject_and_extensions(
+ subject: x509.Name, exts: x509.Extensions
+) -> List[str]:
+ """Gets all DNS SAN names as well as the first Common Name from subject.
- :param cert_or_req: Certificate or CSR.
- :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
+ :param subject: Name of the x509 object, which may include Common Name
+ :type subject: `cryptography.x509.Name`
+ :param exts: Extensions of the x509 object, which may include SANs
+ :type exts: `cryptography.x509.Extensions`
- :returns: A list of Subject Alternative Names that is DNS.
+ :returns: List of DNS Subject Alternative Names and first Common Name
:rtype: `list` of `str`
-
- """
- # This function finds SANs with dns name
-
- # constants based on PyOpenSSL certificate/CSR text dump
- part_separator = ":"
- prefix = "DNS" + part_separator
-
- sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
-
- return [part.split(part_separator)[1]
- for part in sans_parts if part.startswith(prefix)]
-
-
-def _pyopenssl_cert_or_req_san_ip(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
- """Get Subject Alternative Names IPs from certificate or CSR using pyOpenSSL.
-
- :param cert_or_req: Certificate or CSR.
- :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
-
- :returns: A list of Subject Alternative Names that are IP Addresses.
- :rtype: `list` of `str`. note that this returns as string, not IPaddress object
-
"""
+ # We know these are always `str` because `bytes` is only possible for
+ # other OIDs.
+ cns = [
+ typing.cast(str, c.value)
+ for c in subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
+ ]
+ try:
+ san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)
+ except x509.ExtensionNotFound:
+ dns_names = []
+ else:
+ dns_names = san_ext.value.get_values_for_type(x509.DNSName)
- # constants based on PyOpenSSL certificate/CSR text dump
- part_separator = ":"
- prefix = "IP Address" + part_separator
-
- sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
+ if not cns:
+ return dns_names
+ else:
+ # We only include the first CN, if there are multiple. This matches
+ # the behavior of the previously implementation using pyOpenSSL.
+ return [cns[0]] + [d for d in dns_names if d != cns[0]]
- return [part[len(prefix):] for part in sans_parts if part.startswith(prefix)]
+def _cryptography_cert_or_req_san(
+ cert_or_req: Union[x509.Certificate, x509.CertificateSigningRequest],
+) -> List[str]:
+ """Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
-def _pyopenssl_extract_san_list_raw(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
- """Get raw SAN string from cert or csr, parse it as UTF-8 and return.
+ .. note:: Although this is `acme` internal API, it is used by
+ `letsencrypt`.
:param cert_or_req: Certificate or CSR.
- :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
+ :type cert_or_req: `x509.Certificate` or `x509.CertificateSigningRequest`.
- :returns: raw san strings, parsed byte as utf-8
+ :returns: A list of Subject Alternative Names that is DNS.
:rtype: `list` of `str`
+ Deprecated
+ .. deprecated: 3.2.1
"""
- # This function finds SANs by dumping the certificate/CSR to text and
- # searching for "X509v3 Subject Alternative Name" in the text. This method
- # is used to because in PyOpenSSL version <0.17 `_subjectAltNameString` methods are
- # not able to Parse IP Addresses in subjectAltName string.
-
- if isinstance(cert_or_req, crypto.X509):
- # pylint: disable=line-too-long
- text = crypto.dump_certificate(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8')
- else:
- text = crypto.dump_certificate_request(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8')
- # WARNING: this function does not support multiple SANs extensions.
- # Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
- raw_san = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
-
- parts_separator = ", "
- # WARNING: this function assumes that no SAN can include
- # parts_separator, hence the split!
- sans_parts = [] if raw_san is None else raw_san.group(1).split(parts_separator)
- return sans_parts
-
-
-def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None,
- not_before: Optional[int] = None,
- validity: int = (7 * 24 * 60 * 60), force_san: bool = True,
- extensions: Optional[List[crypto.X509Extension]] = None,
- ips: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None
- ) -> crypto.X509:
+ # ???: is this translation needed?
+ exts = cert_or_req.extensions
+ try:
+ san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)
+ except x509.ExtensionNotFound:
+ return []
+
+ return san_ext.value.get_values_for_type(x509.DNSName)
+
+
+# Helper function that can be mocked in unit tests
+def _now() -> datetime:
+ return datetime.now(tz=timezone.utc)
+
+
+def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes,
+ domains: Optional[List[str]] = None,
+ not_before: Optional[datetime] = None,
+ validity: Optional[timedelta] = None, force_san: bool = True,
+ extensions: Optional[List[x509.Extension]] = None,
+ ips: Optional[List[Union[ipaddress.IPv4Address,
+ ipaddress.IPv6Address]]] = None
+ ) -> x509.Certificate:
"""Generate new self-signed certificate.
-
+ :param buffer private_key_pem: Private key, in PEM PKCS#8 format.
:type domains: `list` of `str`
- :param OpenSSL.crypto.PKey key:
+ :param int not_before: A datetime after which the cert is valid. If no
+ timezone is specified, UTC is assumed
+ :type not_before: `datetime.datetime`
+ :param validity: Duration for which the cert will be valid. Defaults to 1
+ week
+ :type validity: `datetime.timedelta`
+ :param buffer private_key_pem: One of
+ `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes`
:param bool force_san:
:param extensions: List of additional extensions to include in the cert.
- :type extensions: `list` of `OpenSSL.crypto.X509Extension`
+ :type extensions: `list` of `x509.Extension[x509.ExtensionType]`
:type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`)
-
If more than one domain is provided, all of the domains are put into
``subjectAltName`` X.509 extension and first domain is set as the
subject CN. If only one domain is provided no ``subjectAltName``
extension is used, unless `force_san` is ``True``.
-
"""
assert domains or ips, "Must provide one or more hostnames or IPs for the cert."
- cert = crypto.X509()
- cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
- cert.set_version(2)
+ builder = x509.CertificateBuilder()
+ builder = builder.serial_number(x509.random_serial_number())
- if extensions is None:
- extensions = []
+ if extensions is not None:
+ for ext in extensions:
+ builder = builder.add_extension(ext.value, ext.critical)
if domains is None:
domains = []
if ips is None:
ips = []
- extensions.append(
- crypto.X509Extension(
- b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
- )
+ builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
+ name_attrs = []
if len(domains) > 0:
- cert.get_subject().CN = domains[0]
- # TODO: what to put into cert.get_subject()?
- cert.set_issuer(cert.get_subject())
+ name_attrs.append(x509.NameAttribute(
+ x509.OID_COMMON_NAME,
+ domains[0]
+ ))
+
+ builder = builder.subject_name(x509.Name(name_attrs))
+ builder = builder.issuer_name(x509.Name(name_attrs))
- sanlist = []
+ sanlist: List[x509.GeneralName] = []
for address in domains:
- sanlist.append('DNS:' + address)
+ sanlist.append(x509.DNSName(address))
for ip in ips:
- sanlist.append('IP:' + ip.exploded)
- san_string = ', '.join(sanlist).encode('ascii')
+ sanlist.append(x509.IPAddress(ip))
if force_san or len(domains) > 1 or len(ips) > 0:
- extensions.append(crypto.X509Extension(
- b"subjectAltName",
- critical=False,
- value=san_string
- ))
-
- cert.add_extensions(extensions)
-
- cert.gmtime_adj_notBefore(0 if not_before is None else not_before)
- cert.gmtime_adj_notAfter(validity)
-
- cert.set_pubkey(key)
- cert.sign(key, "sha256")
- return cert
-
+ builder = builder.add_extension(
+ x509.SubjectAlternativeName(sanlist),
+ critical=False
+ )
-def dump_pyopenssl_chain(chain: Union[List[jose.ComparableX509], List[crypto.X509]],
- filetype: int = crypto.FILETYPE_PEM) -> bytes:
+ if not_before is None:
+ not_before = _now()
+ if validity is None:
+ validity = timedelta(seconds=7 * 24 * 60 * 60)
+ builder = builder.not_valid_before(not_before)
+ builder = builder.not_valid_after(not_before + validity)
+
+ public_key = private_key.public_key()
+ builder = builder.public_key(public_key)
+ return builder.sign(private_key, hashes.SHA256())
+
+
+def dump_cryptography_chain(
+ chain: List[x509.Certificate],
+ encoding: Literal[Encoding.PEM, Encoding.DER] = Encoding.PEM,
+) -> bytes:
"""Dump certificate chain into a bundle.
- :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
- :class:`josepy.util.ComparableX509`).
+ :param list chain: List of `cryptography.x509.Certificate`.
:returns: certificate chain bundle
:rtype: bytes
+ Deprecated
+ .. deprecated: 3.2.1
"""
# XXX: returns empty string when no chain is available, which
# shuts up RenewableCert, but might not be the best solution...
- def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes:
- if isinstance(cert, jose.ComparableX509):
- if isinstance(cert.wrapped, crypto.X509Req):
- raise errors.Error("Unexpected CSR provided.") # pragma: no cover
- cert = cert.wrapped
- return crypto.dump_certificate(filetype, cert)
+ def _dump_cert(cert: x509.Certificate) -> bytes:
+ return cert.public_bytes(encoding)
- # assumes that OpenSSL.crypto.dump_certificate includes ending
+ # assumes that x509.Certificate.public_bytes includes ending
# newline character
return b"".join(_dump_cert(cert) for cert in chain)
diff -Nru python-acme-2.11.0/acme/_internal/tests/challenges_test.py python-acme-4.0.0/acme/_internal/tests/challenges_test.py
--- python-acme-2.11.0/acme/_internal/tests/challenges_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/challenges_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -13,7 +13,7 @@
from acme import errors
from acme._internal.tests import test_util
-CERT = test_util.load_comparable_cert('cert.pem')
+CERT = test_util.load_cert('cert.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
diff -Nru python-acme-2.11.0/acme/_internal/tests/client_test.py python-acme-4.0.0/acme/_internal/tests/client_test.py
--- python-acme-2.11.0/acme/_internal/tests/client_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/client_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -24,6 +24,7 @@
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem')
+CSR_NO_SANS_PEM = test_util.load_vector('csr-nosans.pem')
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
DIRECTORY_V2 = messages.Directory({
@@ -97,6 +98,10 @@
body=self.order,
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_MIXED_PEM)
+ self.orderr2 = messages.OrderResource(
+ body=self.order,
+ uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
+ authorizations=[self.authzr, self.authzr2], csr_pem=CSR_NO_SANS_PEM)
def test_new_account(self):
self.response.status_code = http_client.CREATED
@@ -158,6 +163,10 @@
mock_post_as_get.side_effect = (authz_response, authz_response2)
assert self.client.new_order(CSR_MIXED_PEM) == self.orderr
+ with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:
+ mock_post_as_get.side_effect = (authz_response, authz_response2)
+ assert self.client.new_order(CSR_NO_SANS_PEM) == self.orderr2
+
def test_answer_challege(self):
self.response.links['up'] = {'url': self.challr.authzr_uri}
self.response.json.return_value = self.challr.body.to_json()
diff -Nru python-acme-2.11.0/acme/_internal/tests/crypto_util_test.py python-acme-4.0.0/acme/_internal/tests/crypto_util_test.py
--- python-acme-2.11.0/acme/_internal/tests/crypto_util_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/crypto_util_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -8,23 +8,33 @@
import time
from typing import List
import unittest
+from unittest import mock
+import warnings
-import josepy as jose
-import OpenSSL
import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa, x25519
from acme import errors
from acme._internal.tests import test_util
+class FormatTest(unittest.TestCase):
+ def test_to_cryptography_encoding(self):
+ from acme.crypto_util import Format
+ assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER
+ assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM
+
+
class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
def setUp(self):
- self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
+ self.cert = test_util.load_cert('rsa2048_cert.pem')
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
# pylint: disable=protected-access
- certs = {b'foo': (key, self.cert.wrapped)}
+ certs = {b'foo': (key, self.cert)}
from acme.crypto_util import SSLSocket
@@ -46,8 +56,7 @@
def _probe(self, name):
from acme.crypto_util import probe_sni
- return jose.ComparableX509(probe_sni(
- name, host='127.0.0.1', port=self.port))
+ return probe_sni(name, host='127.0.0.1', port=self.port)
def _start_server(self):
self.server_thread.start()
@@ -85,38 +94,29 @@
_ = SSLSocket(None)
-class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
- """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names."""
-
- @classmethod
- def _call(cls, loader, name):
- # pylint: disable=protected-access
- from acme.crypto_util import _pyopenssl_cert_or_req_all_names
- return _pyopenssl_cert_or_req_all_names(loader(name))
+class MiscTests(unittest.TestCase):
- def _call_cert(self, name):
- return self._call(test_util.load_cert, name)
+ def test_dump_cryptography_chain(self):
+ from acme.crypto_util import dump_cryptography_chain
- def test_cert_one_san_no_common(self):
- assert self._call_cert('cert-nocn.der') == \
- ['no-common-name.badssl.com']
+ cert1 = test_util.load_cert('rsa2048_cert.pem')
+ cert2 = test_util.load_cert('rsa4096_cert.pem')
- def test_cert_no_sans_yes_common(self):
- assert self._call_cert('cert.pem') == ['example.com']
+ chain = [cert1, cert2]
+ dumped = dump_cryptography_chain(chain)
- def test_cert_two_sans_yes_common(self):
- assert self._call_cert('cert-san.pem') == \
- ['example.com', 'www.example.com']
+ # default is PEM encoding Encoding.PEM
+ assert isinstance(dumped, bytes)
-class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
- """Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
+class CryptographyCertOrReqSANTest(unittest.TestCase):
+ """Test for acme.crypto_util._cryptography_cert_or_req_san."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
- from acme.crypto_util import _pyopenssl_cert_or_req_san
- return _pyopenssl_cert_or_req_san(loader(name))
+ from acme.crypto_util import _cryptography_cert_or_req_san
+ return _cryptography_cert_or_req_san(loader(name))
@classmethod
def _get_idn_names(cls):
@@ -177,72 +177,90 @@
['chicago-cubs.venafi.example', 'cubs.venafi.example']
-class PyOpenSSLCertOrReqSANIPTest(unittest.TestCase):
- """Test for acme.crypto_util._pyopenssl_cert_or_req_san_ip."""
-
- @classmethod
- def _call(cls, loader, name):
- # pylint: disable=protected-access
- from acme.crypto_util import _pyopenssl_cert_or_req_san_ip
- return _pyopenssl_cert_or_req_san_ip(loader(name))
-
- def _call_cert(self, name):
- return self._call(test_util.load_cert, name)
-
- def _call_csr(self, name):
- return self._call(test_util.load_csr, name)
-
- def test_cert_no_sans(self):
- assert self._call_cert('cert.pem') == []
-
- def test_csr_no_sans(self):
- assert self._call_csr('csr-nosans.pem') == []
-
- def test_cert_domain_sans(self):
- assert self._call_cert('cert-san.pem') == []
-
- def test_csr_domain_sans(self):
- assert self._call_csr('csr-san.pem') == []
-
- def test_cert_ip_two_sans(self):
- assert self._call_cert('cert-ipsans.pem') == ['192.0.2.145', '203.0.113.1']
-
- def test_csr_ip_two_sans(self):
- assert self._call_csr('csr-ipsans.pem') == ['192.0.2.145', '203.0.113.1']
-
- def test_csr_ipv6_sans(self):
- assert self._call_csr('csr-ipv6sans.pem') == \
- ['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']
-
- def test_cert_ipv6_sans(self):
- assert self._call_cert('cert-ipv6sans.pem') == \
- ['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']
-
-
-class GenSsCertTest(unittest.TestCase):
- """Test for gen_ss_cert (generation of self-signed cert)."""
-
+class GenMakeSelfSignedCertTest(unittest.TestCase):
+ """Test for make_self_signed_cert."""
def setUp(self):
self.cert_count = 5
self.serial_num: List[int] = []
- self.key = OpenSSL.crypto.PKey()
- self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
+ self.privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)
def test_sn_collisions(self):
- from acme.crypto_util import gen_ss_cert
+ from acme.crypto_util import make_self_signed_cert
for _ in range(self.cert_count):
- cert = gen_ss_cert(self.key, ['dummy'], force_san=True,
+ cert = make_self_signed_cert(self.privkey, ['dummy'], force_san=True,
ips=[ipaddress.ip_address("10.10.10.10")])
- self.serial_num.append(cert.get_serial_number())
+ self.serial_num.append(cert.serial_number)
assert len(set(self.serial_num)) >= self.cert_count
+ def test_no_ips(self):
+ from acme.crypto_util import make_self_signed_cert
+ cert = make_self_signed_cert(self.privkey, ['dummy'])
+
+ @mock.patch("acme.crypto_util._now")
+ def test_expiry_times(self, mock_now):
+ from acme.crypto_util import make_self_signed_cert
+ from datetime import datetime, timedelta, timezone
+ not_before = 1736200830
+ validity = 100
+
+ not_before_dt = datetime.fromtimestamp(not_before)
+ validity_td = timedelta(validity)
+ not_after_dt = not_before_dt + validity_td
+ cert = make_self_signed_cert(
+ self.privkey,
+ ['dummy'],
+ not_before=not_before_dt,
+ validity=validity_td,
+ )
+ # TODO: This should be `not_valid_before_utc` once we raise the minimum
+ # cryptography version.
+ # https://github.com/certbot/certbot/issues/10105
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ 'ignore',
+ message='Properties that return.*datetime object'
+ )
+ self.assertEqual(cert.not_valid_before, not_before_dt)
+ self.assertEqual(cert.not_valid_after, not_after_dt)
+
+ now = not_before + 1
+ now_dt = datetime.fromtimestamp(now)
+ mock_now.return_value = now_dt.replace(tzinfo=timezone.utc)
+ valid_after_now_dt = now_dt + validity_td
+ cert = make_self_signed_cert(
+ self.privkey,
+ ['dummy'],
+ validity=validity_td,
+ )
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ 'ignore',
+ message='Properties that return.*datetime object'
+ )
+ self.assertEqual(cert.not_valid_before, now_dt)
+ self.assertEqual(cert.not_valid_after, valid_after_now_dt)
def test_no_name(self):
- from acme.crypto_util import gen_ss_cert
+ from acme.crypto_util import make_self_signed_cert
with pytest.raises(AssertionError):
- gen_ss_cert(self.key, ips=[ipaddress.ip_address("1.1.1.1")])
- gen_ss_cert(self.key)
+ make_self_signed_cert(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")])
+ make_self_signed_cert(self.privkey)
+
+ def test_extensions(self):
+ from acme.crypto_util import make_self_signed_cert
+ extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request])
+ extension = x509.Extension(
+ x509.TLSFeature.oid,
+ False,
+ extension_type
+ )
+ cert = make_self_signed_cert(
+ self.privkey,
+ ips=[ipaddress.ip_address("1.1.1.1")],
+ extensions=[extension]
+ )
+ self.assertIn(extension, cert.extensions)
class MakeCSRTest(unittest.TestCase):
@@ -250,106 +268,74 @@
@classmethod
def _call_with_key(cls, *args, **kwargs):
- privkey = OpenSSL.crypto.PKey()
- privkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
- privkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey)
+ privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ privkey_pem = privkey.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ )
from acme.crypto_util import make_csr
+
return make_csr(privkey_pem, *args, **kwargs)
def test_make_csr(self):
csr_pem = self._call_with_key(["a.example", "b.example"])
- assert b'--BEGIN CERTIFICATE REQUEST--' in csr_pem
- assert b'--END CERTIFICATE REQUEST--' in csr_pem
- csr = OpenSSL.crypto.load_certificate_request(
- OpenSSL.crypto.FILETYPE_PEM, csr_pem)
- # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
- # have a get_extensions() method, so we skip this test if the method
- # isn't available.
- if hasattr(csr, 'get_extensions'):
- assert len(csr.get_extensions()) == 1
- assert csr.get_extensions()[0].get_data() == \
- OpenSSL.crypto.X509Extension(
- b'subjectAltName',
- critical=False,
- value=b'DNS:a.example, DNS:b.example',
- ).get_data()
+ assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem
+ assert b"--END CERTIFICATE REQUEST--" in csr_pem
+ csr = x509.load_pem_x509_csr(csr_pem)
+
+ assert len(csr.extensions) == 1
+ assert list(
+ csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
+ ) == [
+ x509.DNSName("a.example"),
+ x509.DNSName("b.example"),
+ ]
def test_make_csr_ip(self):
- csr_pem = self._call_with_key(["a.example"], False, [ipaddress.ip_address('127.0.0.1'), ipaddress.ip_address('::1')])
- assert b'--BEGIN CERTIFICATE REQUEST--' in csr_pem
- assert b'--END CERTIFICATE REQUEST--' in csr_pem
- csr = OpenSSL.crypto.load_certificate_request(
- OpenSSL.crypto.FILETYPE_PEM, csr_pem)
- # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
- # have a get_extensions() method, so we skip this test if the method
- # isn't available.
- if hasattr(csr, 'get_extensions'):
- assert len(csr.get_extensions()) == 1
- assert csr.get_extensions()[0].get_data() == \
- OpenSSL.crypto.X509Extension(
- b'subjectAltName',
- critical=False,
- value=b'DNS:a.example, IP:127.0.0.1, IP:::1',
- ).get_data()
- # for IP san it's actually need to be octet-string,
- # but somewhere downstream thankfully handle it for us
+ csr_pem = self._call_with_key(
+ ["a.example"],
+ False,
+ [ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")],
+ )
+ assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem
+ assert b"--END CERTIFICATE REQUEST--" in csr_pem
+
+ csr = x509.load_pem_x509_csr(csr_pem)
+
+ assert len(csr.extensions) == 1
+ assert list(
+ csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
+ ) == [
+ x509.DNSName("a.example"),
+ x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
+ x509.IPAddress(ipaddress.ip_address("::1")),
+ ]
def test_make_csr_must_staple(self):
csr_pem = self._call_with_key(["a.example"], must_staple=True)
- csr = OpenSSL.crypto.load_certificate_request(
- OpenSSL.crypto.FILETYPE_PEM, csr_pem)
+ csr = x509.load_pem_x509_csr(csr_pem)
- # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
- # have a get_extensions() method, so we skip this test if the method
- # isn't available.
- if hasattr(csr, 'get_extensions'):
- assert len(csr.get_extensions()) == 2
- # NOTE: Ideally we would filter by the TLS Feature OID, but
- # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID,
- # and the shortname field is just "UNDEF"
- must_staple_exts = [e for e in csr.get_extensions()
- if e.get_data() == b"0\x03\x02\x01\x05"]
- assert len(must_staple_exts) == 1, \
- "Expected exactly one Must Staple extension"
+ assert len(csr.extensions) == 2
+ assert list(csr.extensions.get_extension_for_class(x509.TLSFeature).value) == [
+ x509.TLSFeatureType.status_request
+ ]
def test_make_csr_without_hostname(self):
with pytest.raises(ValueError):
self._call_with_key()
- def test_make_csr_correct_version(self):
- csr_pem = self._call_with_key(["a.example"])
- csr = OpenSSL.crypto.load_certificate_request(
- OpenSSL.crypto.FILETYPE_PEM, csr_pem)
-
- assert csr.get_version() == 0, \
- "Expected CSR version to be v1 (encoded as 0), per RFC 2986, section 4"
-
-
-class DumpPyopensslChainTest(unittest.TestCase):
- """Test for dump_pyopenssl_chain."""
-
- @classmethod
- def _call(cls, loaded):
- # pylint: disable=protected-access
- from acme.crypto_util import dump_pyopenssl_chain
- return dump_pyopenssl_chain(loaded)
+ def test_make_csr_invalid_key_type(self):
+ privkey = x25519.X25519PrivateKey.generate()
+ privkey_pem = privkey.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ )
+ from acme.crypto_util import make_csr
- def test_dump_pyopenssl_chain(self):
- names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
- loaded = [test_util.load_cert(name) for name in names]
- length = sum(
- len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
- for cert in loaded)
- assert len(self._call(loaded)) == length
-
- def test_dump_pyopenssl_chain_wrapped(self):
- names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
- loaded = [test_util.load_cert(name) for name in names]
- wrap_func = jose.ComparableX509
- wrapped = [wrap_func(cert) for cert in loaded]
- dump_func = OpenSSL.crypto.dump_certificate
- length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded)
- assert len(self._call(wrapped)) == length
+ with pytest.raises(ValueError):
+ make_csr(privkey_pem, ["a.example"])
if __name__ == '__main__':
diff -Nru python-acme-2.11.0/acme/_internal/tests/jose_test.py python-acme-4.0.0/acme/_internal/tests/jose_test.py
--- python-acme-2.11.0/acme/_internal/tests/jose_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/jose_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,7 +1,6 @@
"""Tests for acme.jose shim."""
import importlib
import sys
-import unittest
import pytest
diff -Nru python-acme-2.11.0/acme/_internal/tests/jws_test.py python-acme-4.0.0/acme/_internal/tests/jws_test.py
--- python-acme-2.11.0/acme/_internal/tests/jws_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/jws_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -21,7 +21,7 @@
except (ValueError, TypeError):
assert True
else:
- assert False # pragma: no cover
+ pytest.fail("Exception from jose.b64decode wasn't raised") # pragma: no cover
def test_nonce_decoder(self):
from acme.jws import Header
diff -Nru python-acme-2.11.0/acme/_internal/tests/messages_test.py python-acme-4.0.0/acme/_internal/tests/messages_test.py
--- python-acme-2.11.0/acme/_internal/tests/messages_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/messages_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,10 +1,8 @@
"""Tests for acme.messages."""
-import contextlib
import sys
from typing import Dict
import unittest
from unittest import mock
-import warnings
import josepy as jose
import pytest
@@ -12,8 +10,8 @@
from acme import challenges
from acme._internal.tests import test_util
-CERT = test_util.load_comparable_cert('cert.der')
-CSR = test_util.load_comparable_csr('csr.der')
+CERT = test_util.load_cert('cert.der')
+CSR = test_util.load_csr('csr.der')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
@@ -162,6 +160,10 @@
terms_of_service='https://example.com/acme/terms',
website='https://www.example.com/',
caa_identities=['example.com'],
+ profiles={
+ "example": "some profile",
+ "other example": "a different profile"
+ }
),
})
@@ -191,6 +193,10 @@
'termsOfService': 'https://example.com/acme/terms',
'website': 'https://www.example.com/',
'caaIdentities': ['example.com'],
+ 'profiles': {
+ 'example': 'some profile',
+ 'other example': 'a different profile'
+ }
},
}
@@ -528,14 +534,25 @@
def setUp(self):
from acme.messages import NewOrder
- self.reg = NewOrder(
+ self.order = NewOrder(
identifiers=mock.sentinel.identifiers)
def test_to_partial_json(self):
- assert self.reg.to_json() == {
+ assert self.order.to_json() == {
'identifiers': mock.sentinel.identifiers,
}
+ def test_default_profile_empty(self):
+ assert self.order.profile is None
+
+ def test_non_empty_profile(self):
+ from acme.messages import NewOrder
+ order = NewOrder(identifiers=mock.sentinel.identifiers, profile='example')
+ assert order.to_json() == {
+ 'identifiers': mock.sentinel.identifiers,
+ 'profile': 'example',
+ }
+
class JWSPayloadRFC8555Compliant(unittest.TestCase):
"""Test for RFC8555 compliance of JWS generated from resources/challenges"""
diff -Nru python-acme-2.11.0/acme/_internal/tests/standalone_test.py python-acme-4.0.0/acme/_internal/tests/standalone_test.py
--- python-acme-2.11.0/acme/_internal/tests/standalone_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/standalone_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -11,6 +11,8 @@
import josepy as jose
import pytest
import requests
+from cryptography import x509
+from cryptography.hazmat.primitives import serialization
from acme import challenges
from acme import crypto_util
@@ -116,13 +118,13 @@
def setUp(self):
self.certs = {b'localhost': (
- test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
- test_util.load_cert('rsa2048_cert.pem'),
+ serialization.load_pem_private_key(test_util.load_vector('rsa2048_key.pem'), password=None),
+ x509.load_pem_x509_certificate(test_util.load_vector('rsa2048_cert.pem')),
)}
# Use different certificate for challenge.
self.challenge_certs = {b'localhost': (
- test_util.load_pyopenssl_private_key('rsa4096_key.pem'),
- test_util.load_cert('rsa4096_cert.pem'),
+ serialization.load_pem_private_key(test_util.load_vector('rsa4096_key.pem'), password=None),
+ x509.load_pem_x509_certificate(test_util.load_vector('rsa4096_cert.pem')),
)}
from acme.standalone import TLSALPN01Server
self.server = TLSALPN01Server(("localhost", 0), certs=self.certs,
@@ -142,8 +144,8 @@
# cert = crypto_util.probe_sni(
# b'localhost', host=host, port=port, timeout=1)
# # Expect normal cert when connecting without ALPN.
- # self.assertEqual(jose.ComparableX509(cert),
- # jose.ComparableX509(self.certs[b'localhost'][1]))
+ # self.assertEqual(cert,
+ # self.certs[b'localhost'][1])
def test_challenge_certs(self):
host, port = self.server.socket.getsockname()[:2]
@@ -151,8 +153,7 @@
b'localhost', host=host, port=port, timeout=1,
alpn_protocols=[b"acme-tls/1"])
# Expect challenge cert when connecting with ALPN.
- assert jose.ComparableX509(cert) == \
- jose.ComparableX509(self.challenge_certs[b'localhost'][1])
+ assert cert == self.challenge_certs[b'localhost'][1]
def test_bad_alpn(self):
host, port = self.server.socket.getsockname()[:2]
@@ -193,7 +194,7 @@
from acme.standalone import BaseDualNetworkedServers
- mock_bind.side_effect = socket.error(EADDRINUSE, "Fake addr in use error")
+ mock_bind.side_effect = OSError(EADDRINUSE, "Fake addr in use error")
with pytest.raises(socket.error) as exc_info:
BaseDualNetworkedServers(
diff -Nru python-acme-2.11.0/acme/_internal/tests/testdata/csr-mixed.pem python-acme-4.0.0/acme/_internal/tests/testdata/csr-mixed.pem
--- python-acme-2.11.0/acme/_internal/tests/testdata/csr-mixed.pem 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/testdata/csr-mixed.pem 2025-04-07 18:03:33.000000000 -0400
@@ -1,16 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
-MIICdjCCAV4CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXq
-v1y8EIcCbaUIzCtOcLkLS0MJ35oS+6DmV5WB1A0cIk6YrjsHIsY2lwMm13BWIvmw
-tY+Y6n0rr7eViNx5ZRGHpHEI/TL3Neb+VefTydL5CgvK3dd4ex2kSbTaed3fmpOx
-qMajEduwNcZPCcmoEXPkfrCP8w2vKQUkQ+JRPcdX1nTuzticeRP5B7YCmJsmxkEh
-Y0tzzZ+NIRDARoYNofefY86h3e5q66gtJxccNchmIM3YQahhg5n3Xoo8hGfM/TIc
-R7ncCBCLO6vtqo0QFva/NQODrgOmOsmgvqPkUWQFdZfWM8yIaU826dktx0CPB78t
-TudnJ1rBRvGsjHMsZikCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw
-FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAdGMcRCxq
-1X09gn1TNdMt64XUv+wdJCKDaJ+AgyIJj7QvVw8H5k7dOnxS4I+a/yo4jE+LDl2/
-AuHcBLFEI4ddewdJSMrTNZjuRYuOdr3KP7fL7MffICSBi45vw5EOXg0tnjJCEiKu
-6gcJgbLSP5JMMd7Haf33Q/VWsmHofR3VwOMdrnakwAU3Ff5WTuXTNVhL1kT/uLFX
-yW1ru6BF4unwNqSR2UeulljpNfRBsiN4zJK11W6n9KT0NkBr9zY5WCM4sW7i8k9V
-TeypWGo3jBKzYAGeuxZsB97U77jZ2lrGdBLZKfbcjnTeRVqCvCRrui4El7UGYFmj
-7s6OJyWx5DSV8w==
+MIICdjCCAV4CAQAwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANoV
+T1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svNPSa+oPTK
+7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm7Gj6m2Ez
+pSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFnxvvOjBYo
+p7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTDg7P4UAuF
+kejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1RAeEeRTk
+h0WjUfltoem/5f8bIdsCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw
+FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAQ7n/hYen
+5INHlcslHPYCQ/BAbX6Ou+Y8hUu8puWNVpE2OM95L2C87jbWwTmCRnkFBwtyoNqo
+j3DXVW2RYv8y/exq7V6Y5LtpHTgwfugINJ3XlcVzA4Vnf1xqOxv3kwejkq74RuXn
+xd5N28srgiFqb0e4tOAWVI8Tw27bgBqjoXl0QDFPZpctqUia5bcDJ9WzNSM7VaO1
+CBNGHBRz+zL8sqoqJA4HV58tjcgzl+1RtGM+iUHxXpnH+aCNKWIUINrAzIm4Sm00
+93RJjhb1kdNR0BC7ikWVbAWaVviHdvATK/RfpmhWDqfEaNgBpvT91GnkhpzctSFD
+ro0yCUUXXrIr0w==
-----END CERTIFICATE REQUEST-----
diff -Nru python-acme-2.11.0/acme/_internal/tests/test_util.py python-acme-4.0.0/acme/_internal/tests/test_util.py
--- python-acme-2.11.0/acme/_internal/tests/test_util.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/test_util.py 2025-04-07 18:03:33.000000000 -0400
@@ -3,59 +3,55 @@
.. warning:: This module is not part of the public API.
"""
+import importlib.resources
import os
-import sys
+from typing import Callable
+from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
from josepy.util import ComparableECKey
from OpenSSL import crypto
-if sys.version_info >= (3, 9): # pragma: no cover
- import importlib.resources as importlib_resources
-else: # pragma: no cover
- import importlib_resources
-
def load_vector(*names):
"""Load contents of a test vector."""
# luckily, resource_string opens file in binary mode
- vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names)
+ vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)
return vector_ref.read_bytes()
-def _guess_loader(filename, loader_pem, loader_der):
+def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable:
_, ext = os.path.splitext(filename)
- if ext.lower() == '.pem':
+ if ext.lower() == ".pem":
return loader_pem
- elif ext.lower() == '.der':
+ elif ext.lower() == ".der":
return loader_der
- raise ValueError("Loader could not be recognized based on extension") # pragma: no cover
+ else: # pragma: no cover
+ raise ValueError("Loader could not be recognized based on extension")
-def load_cert(*names):
- """Load certificate."""
- loader = _guess_loader(
- names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
- return crypto.load_certificate(loader, load_vector(*names))
-
-
-def load_comparable_cert(*names):
- """Load ComparableX509 cert."""
- return jose.ComparableX509(load_cert(*names))
+def _guess_pyopenssl_loader(filename: str, loader_pem: int, loader_der: int) -> int:
+ _, ext = os.path.splitext(filename)
+ if ext.lower() == ".pem":
+ return loader_pem
+ else: # pragma: no cover
+ raise ValueError("Loader could not be recognized based on extension")
-def load_csr(*names):
- """Load certificate request."""
+def load_cert(*names: str) -> x509.Certificate:
+ """Load certificate."""
loader = _guess_loader(
- names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
- return crypto.load_certificate_request(loader, load_vector(*names))
+ names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate
+ )
+ return loader(load_vector(*names))
-def load_comparable_csr(*names):
- """Load ComparableX509 certificate request."""
- return jose.ComparableX509(load_csr(*names))
+def load_csr(*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_rsa_private_key(*names):
@@ -76,6 +72,6 @@
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
- loader = _guess_loader(
+ loader = _guess_pyopenssl_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_privatekey(loader, load_vector(*names))
diff -Nru python-acme-2.11.0/acme/_internal/tests/util_test.py python-acme-4.0.0/acme/_internal/tests/util_test.py
--- python-acme-2.11.0/acme/_internal/tests/util_test.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/_internal/tests/util_test.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,5 @@
"""Tests for acme.util."""
import sys
-import unittest
import pytest
diff -Nru python-acme-2.11.0/acme/messages.py python-acme-4.0.0/acme/messages.py
--- python-acme-2.11.0/acme/messages.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/messages.py 2025-04-07 18:03:33.000000000 -0400
@@ -13,6 +13,8 @@
from typing import Type
from typing import TypeVar
+from cryptography import x509
+
import josepy as jose
from acme import challenges
@@ -231,6 +233,7 @@
website: str = jose.field('website', omitempty=True)
caa_identities: List[str] = jose.field('caaIdentities', omitempty=True)
external_account_required: bool = jose.field('externalAccountRequired', omitempty=True)
+ profiles: Dict[str, str] = jose.field('profiles', omitempty=True)
def __init__(self, **kwargs: Any) -> None:
kwargs = {self._internal_name(k): v for k, v in kwargs.items()}
@@ -578,18 +581,17 @@
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME newOrder request.
- :ivar jose.ComparableX509 csr:
- `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
+ :ivar x509.CertificateSigningRequest csr: `x509.CertificateSigningRequest`
"""
- csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
+ csr: x509.CertificateSigningRequest = jose.field(
+ 'csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
class CertificateResource(ResourceWithURI):
"""Certificate Resource.
- :ivar josepy.util.ComparableX509 body:
- `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
+ :ivar x509.Certificate body: `x509.Certificate`
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
@@ -601,11 +603,10 @@
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
- :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
- `jose.ComparableX509`
+ :ivar x509.Certificate certificate: `x509.Certificate`
"""
- certificate: jose.ComparableX509 = jose.field(
+ certificate: x509.Certificate = jose.field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason: int = jose.field('reason')
@@ -613,6 +614,8 @@
class Order(ResourceBody):
"""Order Resource Body.
+ :ivar profile: The profile to request.
+ :vartype profile: str
:ivar identifiers: List of identifiers for the certificate.
:vartype identifiers: `list` of `.Identifier`
:ivar acme.messages.Status status:
@@ -624,6 +627,8 @@
:ivar datetime.datetime expires: When the order expires.
:ivar ~.Error error: Any error that occurred during finalization, if applicable.
"""
+ # https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
+ profile: str = jose.field('profile', omitempty=True)
identifiers: List[Identifier] = jose.field('identifiers', omitempty=True)
status: Status = jose.field('status', decoder=Status.from_json, omitempty=True)
authorizations: List[str] = jose.field('authorizations', omitempty=True)
diff -Nru python-acme-2.11.0/acme/standalone.py python-acme-4.0.0/acme/standalone.py
--- python-acme-2.11.0/acme/standalone.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/acme/standalone.py 2025-04-07 18:03:33.000000000 -0400
@@ -16,7 +16,6 @@
from typing import Tuple
from typing import Type
-from OpenSSL import crypto
from OpenSSL import SSL
from acme import challenges
@@ -46,7 +45,7 @@
method=self.method))
def _cert_selection(self, connection: SSL.Connection
- ) -> Optional[Tuple[crypto.PKey, crypto.X509]]: # pragma: no cover
+ ) -> Optional[crypto_util._KeyAndCert]: # pragma: no cover
"""Callback selecting certificate for connection."""
server_name = connection.get_servername()
if server_name:
@@ -98,7 +97,7 @@
logger.debug(
"Successfully bound to %s:%s using %s", new_address[0],
new_address[1], "IPv6" if ip_version else "IPv4")
- except socket.error as e:
+ except OSError as e:
last_socket_err = e
if self.servers:
# Already bound using IPv6.
@@ -121,7 +120,7 @@
if last_socket_err:
raise last_socket_err
else: # pragma: no cover
- raise socket.error("Could not bind to IPv4 or IPv6.")
+ raise OSError("Could not bind to IPv4 or IPv6.")
def serve_forever(self) -> None:
"""Wraps socketserver.TCPServer.serve_forever"""
@@ -152,8 +151,8 @@
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
def __init__(self, server_address: Tuple[str, int],
- certs: List[Tuple[crypto.PKey, crypto.X509]],
- challenge_certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]],
+ certs: List[crypto_util._KeyAndCert],
+ challenge_certs: Mapping[bytes, crypto_util._KeyAndCert],
ipv6: bool = False) -> None:
# We don't need to implement a request handler here because the work
# (including logging) is being done by wrapped socket set up in the
@@ -163,8 +162,7 @@
ipv6=ipv6)
self.challenge_certs = challenge_certs
- def _cert_selection(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey,
- crypto.X509]]:
+ def _cert_selection(self, connection: SSL.Connection) -> Optional[crypto_util._KeyAndCert]:
# TODO: We would like to serve challenge cert only if asked for it via
# ALPN. To do this, we need to retrieve the list of protos from client
# hello, but this is currently impossible with openssl [0], and ALPN
diff -Nru python-acme-2.11.0/acme.egg-info/PKG-INFO python-acme-4.0.0/acme.egg-info/PKG-INFO
--- python-acme-2.11.0/acme.egg-info/PKG-INFO 2024-06-05 17:34:04.000000000 -0400
+++ python-acme-4.0.0/acme.egg-info/PKG-INFO 2025-04-07 18:03:34.000000000 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: acme
-Version: 2.11.0
+Version: 4.0.0
Summary: ACME protocol implementation in Python
Home-page: https://github.com/certbot/certbot
Author: Certbot Project
@@ -11,27 +11,35 @@
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
-Requires-Python: >=3.8
+Requires-Python: >=3.9
License-File: LICENSE.txt
-Requires-Dist: cryptography>=3.2.1
-Requires-Dist: josepy>=1.13.0
-Requires-Dist: PyOpenSSL!=23.1.0,>=17.5.0
+Requires-Dist: cryptography>=43.0.0
+Requires-Dist: josepy>=2.0.0
+Requires-Dist: PyOpenSSL>=25.0.0
Requires-Dist: pyrfc3339
Requires-Dist: pytz>=2019.3
Requires-Dist: requests>=2.20.0
-Requires-Dist: setuptools>=41.6.0
Provides-Extra: docs
Requires-Dist: Sphinx>=1.0; extra == "docs"
Requires-Dist: sphinx_rtd_theme; extra == "docs"
Provides-Extra: test
-Requires-Dist: importlib_resources>=1.3.1; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-xdist; extra == "test"
Requires-Dist: typing-extensions; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
diff -Nru python-acme-2.11.0/acme.egg-info/requires.txt python-acme-4.0.0/acme.egg-info/requires.txt
--- python-acme-2.11.0/acme.egg-info/requires.txt 2024-06-05 17:34:04.000000000 -0400
+++ python-acme-4.0.0/acme.egg-info/requires.txt 2025-04-07 18:03:34.000000000 -0400
@@ -1,17 +1,15 @@
-cryptography>=3.2.1
-josepy>=1.13.0
-PyOpenSSL!=23.1.0,>=17.5.0
+cryptography>=43.0.0
+josepy>=2.0.0
+PyOpenSSL>=25.0.0
pyrfc3339
pytz>=2019.3
requests>=2.20.0
-setuptools>=41.6.0
[docs]
Sphinx>=1.0
sphinx_rtd_theme
[test]
-importlib_resources>=1.3.1
pytest
pytest-xdist
typing-extensions
diff -Nru python-acme-2.11.0/acme.egg-info/SOURCES.txt python-acme-4.0.0/acme.egg-info/SOURCES.txt
--- python-acme-2.11.0/acme.egg-info/SOURCES.txt 2024-06-05 17:34:04.000000000 -0400
+++ python-acme-4.0.0/acme.egg-info/SOURCES.txt 2025-04-07 18:03:34.000000000 -0400
@@ -73,10 +73,13 @@
docs/_templates/.gitignore
docs/api/challenges.rst
docs/api/client.rst
+docs/api/crypto_util.rst
docs/api/errors.rst
docs/api/fields.rst
docs/api/jose.rst
+docs/api/jws.rst
docs/api/messages.rst
docs/api/standalone.rst
+docs/api/util.rst
docs/man/jws.rst
examples/http01_example.py
\ No newline at end of file
diff -Nru python-acme-2.11.0/debian/changelog python-acme-4.0.0/debian/changelog
--- python-acme-2.11.0/debian/changelog 2024-08-31 21:21:36.000000000 -0400
+++ python-acme-4.0.0/debian/changelog 2025-05-24 15:21:02.000000000 -0400
@@ -1,3 +1,10 @@
+python-acme (4.0.0-1) unstable; urgency=medium
+
+ * New upstream version 4.0.0 (Closes: #1083592, #1106464)
+ * Bump dependencies required by upstream.
+
+ -- Harlan Lieberman-Berg <hlieberman@debian.org> Sat, 24 May 2025 15:21:02 -0400
+
python-acme (2.11.0-1) unstable; urgency=medium
[ Debian Janitor ]
diff -Nru python-acme-2.11.0/debian/control python-acme-4.0.0/debian/control
--- python-acme-2.11.0/debian/control 2024-08-31 16:19:13.000000000 -0400
+++ python-acme-4.0.0/debian/control 2025-05-24 15:18:25.000000000 -0400
@@ -7,15 +7,15 @@
Build-Depends: debhelper-compat (= 13),
dh-python,
python3,
- python3-cryptography,
+ python3-cryptography (>= 43.0.0~),
python3-docutils,
- python3-josepy (>= 1.13.0~),
+ python3-josepy (>= 2.0.0~),
python3-ndg-httpsclient,
- python3-openssl,
+ python3-openssl (>= 25.0.0~),
python3-pytest,
python3-requests,
python3-rfc3339,
- python3-setuptools (>= 41.6~),
+ python3-setuptools,
python3-sphinx,
python3-sphinx-rtd-theme,
python3-tz (>= 2019.3~)
diff -Nru python-acme-2.11.0/docs/api/crypto_util.rst python-acme-4.0.0/docs/api/crypto_util.rst
--- python-acme-2.11.0/docs/api/crypto_util.rst 1969-12-31 19:00:00.000000000 -0500
+++ python-acme-4.0.0/docs/api/crypto_util.rst 2025-04-07 18:03:33.000000000 -0400
@@ -0,0 +1,5 @@
+Crypto_util
+-----------
+
+.. automodule:: acme.crypto_util
+ :members:
diff -Nru python-acme-2.11.0/docs/api/jws.rst python-acme-4.0.0/docs/api/jws.rst
--- python-acme-2.11.0/docs/api/jws.rst 1969-12-31 19:00:00.000000000 -0500
+++ python-acme-4.0.0/docs/api/jws.rst 2025-04-07 18:03:33.000000000 -0400
@@ -0,0 +1,5 @@
+JWS
+---
+
+.. automodule:: acme.jws
+ :members:
diff -Nru python-acme-2.11.0/docs/api/util.rst python-acme-4.0.0/docs/api/util.rst
--- python-acme-2.11.0/docs/api/util.rst 1969-12-31 19:00:00.000000000 -0500
+++ python-acme-4.0.0/docs/api/util.rst 2025-04-07 18:03:33.000000000 -0400
@@ -0,0 +1,5 @@
+Util
+----
+
+.. automodule:: acme.util
+ :members:
diff -Nru python-acme-2.11.0/docs/jws-help.txt python-acme-4.0.0/docs/jws-help.txt
--- python-acme-2.11.0/docs/jws-help.txt 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/docs/jws-help.txt 2025-04-07 18:03:33.000000000 -0400
@@ -3,6 +3,6 @@
positional arguments:
{sign,verify}
-options:
+optional arguments:
-h, --help show this help message and exit
--compact
diff -Nru python-acme-2.11.0/examples/http01_example.py python-acme-4.0.0/examples/http01_example.py
--- python-acme-2.11.0/examples/http01_example.py 2024-06-05 17:34:02.000000000 -0400
+++ python-acme-4.0.0/examples/http01_example.py 2025-04-07 18:03:33.000000000 -0400
@@ -28,6 +28,7 @@
from contextlib import contextmanager
from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import josepy as jose
import OpenSSL
@@ -68,10 +69,9 @@
"""Create certificate signing request."""
if pkey_pem is None:
# Create private key.
- pkey = OpenSSL.crypto.PKey()
- pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS)
- pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
- pkey)
+ pkey = rsa.generate_private_key(public_exponent=65537, key_size=CERT_PKEY_BITS)
+ pkey_pem = pkey.public_bytes(serialization.Encoding.PEM)
+
csr_pem = crypto_util.make_csr(pkey_pem, [domain_name])
return pkey_pem, csr_pem
@@ -200,9 +200,7 @@
# Revoke certificate
- fullchain_com = jose.ComparableX509(
- OpenSSL.crypto.load_certificate(
- OpenSSL.crypto.FILETYPE_PEM, fullchain_pem))
+ fullchain_com = x509.load_pem_x509_certificate(fullchain_pem)
try:
client_acme.revoke(fullchain_com, 0) # revocation reason = 0
diff -Nru python-acme-2.11.0/PKG-INFO python-acme-4.0.0/PKG-INFO
--- python-acme-2.11.0/PKG-INFO 2024-06-05 17:34:04.477813500 -0400
+++ python-acme-4.0.0/PKG-INFO 2025-04-07 18:03:34.974550000 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: acme
-Version: 2.11.0
+Version: 4.0.0
Summary: ACME protocol implementation in Python
Home-page: https://github.com/certbot/certbot
Author: Certbot Project
@@ -11,27 +11,35 @@
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
-Requires-Python: >=3.8
+Requires-Python: >=3.9
License-File: LICENSE.txt
-Requires-Dist: cryptography>=3.2.1
-Requires-Dist: josepy>=1.13.0
-Requires-Dist: PyOpenSSL!=23.1.0,>=17.5.0
+Requires-Dist: cryptography>=43.0.0
+Requires-Dist: josepy>=2.0.0
+Requires-Dist: PyOpenSSL>=25.0.0
Requires-Dist: pyrfc3339
Requires-Dist: pytz>=2019.3
Requires-Dist: requests>=2.20.0
-Requires-Dist: setuptools>=41.6.0
Provides-Extra: docs
Requires-Dist: Sphinx>=1.0; extra == "docs"
Requires-Dist: sphinx_rtd_theme; extra == "docs"
Provides-Extra: test
-Requires-Dist: importlib_resources>=1.3.1; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-xdist; extra == "test"
Requires-Dist: typing-extensions; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
diff -Nru python-acme-2.11.0/setup.py python-acme-4.0.0/setup.py
--- python-acme-2.11.0/setup.py 2024-06-05 17:34:03.000000000 -0400
+++ python-acme-4.0.0/setup.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,19 +1,17 @@
-import sys
-
from setuptools import find_packages
from setuptools import setup
-version = '2.11.0'
+version = '4.0.0'
install_requires = [
- 'cryptography>=3.2.1',
- 'josepy>=1.13.0',
- # pyOpenSSL 23.1.0 is a bad release: https://github.com/pyca/pyopenssl/issues/1199
- 'PyOpenSSL>=17.5.0,!=23.1.0',
+ 'cryptography>=43.0.0',
+ 'josepy>=2.0.0',
+ # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be
+ # relaxed to >=24.0.0 if needed.
+ 'PyOpenSSL>=25.0.0',
'pyrfc3339',
'pytz>=2019.3',
'requests>=2.20.0',
- 'setuptools>=41.6.0',
]
docs_extras = [
@@ -22,15 +20,6 @@
]
test_extras = [
- # In theory we could scope importlib_resources to env marker 'python_version<"3.9"'. But this
- # makes the pinning mechanism emit warnings when running `poetry lock` because in the corner
- # case of an extra dependency with env marker coming from a setup.py file, it generate the
- # invalid requirement 'importlib_resource>=1.3.1;python<=3.9;extra=="test"'.
- # To fix the issue, we do not pass the env marker. This is fine because:
- # - importlib_resources can be applied to any Python version,
- # - this is a "test" extra dependency for limited audience,
- # - it does not change anything at the end for the generated requirement files.
- 'importlib_resources>=1.3.1',
'pytest',
'pytest-xdist',
'typing-extensions',
@@ -44,18 +33,18 @@
author="Certbot Project",
author_email='certbot-dev@eff.org',
license='Apache License 2.0',
- python_requires='>=3.8',
+ python_requires='>=3.9',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
--- End Message ---