<!-- https://pkizerotohero.com/guides/check-remote-certificate-chain/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Check the chain a server sends with openssl s_client -showcerts: find a missing intermediate, wrong order or wrong root, then verify it strictly.

Source: https://pkizerotohero.com/guides/check-remote-certificate-chain/ · Last reviewed: 2026-10-20

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.

Task

[Inspect and check](https://pkizerotohero.com/guides/#task-inspect)

Platforms

- Linux
- macOS
- Windows
- Java

Who it is for

- [Developer](https://pkizerotohero.com/start/#developer)
- [IT ops](https://pkizerotohero.com/start/#itops)
- [Expert](https://pkizerotohero.com/start/#expert)

Level

Practitioner · 6 steps

## Before you start

- **OpenSSL 1.1.1 or later** (Linux, macOS with Homebrew, or Git Bash on Windows). Java users can use `keytool` instead (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](https://pkizerotohero.com/handbook/glossary/#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

```bash
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](http://www.rfc-editor.org) on 20 October 2026:

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

1. **Entry 0 is your server’s certificate** (the [leaf](https://pkizerotohero.com/handbook/glossary/#leaf-certificate)), for the name you connected to.
2. **Every `i:` equals the next entry’s `s:`.** Above, the leaf was issued by `WE1`, `WE1` by `GTS Root R4`, and so on. A gap means a missing or wrong certificate.
3. **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.
4. **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](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1)).

Entry 2 above is a [cross-signed](https://pkizerotohero.com/handbook/glossary/#cross-signing) 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](https://pkizerotohero.com/handbook/cross-signing/) 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:

```text
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](https://pkizerotohero.com/errors/openssl-verify-error-20/) and [21](https://pkizerotohero.com/errors/openssl-verify-error-21/): the intermediate is missing. The same server with the intermediate added, checked against our private root:

```bash
openssl s_client -connect 127.0.0.1:8444 -servername www.example.com -CAfile root.crt -verify_return_error </dev/null
```

```text
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_error` stops the handshake at the first error, as a real client would. Without it, `s_client` reports the error and carries on.
- `-CAfile root.crt` tells 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.

Common mistake

Verify return code 0 does not check the name

By default, `s_client` checks the chain but not the host name. Our test server returned `0 (ok)` when we asked for `shop.example.com`, a name its certificate does not contain. Add `-verify_hostname shop.example.com` and the result becomes `Verify return code: 62 (hostname mismatch)`.

### 4. Save the chain and check it offline

Save every certificate the server sent into one file:

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

```bash
openssl verify -show_chain -CAfile root.crt -untrusted sent.pem sent.pem
```

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

*Interactive tool: Chain checker (runs in the browser at [/tools/chain/](https://pkizerotohero.com/tools/chain/))*

### 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:

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

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

```bash
keytool -printcert -sslserver www.rfc-editor.org:443
```

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

```bash
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](https://pkizerotohero.com/guides/build-fullchain/). Clients report it as [curl’s `unable to get local issuer certificate`](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/) or [Java’s `PKIX path building failed`](https://pkizerotohero.com/errors/java-pkix-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](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/) and [How to work behind a TLS-inspecting proxy](https://pkizerotohero.com/guides/corporate-proxy-certificates/).
- **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 the `depth=` 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](https://pkizerotohero.com/errors/openssl-certificate-has-expired/).
- **Error 62, `hostname mismatch`:** the certificate does not cover the name. Check `-servername` and the SAN list ([How to inspect a certificate](https://pkizerotohero.com/guides/inspect-certificate-openssl/)).
- **`wrong version number` (some OpenSSL 3.2–3.6 releases print `packet length too long` instead) or a hang:** the port does not speak TLS directly. Try `-starttls`, or check the port ([`curl: (35)`](https://pkizerotohero.com/errors/curl-35-ssl-connect-error/)).

## 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](https://pkizerotohero.com/handbook/glossary/#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](https://pkizerotohero.com/handbook/certificate-chains/) explains the linking and checking, [Who do we trust?](https://pkizerotohero.com/handbook/trust-stores/) explains which store each client uses, and [Two roads to the same root](https://pkizerotohero.com/handbook/cross-signing/) explains why chains differ between clients.

## Go deeper

- [OpenSSL `s_client` manual](https://docs.openssl.org/master/man1/openssl-s_client/): `-showcerts`, `-verify_return_error`, `-starttls` and `-proxy`.
- [OpenSSL `verify` manual](https://docs.openssl.org/master/man1/openssl-verify/): offline checks with `-untrusted` and `-show_chain`.
- [RFC 9846, section 4.5.1](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1): what a TLS 1.3 server must put in its Certificate message.
- [RFC 5280, section 6](https://www.rfc-editor.org/rfc/rfc5280#section-6): the path validation algorithm.
- [curl: SSL certificate verification](https://curl.se/docs/sslcerts.html): how curl finds its CA certificates.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/check-remote-certificate-chain/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 13: Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/)
- [Chapter 12: Who do we trust? CAs, trust stores and self-signed certificates](https://pkizerotohero.com/handbook/trust-stores/)
- [Chapter 14: Two roads to the same root: cross-signing and root expiry](https://pkizerotohero.com/handbook/cross-signing/)

### Error pages

- [verify error:num=20:unable to get local issuer certificate](https://pkizerotohero.com/errors/openssl-verify-error-20/)
- [verify error:num=21:unable to verify the first certificate](https://pkizerotohero.com/errors/openssl-verify-error-21/)
- [SSL certificate problem: unable to get local issuer certificate](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/)
- [PKIX path building failed … unable to find valid certification path to requested target](https://pkizerotohero.com/errors/java-pkix-path-building-failed/)
- [SSL certificate problem: self-signed certificate in certificate chain](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.

### Topics

- [Chains and trust stores](https://pkizerotohero.com/topics/chains-and-trust/)
- [TLS and HTTPS](https://pkizerotohero.com/topics/tls/)

## More guides: Inspect and check

- [How to inspect a certificate with OpenSSL](https://pkizerotohero.com/guides/inspect-certificate-openssl/)
- [How to check that a private key matches a certificate](https://pkizerotohero.com/guides/check-key-matches-certificate/)
