How-to guide · Inspect and check
How to check the certificate chain a server sends
In one sentence
You will have listed every certificate a server sends, checked their order and links, and verified the chain with a strict client instead of a browser.
- Platforms
- Linux
- macOS
- Windows
- Java
- Level
- Practitioner · 6 steps
On this page
Before you start
- OpenSSL 1.1.1 or later (Linux, macOS with Homebrew, or Git Bash on Windows). Java users can use
keytoolinstead (step 6). - The host name and port of the server, and network access to it from where you run the command.
- For an internal server: your organisation’s root certificate as a PEM file, to use with
-CAfile.
Why bother? A browser hides most chain mistakes: it can download a missing intermediate certificate or reuse one from another site. curl on Linux, Java, Python, many mobile apps and partner systems cannot, so they fail while your browser test passes.
Steps
1. List the certificates the server sends
openssl s_client -connect www.rfc-editor.org:443 -servername www.rfc-editor.org -showcerts </dev/null 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ +[iav]:|Verify return code'
This is the live chain of www.rfc-editor.org on 20 October 2026:
0 s:CN=rfc-editor.org
i:C=US, O=Google Trust Services, CN=WE1
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
v:NotBefore: Oct 10 11:18:19 2026 GMT; NotAfter: Jan 8 12:18:06 2027 GMT
1 s:C=US, O=Google Trust Services, CN=WE1
i:C=US, O=Google Trust Services LLC, CN=GTS Root R4
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Dec 13 09:00:00 2023 GMT; NotAfter: Feb 20 14:00:00 2029 GMT
2 s:C=US, O=Google Trust Services LLC, CN=GTS Root R4
i:C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: RSA-SHA256
v:NotBefore: Nov 15 03:43:21 2023 GMT; NotAfter: Jan 28 00:00:42 2028 GMT
Verify return code: 0 (ok)
Each numbered entry is one certificate, in the order the server sent it: s: is the subject, i: the issuer, a: the key and signature algorithm, and v: the validity dates. OpenSSL 3.0 prints the same lines with spaces around =. Without the grep, you also get each certificate’s PEM block.
As the OpenSSL manual says about -showcerts, “It is not a verified chain”: it shows what arrived, not what your client trusted.
2. Read the chain
Check four things:
- Entry 0 is your server’s certificate (the leaf), for the name you connected to.
- Every
i:equals the next entry’ss:. Above, the leaf was issued byWE1,WE1byGTS Root R4, and so on. A gap means a missing or wrong certificate. - The intermediates are there. If you see only entry 0 and its issuer is not a root in your trust store, the intermediate is missing.
- The root is optional. The client must already have it. The TLS 1.3 specification, RFC 9846, lets the server omit it (section 4.5.1).
Entry 2 above is a cross-signed certificate: Google’s root GTS Root R4, signed by the older GlobalSign Root CA. Clients that already trust GTS Root R4 stop at entry 1; older clients follow the cross-sign to GlobalSign. Two roads to the same root explains why servers send these.
3. Let OpenSSL verify it
The last line, Verify return code, is OpenSSL’s verdict against its own trust store. Here is a test server that sends only its leaf:
depth=0 C=BE, O=Example Shop, CN=www.example.com
verify error:num=20:unable to get local issuer certificate
...
depth=0 C=BE, O=Example Shop, CN=www.example.com
verify error:num=21:unable to verify the first certificate
...
Certificate chain
0 s:C=BE, O=Example Shop, CN=www.example.com
i:C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
...
Verify return code: 21 (unable to verify the first certificate)
One entry, and errors 20 and 21: the intermediate is missing. The same server with the intermediate added, checked against our private root:
openssl s_client -connect 127.0.0.1:8444 -servername www.example.com -CAfile root.crt -verify_return_error </dev/null
depth=2 C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
verify return:1
depth=1 C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
verify return:1
depth=0 C=BE, O=Example Shop, CN=www.example.com
verify return:1
...
Verify return code: 0 (ok)
Two options make the test strict:
-verify_return_errorstops the handshake at the first error, as a real client would. Without it,s_clientreports the error and carries on.-CAfile root.crttells OpenSSL which root to trust for a private CA. Give it only the root: if you add the intermediate too, you hide the very mistake you are looking for.
4. Save the chain and check it offline
Save every certificate the server sent into one file:
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null 2>/dev/null \
| sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' > sent.pem
Then verify the first certificate in it, offering the rest as intermediates. Our test chain ends in a private root, so we pass it with -CAfile; for a public CA, leave that option out:
openssl verify -show_chain -CAfile root.crt -untrusted sent.pem sent.pem
sent.pem: OK
Chain:
depth=0: C=BE, O=Example Shop, CN=www.example.com (untrusted)
depth=1: C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1 (untrusted)
depth=2: C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
“Untrusted” means “supplied, not a trust anchor”, which is right for a leaf and its intermediates. You can also paste sent.pem into the chain checker:
5. Check servers on other ports and protocols
Mail, LDAP and database servers often start in plain text and switch to TLS. Tell s_client the protocol:
openssl s_client -connect smtp.gmail.com:587 -starttls smtp -showcerts </dev/null 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ +i:|Verify return code'
0 s:CN=smtp.gmail.com
i:C=US, O=Google Trust Services, CN=WE2
1 s:C=US, O=Google Trust Services, CN=WE2
i:C=US, O=Google Trust Services LLC, CN=GTS Root R4
2 s:C=US, O=Google Trust Services LLC, CN=GTS Root R4
i:C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
Verify return code: 0 (ok)
-starttls accepts smtp, imap, pop3, ftp, ldap, postgres, mysql and a few more. If your network only allows web traffic through a proxy, add -proxy proxy.example.com:8080.
6. On Windows or with Java
Git for Windows includes OpenSSL, so every command above runs in Git Bash. With a JDK, keytool prints each certificate the server sends with its SHA-256 fingerprint:
keytool -printcert -sslserver www.rfc-editor.org:443
Certificate #0
Owner: CN=rfc-editor.org
Issuer: CN=WE1, O=Google Trust Services, C=US
...
Certificate #1
Owner: CN=WE1, O=Google Trust Services, C=US
Issuer: CN=GTS Root R4, O=Google Trust Services LLC, C=US
Check it worked
Run the strict test from outside your own network, against the public name, after every certificate change:
openssl s_client -connect www.example.com:443 -servername www.example.com \
-verify_return_error -verify_hostname www.example.com </dev/null 2>&1 | grep 'Verify return code'
Verify return code: 0 (ok) means the chain links up to a root OpenSSL trusts, every certificate is in date, and the name matches. For an internal server, add -CAfile with your root.
If something goes wrong
- Only
0 s:is listed, with error 20 or 21: the server does not send its intermediate. Fix the server with How to build the full chain file. Clients report it as curl’sunable to get local issuer certificateor Java’sPKIX path building failed. - Error 19,
self-signed certificate in certificate chain: the chain ends in a root your client does not trust: a private CA, or a TLS-inspection proxy. See curl’s self-signed chain error and How to work behind a TLS-inspecting proxy. - The issuer of a public site is your security product: your traffic is being inspected. Same guide as above.
- Error 10,
certificate has expired: look at thedepth=line before it. Depth 0 is the leaf; depth 1 or more is an expired intermediate, usually an old chain file. See certificate has expired. - Error 62,
hostname mismatch: the certificate does not cover the name. Check-servernameand the SAN list (How to inspect a certificate). wrong version number(some OpenSSL 3.2–3.6 releases printpacket length too longinstead) or a hang: the port does not speak TLS directly. Try-starttls, or check the port (curl: (35)).
Why this works
In TLS, the server sends its certificate chain in one message, and the client must link each certificate to its issuer until it reaches a root in its own trust store. OpenSSL, like curl on Linux, Python and Java, uses only what the server sent plus its trust store; it does not download missing intermediates. That makes it a faithful stand-in for your strictest clients. On Windows, curl.exe and other tools that use the Windows chain engine can download a missing intermediate, so they may pass where OpenSSL fails. Certificate chains and path validation explains the linking and checking, Who do we trust? explains which store each client uses, and Two roads to the same root explains why chains differ between clients.
Go deeper
- OpenSSL
s_clientmanual:-showcerts,-verify_return_error,-starttlsand-proxy. - OpenSSL
verifymanual: offline checks with-untrustedand-show_chain. - RFC 9846, section 4.5.1: what a TLS 1.3 server must put in its Certificate message.
- RFC 5280, section 6: the path validation algorithm.
- curl: SSL certificate verification: how curl finds its CA certificates.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.