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
- openssl 1.1.1 or later. See the verify manual for the error numbers below.
- curl, any build, for step 4.
- Steps 5 and 6 read two files split out of the step 1 download: pipe the
-showcertsoutput throughawk '/BEGIN CERT/,/END CERT/', put the first block inleaf.pemand the rest ininter.pem.
Steps
- 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 thes:of the next one. A gap is the defect. - 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'; doneexample.com 4 incomplete-chain.badssl.com 1 - 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/nulldepth=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 DONENote
depth=0on both errors. Verification never reached depth 1, because nothing was sent at depth 1. - 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=60Open 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.
- Step 5.
Verify the leaf on its own, which is what a strict client does.
openssl verify leaf.pemCN = example.com error 20 at 0 depth lookup: unable to get local issuer certificate error leaf.pem: verification failedError 20 again, this time on a healthy host, because the file holds no intermediates.
- Step 6.
Add the intermediates and verify once more.
openssl verify -untrusted inter.pem leaf.pemleaf.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
What to check next
- How to check SSL certificate from command line: the leaf fields behind the chain you just read.
- How to check SSL certificate expiry: an intermediate expires on its own schedule.
- How to check if a certificate is self-signed: the case where subject and issuer are the same line.
- How to check if OCSP stapling is enabled: the other thing the handshake carries beside certificates.
- Ssl certificate checker: the chain read from a server rather than your machine.
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.
Related on this site
- Checker: tls-certificate TLS versions offered, certificate chain, expiry, SANs
- TLS and certificate review
- Website launch checklist
- All security headers and tls checks
intermediate8 minpublished updated Maks Verny