Verification and testing
OpenSSL verify: check certificate chains, hostnames and keys
These commands check whether a certificate chains up to a trusted root, matches a hostname, carries the right extended key usage and belongs to a given private key. Everything here uses OpenSSL 3.x (output from 3.6.1); the same options work on 1.1.1 except where noted. In the examples leaf.pem is the server certificate, intermediate.pem its issuing CA and root.pem a root you trust.
Verify a certificate against a specific root
Pass the root with -CAfile and every intermediate with -untrusted; that is how a TLS client sees it.
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
leaf.pem: OK
-CAfile adds trust anchors. -untrusted only supplies building blocks that must themselves chain up to an anchor. Several intermediates can go into one -untrusted file. If you instead put the intermediate into -CAfile, OpenSSL stops there and reports error 2 at 1 depth lookup: unable to get issuer certificate unless you also add -partial_chain, which tells it to accept a non-self-signed trust anchor.
Reproduce the missing-intermediate error
Leaving out the intermediate gives error 20 at depth 0, the same failure clients see when a server sends only its leaf certificate.
openssl verify -CAfile root.pem leaf.pem
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.pem: verification failed
Read the error as "at depth 0 (the leaf) I could not find the certificate that signed it". Depth 1 is the intermediate, depth 2 the root. The fix is server-side: serve the full chain (see missing intermediate certificate), or build it with Chain Builder.
Print the chain OpenSSL built
-show_chain lists every certificate in the verified path and marks which ones came from untrusted input.
openssl verify -show_chain -CAfile root.pem -untrusted intermediate.pem leaf.pem
leaf.pem: OK
Chain:
depth=0: CN=www.example.com (untrusted)
depth=1: C=CN, O=Example Corp, CN=Example Intermediate CA (untrusted)
depth=2: C=CN, O=Example Corp, CN=Example Root CA
This is the quickest way to see which root a cross-signed chain actually ended on. When several paths exist, OpenSSL prefers a trusted certificate from the store over one supplied in -untrusted.
Verify against the system trust store
Without -CAfile, OpenSSL uses the default store under OPENSSLDIR, which may differ from your operating system's store.
openssl version -d
openssl verify -untrusted chain.pem leaf.pem
OPENSSLDIR: "/opt/homebrew/etc/openssl@3"
leaf.pem: OK
On Debian and Ubuntu the default store is /etc/ssl/certs (maintained by update-ca-certificates); on RHEL-family systems it is /etc/pki/tls; Homebrew ships its own cert.pem. Use -no-CAfile -no-CApath -no-CAstore together with -CAfile when you want to make sure only one root is trusted. A brand-new root may be missing from an old store even though browsers trust it.
Verify a whole bundle file
openssl verify checks only the first certificate in a file, so pass the same bundle as -untrusted too.
cat leaf.pem intermediate.pem > fullchain.pem
openssl verify -CAfile root.pem fullchain.pem
openssl verify -CAfile root.pem -untrusted fullchain.pem fullchain.pem
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error fullchain.pem: verification failed
fullchain.pem: OK
The first command fails because the intermediate in the same file is ignored. The second succeeds and proves the bundle is complete and in a usable order.
Check the hostname the way a client does
-verify_hostname checks the name against the Subject Alternative Name, exactly as a browser or curl would.
openssl verify -CAfile root.pem -untrusted intermediate.pem \
-verify_hostname shop.example.com leaf.pem
openssl x509 -in leaf.pem -noout -checkhost www.example.com
CN=www.example.com
error 62 at 0 depth lookup: hostname mismatch
error leaf.pem: verification failed
Hostname www.example.com does match certificate
Modern clients ignore the Common Name when a SAN is present, so a name that appears only in CN will fail. A wildcard *.example.com covers shop.example.com but not example.com or a.b.example.com. Use -verify_ip 203.0.113.10 for IP address certificates.
Check the certificate purpose (EKU)
-purpose sslserver requires the serverAuth extended key usage; sslclient requires clientAuth.
openssl verify -CAfile root.pem -untrusted intermediate.pem -purpose sslclient leaf.pem
openssl x509 -in leaf.pem -noout -ext extendedKeyUsage
CN=www.example.com
error 26 at 0 depth lookup: unsuitable certificate purpose
error leaf.pem: verification failed
X509v3 Extended Key Usage:
TLS Web Server Authentication
This matters for mutual TLS: the Chrome Root Program began phasing clientAuth out of publicly trusted TLS hierarchies on 2026-06-15 (subscriber certificates issued from 2027-03-15 must assert only serverAuth), and Let's Encrypt's Generation Y certificates no longer include it, so public certificates increasingly fail -purpose sslclient. See client auth EKU removed and mTLS.
Find and download a missing intermediate
The caIssuers URL in the Authority Information Access extension points to the issuer certificate, usually served as DER.
openssl x509 -in leaf.pem -noout -ext authorityInfoAccess
curl -s -o issuer.der http://ca.example.net/issuing-ca.crt
openssl x509 -inform DER -in issuer.der -out intermediate.pem
openssl x509 -in intermediate.pem -noout -subject -issuer
Authority Information Access:
CA Issuers - URI:http://ca.example.net/issuing-ca.crt
subject=C=US, O=Example CA, CN=Example TLS Issuing CA
issuer=C=US, O=Example CA, CN=Example Root R1
Check that the downloaded certificate's subject equals the leaf's issuer. If the file starts with -----BEGIN it is already PEM; file issuer.der tells you. Some CAs serve a PKCS#7 bundle (.p7c) instead: convert it with openssl pkcs7 -inform DER -in issuer.p7c -print_certs -out intermediate.pem. Repeat until the issuer is a root you trust, but do not put the root into the chain file.
Check the order of certificates in a bundle
Each certificate's issuer must be the subject of the next one, starting with the leaf.
openssl storeutl -noout -text -certs fullchain.pem | grep -E '^[0-9]+: |Subject:|Issuer:'
0: Certificate
Issuer: C=CN, O=Example Corp, CN=Example Intermediate CA
Subject: CN=www.example.com
1: Certificate
Issuer: C=CN, O=Example Corp, CN=Example Root CA
Subject: C=CN, O=Example Corp, CN=Example Intermediate CA
On older OpenSSL without storeutl use openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout. Nginx and HAProxy read the first certificate as the server certificate, so a bundle that starts with the intermediate produces a key mismatch error (see nginx key values mismatch).
Check that a certificate matches its private key
Compare a hash of the public key from both files; identical hashes mean they belong together.
openssl x509 -in leaf.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256
openssl req -in request.csr -noout -pubkey | openssl sha256
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
This works for RSA, ECDSA and Ed25519 keys alike, unlike the old -modulus trick which only works for RSA. The third line checks the CSR as well. The browser-based Key Matcher does the same comparison without uploading the key.
Verify as of a past or future date
-attime takes a Unix timestamp and checks validity at that moment, which is handy before a planned rollover.
openssl verify -attime $(date -d 2028-01-01 +%s) \
-CAfile root.pem -untrusted intermediate.pem leaf.pem
CN=www.example.com
error 10 at 0 depth lookup: certificate has expired
error leaf.pem: verification failed
On macOS use date -j -f %Y-%m-%d 2028-01-01 +%s. Error 9 means "certificate is not yet valid", which often points to a wrong system clock rather than a bad certificate. For a simple expiry check, openssl x509 -in leaf.pem -noout -checkend 2592000 exits non-zero if the certificate expires within 30 days.
Verify what a live server actually sends
s_client shows the chain the server presents; -verify_return_error turns verification failures into a hard error.
openssl s_client -connect www.example.com:443 -servername www.example.com \
-verify_return_error < /dev/null
depth=0 CN=www.example.com
verify error:num=20:unable to get local issuer certificate
...:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:...
---
Certificate chain
0 s:CN=www.example.com
i:C=CN, O=Example Corp, CN=Example Intermediate CA
Without -verify_return_error, s_client prints the error but finishes the handshake anyway, ending with Verify return code: 21 (unable to verify the first certificate). Only one certificate under "Certificate chain" confirms that the server does not send its intermediate. More options are in testing TLS connections.
Save every certificate a server sends to separate files
Combine -showcerts with a short awk script to split the chain into cert-1.pem, cert-2.pem and so on.
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null 2>/dev/null \
| awk '/BEGIN CERT/{n++; f="cert-" n ".pem"} f{print > f} /END CERT/{f=""}'
for f in cert-*.pem; do openssl x509 -in "$f" -noout -subject; done
subject=CN=www.example.com
subject=C=CN, O=Example Corp, CN=Example Intermediate CA
cert-1.pem is the leaf. Feed the rest into -untrusted to verify offline, or into the Certificate Decoder to inspect them. If the last file is self-signed, the server is sending its root, which is unnecessary.
FAQ
Why does openssl verify fail when the browser says the certificate is fine?
Browsers cache intermediate certificates and some fetch missing ones from the AIA URL, so they can hide an incomplete chain. openssl verify only uses what you give it. If it reports error 20 at depth 0, pass the intermediate with -untrusted; if that fixes it, the server is not sending the intermediate either. See missing intermediate certificate.
Should the root certificate be in my fullchain.pem?
No. Clients already have the root in their trust store, and sending it only adds bytes to every handshake. The chain file should contain the leaf followed by the intermediate(s). Some strict clients report self-signed certificate in certificate chain when an untrusted root is included.
What is the difference between -CAfile and -untrusted?
-CAfile adds trust anchors: anything in it is trusted unconditionally. -untrusted adds candidates for building the path, which must still chain up to a trust anchor. Putting an intermediate in -CAfile makes OpenSSL stop at that intermediate and report error 2 unless you add -partial_chain.