I downloaded and pinned the ISRG Root X1 certificate in my client application in order to only initiate a TLS connection to my server if the server has a valid certificate from LetsEncrypt, rejecting any other CAs. However the certificate returned by the server doesn't seem to be signed with this root. The chain is:
My domain -> R3 -> IdenTrust DST Root CA X3
Switching out my pinned certificate for the IdenTrust one results in a succesful TLS connection. But from the image at https://letsencrypt.org/certificates/ it looks like the ISRG Root X1 should be signing the R3 certificate as well. I'm not sure if I'm missing something.
One signed by "DST Root CA X3" and one signed by "ISRG Root X1".
For your "ISRG Root X1" pinning to work, your certificate chain should use the latter R3 certificate. By default (until January 11), you will have received the former R3 certificate from Let's Encrypt.
Your client should be able to build a chain to ISRG Root X1 too, indeed, but it would need knowledge of the intermediate certificate signed by that root.
Currently, servers usually send only the intermediate R3 signed by IdenTrusts root DST Root CA X3, so probably your client hasn't cached the R3 signed by ISRG Root X1 or doesn't build a chain to it due to other reasons.
The two intermediates discussed here (R3 signed by ISRG Root X1 and R3 signed by DST Root CA X3) contain the same public key and thus the end leaf certificates are signed by the same private key. You can think of the CommonName R3 from the intermediates as the public/private keypair.
As the end leaf certificates are signed by only a single private key (R3), end leaf certificates can chain to both roots, depending on the intermediate used. Therefore, your end leaf certificates are the same, no matter what chain is being build.
Well, actually meaning "no", as Let's Encrypt doesn't need to issue a specific end leaf certificate when it comes to which root the chain is being build. I don't consider choosing the intermediate as being "issued a X1 variant" to be honest.
Perhaps a little bit "too correct", but when it comes to understanding PKI, root, intermediates, cross-signing, I like to be overly correct just to be sure everything is understood correctly.
Makes sense. I would have to fork the library that I'm using for certificate issuance in order to substitute the intermediary, or replace the intermediary manually in the certificate file on disk, so I will just pin both until January 11th as that's simpler.
Note that you shouldn't hardcode the intermediate. The intermediate might change without any notice, for example, when an incident forces Let's Encrypt to start using the backup intermediate R4 in stead of R3. When you're being issued a certificate from the ACME server, the server sends HTTP Link headers with the "alternate" relation type containing alternate intermediate certificates which could be used too.
You could analyze the intermediates send to you by the ACME server and choose the one you require. For example, you could check the Issuer of the intermediate to identify the signing root certificate and choose by that criterium.