Verification and testing

OpenSSL verify: check certificate chains, hostnames and keys

By · Last verified Tested macOS · OpenSSL 3.6.1

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.

-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.

Sources

  1. OpenSSL manual: openssl-verify
  2. OpenSSL manual: openssl-verification-options
  3. OpenSSL manual: openssl-s_client
  4. RFC 5280: Internet X.509 PKI Certificate and CRL Profile