How do I verify a Let's Encrypt certificate?

I've run into an issue with the nginxproxy/acme-companion docker image. It obtains certificates with acme.sh. As a result I get:

cert.pem (example.com) + chain.pem (R3 + ISRG Root X1)
  == fullchain.pem

It also provides a tool that among other things verifies the certificates. It does it like so:

$ openssl verify -CAfile chain.pem fullchain.pem

I tried to investigate the issue:

$ openssl crl2pkcs7 -nocrl -certfile /etc/nginx/certs/example.com/fullchain.pem \
| openssl pkcs7 -print_certs -text -noout \
| egrep '(Issuer|Subject):'
        Issuer: C=US, O=Let's Encrypt, CN=R3
        Subject: CN=example.com
        Issuer: C=US, O=Internet Security Research Group, CN=ISRG Root X1
        Subject: C=US, O=Let's Encrypt, CN=R3
        Issuer: O=Digital Signature Trust Co., CN=DST Root CA X3
        Subject: C=US, O=Internet Security Research Group, CN=ISRG Root X1

So I guess the chain is: example.com <- R3 <- ISRG Root X1 <- DST Root CA X3.

Now let's say I split fullchain.pem into 3 files: example.com.pem, R3.pem, and ISRG-Root-X1.pem and try to verify the certificates:

$ openssl verify -CAfile R3.pem example.com.pem
example.com.pem: OK

Succeeds because the ISRG Root X1 certificate is trusted.

$ openssl verify R3.pem
R3.pem: OK

Succeeds for the same reason.

$ openssl verify -CAfile ISRG-Root-X1.pem R3.pem
C = US, O = Internet Security Research Group, CN = ISRG Root X1
error 2 at 1 depth lookup: unable to get issuer certificate
error R3.pem: verification failed

I guess this fails because the issuer of the ISRG-Root-X1.pem certificate is DST Root CA X3, which is not trusted. And this seems like the reason the original command fails:

$ openssl verify -CAfile chain.pem fullchain.pem

Can you suggest a command to verify a certificate that works for both Let's Encrypt and non-Let's Encrypt certificates?

It'd be something like:

openssl verify \
-CAfile /etc/ssl/certs/ca-certificates.crt \
-untrusted chain.pem \
cert.pem

(assuming a Debian-ish environment)

7 Likes

Can you possibly explain what it does? It takes cert.pem, sees that the issuer is R3 which it finds in chain.pem. The R3's issuer is ISRG Root X1, which it finds in /etc/ssl/certs/ca-certificates.crt. That is, it ignores the ISRG Root X1 certificate in chain.pem. And the reason my command fails is because of your ISRG Root X1 certificate (DST Root CA X3 issuer). Is this correct?

Also, it works w/o -CAfile /etc/ssl/certs/ca-certificates.crt (supposedly because ISRG Root X1 is trusted anyways). Does it makes any difference?

The ISRG Root X1 signed-by DST Root CA X3 is only present for compatibility reasons on older Android versions. See Extending Android Device Compatibility for Let's Encrypt Certificates - Let's Encrypt for more info.

4 Likes

It depends on your openssl version. I think some default to the trusted root store, but others may not. I could be wrong about that.

Your interpretation is pretty much right. You can see the command flags in the docs: /docs/man1.1.1/man1/openssl-verify.html

One thing to note is that modern versions of openssl accept multiple -untrusted arguments:

openssl verify -purpose sslserver -CAfile root.pem [[-untrusted intermediate.pem],[-untrusted intermediate.pem],] cert.pem

However older ones only want to see a single -untrusted flag:

openssl verify -purpose sslserver -CAfile root.pem -untrusted intermediate.pem cert.pem

You can also use the following, which is slightly different:

openssl verify -purpose sslserver -partial_chain -trusted {ROOT.pem} {CHAIN.pem}

the -partial_chain allows the the command to succeed if the trusted root is NOT self-signed. This can be useful for verifying up to the intermediate certificates when the ISRG root is not in the trust store.

5 Likes

Actually I've read this post. The thing is, it apparently targets a broad audience, and as such written in a way that is difficult to understand for me (as a Let's Encrypt subscriber).

The way I understand it, or rather the way it more or less starts to makes sense:

  1. Old Androids ignore expiration dates of the certificates located in the trust store.
  2. openssl client is allowed to choose which path to take (R3 -> ISRG Root X1 from the server, R3 -> ISRG Root X1 from the trust store).

Then on old Androids openssl client chooses ISRG Root X1 from the server (because there's no ISRG Root X1 in the trust store), which points to DST Root CA X3 in the trust store. Which expired, but that's acceptable for Androids (see 1).

On other devices openssl client chooses ISRG Root X1 from the trust store (because DST Root CA X3 is not in the trust store or expired).

Also, the post seems to contain discrepancies/typing errors:

However, note that most ACME clients don’t yet have a way to select this alternate chain (for example, Certbot selects chains by looking to see if they contain a given Issuer Name, but this chain doesn’t contain any Issuer Names which the high compatibility chain above doesn’t).

The chain above doesn't?..

It's not necessarily "OpenSSL" on android or any device. There are many clients and libraries that handle this, but OpenSSL is one of the more popular libraries for direct use, or building applications against.

Most of these clients will choose the ISRG root simply because they find it faster. This does not happen with ALL clients and libraries, just newer ones that have implemented a different logic by default. These clients basically ignore the server's instructions to chain up to the DST root, and build their own chain. When they get to a cross-signed certificate from the server, they can safely ignore validating up another level, because that certificate shares the same cryptographic key pairing as the ISRG Root X1 key that is in their trust store.

The blog post is old. As promised in that post, LetsEncrypt worked with Certbot to change the functionality so users could select the appropriate chain. See Providing different alternate chain selection methods · Issue #8577 · certbot/certbot · GitHub

5 Likes