The main determining factor for whether a platform can validate Let’s Encrypt certificates is whether that platform includes IdenTrust’s DST Root X3 certificate in its trust store.
Could someone at Let's Encrypt please update that page to indicate what clients will and will not be compatible with Let's Encrypt once the transition to ISRG's roots is complete? The existing compatibility information for IdenTrust's roots should also be retained until such a time as using those roots is no longer an option.
Since the both intermediate certificate are using the same private key, machines that does not support new ISRG signed certificate should still be able to pull the DST one (from a alt-link inside the ISRG certificate) and continue to trust it until 2021 (when the intermediate certificate expires OR the DST root certificate expries).
Correct. Having up-to-date information on what systems are supported by what roots would be helpful in deciding whether or not that step is necessary for any particular site.
There is a mechanism in some browsers that will automate alternative trust path building (so that if a browser caches and trusts the DST intermediate, it can still use it even if the site suggests the ISRG intermediate), although I haven't heard of the "alt-link" that you suggest. Currently the ISRG intermediate cert doesn't mention or refer to the DST intermediate in any way:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
d3:b1:72:26:34:23:32:dc:f4:05:28:51:2a:ec:9c:6a
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=Internet Security Research Group, CN=ISRG Root X1
Validity
Not Before: Oct 6 15:43:55 2016 GMT
Not After : Oct 6 15:43:55 2021 GMT
Subject: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus: [...]
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0
X509v3 Certificate Policies:
Policy: 2.23.140.1.2.1
Policy: 1.3.6.1.4.1.44947.1.1.1
CPS: http://cps.root-x1.letsencrypt.org
X509v3 Subject Key Identifier:
A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl.root-x1.letsencrypt.org
Authority Information Access:
OCSP - URI:http://ocsp.root-x1.letsencrypt.org/
CA Issuers - URI:http://cert.root-x1.letsencrypt.org/
X509v3 Authority Key Identifier:
keyid:79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E
Signature Algorithm: sha256WithRSAEncryption [...]
That’s great! Unfortunately I don’t actually know what devices are and are not supported by the new root, so I wouldn’t be able to make that change myself. Would you like me to file an issue so this doesn’t get lost?
Yep, we will update that page soon. Thanks for the suggestion.
There are two mechanisms:
If a browser has seen the DST Root X3-signed copy of the Let's Encrypt Authority X3 intermediate, it will cache it for some amount of time for use in future chain building. This is why sites that don't send a full chain sometimes work and sometimes doesn't. It makes chain issues notoriously hard to debug!
A certificate can contain an "Authority Information Access" section with a "CA Issuers" URL that allows fetching the parent certificate if it's not in the chain. To make effective use of this, we would have to get a second cross-sign, this time on ISRG Root X1, instead of on Let's Encrypt Intermediate X3, and provide that cross-signed root at the AIA CA Issuers URL in our intermediates. That would let browsers that don't have our root in their root store download a cross-signed copy of our root so they could trust it.
Unfortunately AIA CA Issuers is not implemented by Chrome on Android, which is the main source of compatibility issues. And this technique could not extend trust beyond the expiration of DST Root X3 anyhow. So right now we don't plan to pursue that path.
@orangepizza, unfortunately Internet standards don’t currently allow that (although indeed many clients will accept it). I believe there are a few clients that will actively reject it, so it might create a trade-off where compatibility increases in some areas yet decreases in others.
I plan to implement this in our cPanel client for as long as the old chain is valid, since the compatibility situation doesn't look so hot and we don't want to surprise our users.
From what I can tell, there isn't currently any way to find the cross-signed issuer certificate from a leaf certificate.
So at present, I would have to hard-code a substitution of the intermediate, which seems too risky and I can't think of a way to do it that would be future-proof (X2->X3 transition tripped up a lot of client developers). Putting the burden on end-users to configure the chain seems backwards too - ACME clients are often completely non-interactive.
The AIA extension seems to provide the ability to include multiple caIssuers, though Boulder seems to explicitly say it won't do that. If it did, we could choose the most compatible issuer by looking at the certificate itself.
Alternatively, and although it's not strictly what's written in RFC8555, could the server include the alternate issuer in an Link: rel="up" header, when downloading the certificate?
We don’t have any planned implementation yet. The issue was simply assigned for that person to look into the problem. Anyone who is interested can watch the issue for updates.
I ended up implementing this with the approach below, hopefully it can get some eyeballs from smart people to see if there’s any problems. It works on the principle of identifying the X3 issuer by its SKI (which remains the same between ISRG & DST) and always replacing it with the DST-signed one.
let x3_subjectKeyId = "a84a6a63047dddbae6d139b7a64565eff3a8eca1";
let x3_dst_signed = "<contents of https://censys.io/certificates/25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d/pem>";
// Download the fullchain from ACMEv2 /acme/cert/...
let (leaf, issuer) = fullchain;
// If any assertions fail, we don't modify the chain, we just use whatever the ACME server gave us
// We expect a leaf and single issuer, only
assert len(fullchain) == 2;
// AKI of leaf must match SKI of issuer, both must match the SKI of Let's Encrypt X3
assert leaf.authority_key_id == issuer.subject_key_id == x3_subjectKeyId;
// Now that we have established that the issuer is one of the X3 certs (DST or ISRG)
// we can replace it with the DST-signed one.
let new_chain = (leaf, x3_dst_signed);
// The chain should verify both now, but also right upto the expiry of the leaf.
// Otherwise we might substitute an issuer that has shorter validity than the leaf.
let verification_time = max(now(), leaf.not_after - (1 * minute));
// x.509 verification must succeed using the new chain
// (using verification_time and any of the names on the certificate).
// In Go, this checks everything except revocation status. We mainly
// care about it finding a path to a trust anchor.
assert validate_chain(new_chain, leaf.dns_names[0], verification_time);
// Now safe to use new_chain instead of the original fullchain
e: Just realized that X4 has already been cross-signed as well, but since it shares the same validity period as X3, there’s probably not a big chance it’ll ever be deployed. Probably won’t bother covering it.
e2: Changed x.509 verification clock time to cover the bug uncovered by @orangepizza
Edit: sorry, I misread. Yeah, the cross-signed one expires a little earlier, but I suspect that ACME server will have transitioned to X5 by then anyway. Unless X3 is going to get re-signed in perpetuity? IDK.
I think you should use 3 months later minus one hour (when leaf certificate expires). for validate check for certificate.
if we use current time, for example, if we still use X3 in 2021/02/01, then validate check will pass because it’s currently valid, but cert with modified chain will fail before renewal (2021/04/02) because DST intermediate expires at 2021/3/18, shorter then lifetime of leaf certificate. (it ends about 2021/05/04?)
I've mostly followed this thread via email, so I completely missed this edit. This is how the ACME standard proposes that alternate chains can be delivered, letting the client decide whether to take the default one or an alternate one. So I really think that this is what should be implemented! I'm happy that you already tried implementing that for Boulder; have you considered turning this into a proper PR to make this more visible? I'm afraid most people will miss it here. @cpu@jsha any comment on this? Is there a chance we can have this implemented in Boulder when Let's Encrypt switches to the new intermediate, to offer the old intermediate (until it is no longer valid) as an "official alternative"?
I'll also play around with Pebble a bit today, maybe I can implement something similar (and try to add support for that to my client).