Bug#1106578: marked as done (unblock: python-acme/4.0.0-1) (2/3)
From
Debian Bug Tracking System@21:1/5 to
All on Mon May 26 19:50:01 2025
[continued from previous message]
+ 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.