Skip to content
ecdsa.com

Verify a certificate chain · OpenSSL

How to verify a certificate chain against your own trust store with OpenSSL

To verify a certificate chain against your own trust store with OpenSSL, run openssl verify -no-CApath -no-CAstore -CAfile root.pem -untrusted intermediate.pem leaf.pem: -CAfile holds what you trust, -untrusted the intermediates that must chain up to it, and the two -no options keep the system's roots out. This recipe validates a leaf → intermediate → root chain from a test CA and shows the failures for a look-alike root, an expired leaf and a missing intermediate.

Tested with OpenSSL 3.6.4 (Debian forky (buildpack-deps:forky-curl)).

validate leaf → intermediate → root, print the path, check the host name
# -CAfile: what you trust; -untrusted: intermediates that must chain up to it;
# -no-CApath -no-CAstore: do not also trust the system's CA directory and store
openssl verify -no-CApath -no-CAstore -CAfile root.pem -untrusted intermediate.pem leaf.pem
# -show_chain prints the path OpenSSL built; -verify_hostname checks the name too
openssl verify -show_chain -verify_hostname api.example.com -no-CApath -no-CAstore \
-CAfile root.pem -untrusted intermediate.pem leaf.pem
the three failures every chain check must catch
# Each of these must fail: another root with the same name, an expired leaf, a missing intermediate
openssl verify -no-CApath -no-CAstore -CAfile impostor-root.pem -untrusted intermediate.pem leaf.pem
openssl verify -no-CApath -no-CAstore -CAfile root.pem -untrusted intermediate.pem expired.pem
openssl verify -no-CApath -no-CAstore -CAfile root.pem leaf.pem
echo "exit code of the last check: $?"

How it works

  1. -CAfile lists your trust anchors, and -no-CApath -no-CAstore turn off the system's CA directory and store, so nothing else is trusted.
  2. -untrusted supplies intermediates. OpenSSL builds the path through them to an anchor, checks every signature, validity period, CA flag and path length, and prints leaf.pem: OK.
  3. -show_chain prints the path it built, marking certificates that came from -untrusted; -verify_hostname adds the host name check a TLS client would make.
  4. Each failure names the depth and the reason: error 20 at 1 depth lookup: unable to get local issuer certificate for the look-alike root, error 10 at 0 depth lookup: certificate has expired for the expired leaf. The exit code is 2.

Gotchas

  • -CAfile alone does not mean "only this file": OpenSSL still loads the default CA directory. In the run, a public root from /etc/ssl/certs verifies OK against -CAfile root.pem, and fails once -no-CApath -no-CAstore are added.
  • A root with the same name but another key is not a trust anchor for this chain: the intermediate's signature does not verify with it, and OpenSSL reports error 20 at depth 1 — the issuer of the intermediate was not found.
  • An intermediate given as -CAfile is not trusted on its own: verification still fails unless you add -partial_chain, which turns it into an anchor. Use that switch only when you really mean to trust the intermediate.
  • openssl verify checks the first certificate of each file: a bundle of leaf + intermediate passed as the certificate fails, because the intermediate in the same file is ignored. Pass intermediates with -untrusted.
  • Without the intermediate the leaf fails with error 20 at depth 0 — the classic "works in the browser, fails in curl" case: browsers often have the intermediate cached, other clients only see what the server sends.

Related recipes