How can I most easily, when interacting with Let’s Encrypt’s API (not certbot or some other client application), use LE’s alternative trust chain? (The one that doesn’t refer back to the now-expired DST root.)
What works is to fetch the “normal” one then look at the Link headers to find the rel="alternate" link; I wonder if there’s a way I could cut out that “throwaway” initial fetch and just always retrieve the trust chain that doesn’t go back to an expired root?
I don't think there's any way to avoid the initial download of the default cert+chain. No other endpoint I'm aware of provides the URL you'd need from that rel="alternate" header. However, for your particular use-case it's probably also fine to just hard code the alternate chain details in your integration.
Keep in mind that what is currently "default" and what is currently "alternate" may change in the future as the global need for the old-android-compatible chain becomes less. So in that sense, it's another reason you specifically may want to hard-code the chain you care about so it doesn't get switched out from under you unexpectedly later...with the understanding that you (or someone) will need to pay closer attention to things like the API Announcements category for things like the inevitable replacement of R3 with whatever comes next.
The signing intermediate may change from R3 to the backup intermediate R4 at any notice and, more importantly perhaps, without timely notice, breaking any hardcoded chain with it.
Unfortunately, the ACME protocol doesn't have a "please use this alternative chain by default" option. The protocol only leaves room for those Link headers and the rest has to be done client side.
Part of what complicates this is OpenSSL’s inconsistent verification: old/new versions, connection vs. verify, CLI vs. API, and all the varied configuration options.
Does anyone have a way, when using OpenSSL’s API, of reliably failing the verification? I need to build a heuristic that will fail LE’s default cert, ideally without hard-coding the specific intermediates I need to avoid.
The only proper way to do this, right now, is to download and inspect all chains.
Some clients will have a “preferred chain” option, but that must download and inspect the chains to properly work.
IMHO, and I’ve brought this up with LetsEncrypt, the links should contain a fingerprint for each chain — which would allow for a client to discern previously encountered chains without downloading them.
No such features currently exist, and the LetsEncrypt service does not / can not guarantee any particular chain for a request. So the only correct way to handle this is downloading and inspecting each chain until you find your match. Personally, I download all chains.
There is one workaround though - if you have your own client, you can just inspect keypair for the chain (you can pull enough data off the cert too) and maintain a database of compatible key pairs. I.e. R3 old, R3 new, and R3 cross sign all have the same keys, and therefore certs signed by them are interoperable with one another. This still causes issues if a new compatible intermediate is released, but it gives you a decent framework to work with.
@jvanasco Downloading all the chains isn’t a problem; I’m wondering about how to implement the “inspection”. Keeping a “deny-list” of chains seems a decidedly hacky approach.
It seems like OpenSSL 1.1.1 does its best to pass verification with the Android chain. I’d like to configure OpenSSL instead such that it highlights the problem that it goes back to an expired root, rather than ignoring the expired root because there’s another root in the local store that matches one of the chain’s intermediates.
If I understand you’re comment correctly, this may help. If not, I hope I don’t confuse you:
The canonical way to verify a chain is to build a container and verify upwards from the end entity to the root, one certificate at a time. I do not believe that approach can utilize the shortcircuit behavior, because you pass in an explicit root.
I’m away from a computer and can’t test, but you can see the OpenSSL and Python implementations on ensure-chain and ensure-chain-order in this file; this may help.
I’m not sure the order matters in that case; you’re just adding certs to the internal X509_STORE?
This is, I think, similar to what we do: create the X509_STORE, do X509_STORE_load_locations($root_certs_file), then create an X509_STORE_CTX and call X509_STORE_CTX_init($x509_store, $cert, $untrusted_certs).
We could, I suppose, do that verification with 1 intermediates, 2 intermediates, 3, etc. until we either get a success or run out of intermediates. That would be more like a “normalization” of a given chain rather than something we could use to compare two different chains … I guess that’d be OK.
Our temporary fix will be just to download the CA Issuers chain and use that. We cache these, so it’ll be faster than fetching the “alternate” link from the ACME server.
With the “fire” put out we can then look to a more robust solution.