• 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.