zenhack / simp_le

Simple Let's Encrypt client
GNU General Public License v3.0
224 stars 38 forks source link

Support --preferred-chain like certbot #146

Closed lukecyca closed 3 years ago

lukecyca commented 4 years ago

I had initially opened this on kuba's repo. Cross-posting it here now after being advised this is where simp_le development is happening now.

From nginx-proxy/docker-letsencrypt-nginx-proxy-companion#695:

Let's Encrypt is changing the default chain of trust (details), which has wide implications for backwards compatibility for clients with old CA root stores. Certbot now supports specifying --preferred-chain so that we can manage this transition (details).

The first part of this transition is going to occur in less than a week, or as soon as they receive their cross-signature from IdentTrust for the new "R3" Intermediate. So this is very time sensitive.

Can simp_le support this option?

buchdag commented 4 years ago

@zenhack : the corresponding commit to certbot is https://github.com/certbot/certbot/commit/f743dbec3a04349533735a161b650f02844b2294

The relevant bits are this new function:

def find_chain_with_issuer(fullchains, issuer_cn, warn_on_no_match=False):
    """Chooses the first certificate chain from fullchains which contains an
    Issuer Subject Common Name matching issuer_cn.
    :param fullchains: The list of fullchains in PEM chain format.
    :type fullchains: `list` of `str`
    :param `str` issuer_cn: The exact Subject Common Name to match against any
        issuer in the certificate chain.
    :returns: The best-matching fullchain, PEM-encoded, or the first if none match.
    :rtype: `str`
    """
    for chain in fullchains:
        certs = [x509.load_pem_x509_certificate(cert, default_backend()) \
                 for cert in CERT_PEM_REGEX.findall(chain.encode())]
        # Iterate the fullchain beginning from the leaf. For each certificate encountered,
        # match against Issuer Subject CN.
        for cert in certs:
            cert_issuer_cn = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
            if cert_issuer_cn and cert_issuer_cn[0].value == issuer_cn:
                return chain

    # Nothing matched, return whatever was first in the list.
    if warn_on_no_match:
        logger.info("Certbot has been configured to prefer certificate chains with "
                    "issuer '%s', but no chain from the CA matched this issuer. Using "
                    "the default certificate chain instead.", issuer_cn)
    return fullchains[0]

and the following change to the obtain_certificate_from_csr() function:

    def obtain_certificate_from_csr(self, csr, orderr=None):
        """Obtain certificate.
        :param .util.CSR csr: PEM-encoded Certificate Signing
            Request. The key used to generate this CSR can be different
            than `authkey`.
        :param acme.messages.OrderResource orderr: contains authzrs
        :returns: certificate and chain as PEM byte strings
        :rtype: tuple
        """
        if self.auth_handler is None:
            msg = ("Unable to obtain certificate because authenticator is "
                   "not set.")
            logger.warning(msg)
            raise errors.Error(msg)
        if self.account.regr is None:
            raise errors.Error("Please register with the ACME server first.")
        logger.debug("CSR: %s", csr)
        if orderr is None:
            orderr = self._get_order_and_authorizations(csr.data, best_effort=False)

        deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
-       orderr = self.acme.finalize_order(orderr, deadline)
-       cert, chain = crypto_util.cert_and_chain_from_fullchain(orderr.fullchain_pem)
+       get_alt_chains = self.config.preferred_chain is not None
+       orderr = self.acme.finalize_order(orderr, deadline,
+                                         fetch_alternative_chains=get_alt_chains)
+       fullchain = orderr.fullchain_pem
+       if get_alt_chains and orderr.alternative_fullchains_pem:
+           fullchain = crypto_util.find_chain_with_issuer([fullchain] + \
+                                                          orderr.alternative_fullchains_pem,
+                                                          self.config.preferred_chain,
+                                                          not self.config.dry_run)
+      cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain)
        return cert.encode(), chain.encode()
def obtain_certificate_from_csr(self, csr, orderr=None):
        """Obtain certificate.
        :param .util.CSR csr: PEM-encoded Certificate Signing
            Request. The key used to generate this CSR can be different
            than `authkey`.
        :param acme.messages.OrderResource orderr: contains authzrs
        :returns: certificate and chain as PEM byte strings
        :rtype: tuple
        """
        if self.auth_handler is None:
            msg = ("Unable to obtain certificate because authenticator is "
                   "not set.")
            logger.warning(msg)
            raise errors.Error(msg)
        if self.account.regr is None:
            raise errors.Error("Please register with the ACME server first.")
        logger.debug("CSR: %s", csr)
        if orderr is None:
            orderr = self._get_order_and_authorizations(csr.data, best_effort=False)

        deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
        get_alt_chains = self.config.preferred_chain is not None
        orderr = self.acme.finalize_order(orderr, deadline,
                                          fetch_alternative_chains=get_alt_chains)
        fullchain = orderr.fullchain_pem
        if get_alt_chains and orderr.alternative_fullchains_pem:
            fullchain = crypto_util.find_chain_with_issuer([fullchain] + \
                                                           orderr.alternative_fullchains_pem,
                                                           self.config.preferred_chain,
                                                           not self.config.dry_run)
        cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain)
        return cert.encode(), chain.encode()

plus handling of the new flag.

Everything else is done by the acme module starting with version 1.6.0

zenhack commented 4 years ago

I'll try to tackle this in the next few days.

buchdag commented 4 years ago

The actual deadline is January 11, so there is no rush yet. Let me know if you need help.

zenhack commented 4 years ago

I certainly wouldn't say no, if you wanted to tackle it.

Quoting Nicolas Duchon (2020-09-26 03:04:34)

The actual deadline is January 11, so there is no rush yet. Let me know if you need help.

-- You are receiving this because you were mentioned. Reply to this email directly, [1]view it on GitHub, or [2]unsubscribe.

Verweise

  1. https://github.com/zenhack/simp_le/issues/146#issuecomment-699444749
  2. https://github.com/notifications/unsubscribe-auth/AAGXYPW2NEDF3I7ES6LLXITSHWHAFANCNFSM4RYQBDKQ
buchdag commented 4 years ago

@zenhack sorry for the lack of answer, things shifted a bit since then and letsencrypt-nginx-proxy-companion will be moving away from simp_le soonish. I probably won't have the spare time needed to work on this after all. 😞

zenhack commented 4 years ago

Ok, thanks for letting me know.

milot-mirdita commented 3 years ago

Now that DST Root CA X3 has expired, I am getting certificate failures from an old python 3.4 application (using OpenSSL<1.1.0) requesting data from a let's encrypt signed API I host.

It would be great if I could sign the API endpoint with the alternate chain to give the developers of the old python application more time to migrate.

zenhack commented 3 years ago

I think it is unlikely that I'll find time to work on this any time soon, but I will review patches if someone else is interested in doing the work.

tometzky commented 3 years ago

I've created a pull request for adding a new option (https://github.com/zenhack/simp_le/pull/150):

  --use_alt_chain CHAIN_NO
                        If non-zero, then use alternative certificate chain
                        number. (default: 0)

Then by using --use_alt_chain=1 I was able to renew my Let's Encrypt certificate with the following chain:

Certificate chain
 0 s:CN = …
   i:C = US, O = Let's Encrypt, CN = R3
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

It's not perfect, as it would be better to use a CN instead of a number. But it is very simple change and at least it might allow to temporarily work-around current problems with expired "DST Root CA X3" certificate in the chain. Apparently some buggy TLS implementations will reject the cert even if "ISRG Root X1" is trusted, just because it is signed by an expired cert.

zenhack commented 3 years ago

Closing, since #150 is merged.