dnsprobe v1

Reading an SSL certificate chain

· ~3 min read · dnsprobe.net/blog

A certificate alone is not a trust statement. It is a signed claim by a CA that a particular public key belongs to a particular name. The trust statement comes from the chain: the leaf is signed by an intermediate, the intermediate is signed by another intermediate or directly by a root, and the root is in the validator's trust store because the OS or browser vendor put it there.

The three layers

For a typical Let's Encrypt cert today:

(leaf)         CN=example.com                signed by R10
(intermediate) CN=R10                        signed by ISRG Root X1
(root)         CN=ISRG Root X1               self-signed, in trust stores

The leaf cert is what your server presents on the wire. The intermediate is included in the chain you send alongside the leaf. The root is not sent — the client already has it. Sending the root is harmless but wastes bytes.

Where the chain comes from

When your ACME client issues a cert (certbot, lego, acme.sh, Caddy's built-in), it gets back two files: the leaf and a chain bundle. Most clients concatenate them into a single fullchain.pem. That is the file you point your web server at as the cert. The private key is separate.

# nginx
ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# apache
SSLCertificateFile      /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile   /etc/letsencrypt/live/example.com/privkey.pem

If you instead point nginx at cert.pem (the leaf only, no intermediate), some browsers will succeed (they have the intermediate cached from a previous site) and others will fail with an "incomplete certificate chain" error. This is the most common chain misconfiguration in the wild.

How the validator walks it

When a client connects:

  1. Server presents the leaf + intermediate(s). Client parses the certificate message.
  2. Client takes the leaf. Reads the Issuer DN. Looks for a cert in the presented chain whose Subject DN matches.
  3. If found, that is the next link. Verify the leaf's signature using the intermediate's public key. Reject on signature mismatch.
  4. Take the intermediate. Repeat: find its issuer in the presented chain.
  5. Eventually you find an issuer that is not in the presented chain. Look it up in the local trust store. If found, you have a chain to a trusted root. If not, the chain is broken.
  6. At each step, also check: cert not expired, cert not revoked (OCSP or CRL), name on the leaf matches the hostname requested.

The three failure modes

  1. Missing intermediate. Server only sends the leaf. Some clients have a cached intermediate from another site; some don't. Symptom: works in Chrome on your laptop, fails in curl on a fresh server. Fix: use fullchain.pem, not cert.pem.
  2. Wrong intermediate. Server sends an intermediate that does not chain to the leaf. Symptom: every client fails with "unable to get local issuer certificate". Fix: rebuild fullchain from the leaf's actual issuer chain.
  3. Expired intermediate. This happened to a chunk of the internet in September 2021 when the Let's Encrypt cross-signed DST Root CA X3 expired. Older Android devices that didn't have the newer ISRG Root X1 in their trust store could not validate, even though every cert in the served chain was technically still in date. Fix: serve the alternate chain that does not cross-sign through the expired root.

Inspecting a chain on the command line

# Pull the full chain a server presents
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null

# Decode each cert
openssl x509 -in cert.pem -noout -text

# Verify a chain locally
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted intermediate.pem leaf.pem

The -servername flag is mandatory for SNI — without it, the server may present a default certificate that has nothing to do with your hostname.

For a visual at-a-glance view of the chain a host serves, the SSL panel on dnscheck shows the leaf details (issuer, subject, SANs, validity, key type, signature algorithm). Try letsencrypt.org for a clean example.

References: RFC 5280 (X.509 PKIX), Let's Encrypt's writeup of the 2021 root expiry.