How to check certificate chain

Ask for every certificate the server sends and read the last line: openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null. Each s: is a subject, each i: its issuer, and they must link end to end. Verify return code: 0 (ok) is the only passing value.

Checker offline. Follow the manual steps below, they give the same answer.

Why check this

A server that forgets its intermediate certificate can still load in a desktop browser, because the browser repairs the chain from a URL inside the certificate. curl on the same host refuses the connection with exit code 60, as step 4 shows. The bug arrives as "the integration partner cannot reach our API, but the site loads fine for everyone here". Run this after every certificate deployment and after any change of certificate authority, from a machine that is not a browser.

Prerequisites

Steps

  1. Step 1.

    List the chain the server presents.

    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E '^(Certificate chain| [0-9] s:| +i:|Verify return code)'
    
    Certificate chain
    0 s:CN = example.com
     i:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
    1 s:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
     i:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
    2 s:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
     i:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
    3 s:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
     i:C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
    Verify return code: 0 (ok)

    Read it downward. The i: of every entry is the s: of the next one. A gap is the defect.

  2. Step 2.

    Count what the server sends, against a host known to send too little.

    for h in example.com incomplete-chain.badssl.com; do printf '%-30s ' "$h"; openssl s_client -connect $h:443 -servername $h -showcerts </dev/null 2>/dev/null | grep -c 'BEGIN CERTIFICATE'; done
    
    example.com                    4
    incomplete-chain.badssl.com    1
  3. Step 3.

    Read the broken host in full, where the verdict line names the failure.

    openssl s_client -brief -connect incomplete-chain.badssl.com:443 -servername incomplete-chain.badssl.com </dev/null
    
    depth=0 CN = *.badssl.com
    verify error:num=20:unable to get local issuer certificate
    depth=0 CN = *.badssl.com
    verify error:num=21:unable to verify the first certificate
    CONNECTION ESTABLISHED
    Protocol version: TLSv1.2
    Peer certificate: CN = *.badssl.com
    Verification error: unable to verify the first certificate
    DONE

    Note depth=0 on both errors. Verification never reached depth 1, because nothing was sent at depth 1.

  4. Step 4.

    Confirm that a non-browser client refuses the same host.

    curl -sS -o /dev/null -w '%{http_code}\n' https://incomplete-chain.badssl.com/; echo "exit=$?"
    
    curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
    000
    exit=60

    Open the same URL in a browser and compare the two results. The gap between them is the finding, not the curl exit code on its own.

  5. Step 5.

    Verify the leaf on its own, which is what a strict client does.

    openssl verify leaf.pem
    
    CN = example.com
    error 20 at 0 depth lookup: unable to get local issuer certificate
    error leaf.pem: verification failed

    Error 20 again, this time on a healthy host, because the file holds no intermediates.

  6. Step 6.

    Add the intermediates and verify once more.

    openssl verify -untrusted inter.pem leaf.pem
    
    leaf.pem: OK

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Verify return code: 0 (ok) | A path reached a trusted root | Pass. Record the depth count for the next deployment. | | 20 (unable to get local issuer certificate) | The issuer of some certificate was neither sent nor trusted | Append the missing intermediate to the server bundle. | | 21 (unable to verify the first certificate) | Only the leaf arrived | The same defect, stated from the client side. | | Chain deeper than the issuer documents | Extra or stale certificates are being sent | Harmless for trust, costly for handshake size. Trim the bundle. | | A root at the last position | The server sends its own root | Also harmless, also wasted bytes. Clients use their own copy. |

Common mistakes

Sign: It works in every browser, so the chain is declared fine.Cause: The leaf on that host carries an Authority Information Access extension: CA Issuers - URI:http://yr2.i.lencr.org/, read with openssl x509 -noout -ext authorityInfoAccess. A client that follows the URL repairs the chain for itself. curl, on the same host and the same second, exits 60.
Sign: The bundle file was reviewed and the order looked right, yet clients still fail.Cause: The server was never reloaded, or one node of several was. Read the chain over the network as in step 1, not from the file, and repeat per node by IP.
Sign: Verification passes on the build agent and fails on a customer machine.Cause: openssl used the local trust store. An older store missing the new root gives error 20 on a correctly configured server. Name the store in the report, or verify with -CAfile against the store the client ships.

What to check next

FAQ

How to verify a certificate chain with openssl?

Two ways, both above. Over the network, s_client -showcerts and the Verify return code line. Offline, openssl verify -untrusted inter.pem leaf.pem, which is the right check for a bundle before it is deployed.

How to check if intermediate certificates are installed correctly?

Count them, as in step 2. A correct bundle sends the leaf plus every intermediate up to a root, and the count matches what the authority documents. One certificate means intermediates are missing.

How to check certificate transparency logs?

The signed timestamps travel inside the certificate. openssl x509 -in cert.pem -noout -ext ct_precert_scts prints each log ID and its timestamp. That proves the certificate was logged; querying a log for all certificates on a domain is a separate, browser-side check.

Does the order of certificates in the bundle matter?

Keep the leaf first and each issuer after the certificate it signed. That is the order step 1 shows on a working host, and the order openssl verify -untrusted accepted in step 6. Some clients sort the list themselves, so an unordered bundle fails unevenly rather than always.

Verified

Verified by Maks Vernyopenssl 3.1.1curl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate8 minpublished updated Maks Verny