# OpenSSL verify: check certificate chains, hostnames and keys

> OpenSSL 3.x commands to verify a certificate chain: -CAfile vs -untrusted, hostname and EKU checks, missing intermediates, chain order, key match.

Source: https://ssl.sh/cookbook/verify-chains/
Last updated: 2026-11-10

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.

```bash
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

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

```bash
openssl verify -CAfile root.pem leaf.pem
```

```text
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](https://ssl.sh/errors/missing-intermediate-certificate/)), or build it with [Chain Builder](https://ssl.sh/tools/chain-builder/).

## Print the chain OpenSSL built

`-show_chain` lists every certificate in the verified path and marks which ones came from untrusted input.

```bash
openssl verify -show_chain -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

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

```bash
openssl version -d
openssl verify -untrusted chain.pem leaf.pem
```

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

```bash
cat leaf.pem intermediate.pem > fullchain.pem
openssl verify -CAfile root.pem fullchain.pem
openssl verify -CAfile root.pem -untrusted fullchain.pem fullchain.pem
```

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

```bash
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
```

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

```bash
openssl verify -CAfile root.pem -untrusted intermediate.pem -purpose sslclient leaf.pem
openssl x509 -in leaf.pem -noout -ext extendedKeyUsage
```

```text
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](https://ssl.sh/errors/client-auth-eku-removed/) and [mTLS](https://ssl.sh/learn/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.

```bash
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
```

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

```bash
openssl storeutl -noout -text -certs fullchain.pem | grep -E '^[0-9]+: |Subject:|Issuer:'
```

```text
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](https://ssl.sh/errors/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.

```bash
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
```

```text
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](https://ssl.sh/tools/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.

```bash
openssl verify -attime $(date -d 2028-01-01 +%s) \
  -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

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

```bash
openssl s_client -connect www.example.com:443 -servername www.example.com \
  -verify_return_error < /dev/null
```

```text
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](https://ssl.sh/cookbook/test-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.

```bash
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
```

```text
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](https://ssl.sh/tools/certificate-decoder/) to inspect them. If the last file is self-signed, the server is sending its root, which is unnecessary.
