← Recipes

阅读中文版本

Validate a TLS Certificate Chain with openssl Only

Hand-verify the served chain, intermediate match, expiration, and hostname coverage before blaming the app for handshake failures.

~25 minsecuritytlsopenssl

Task

Extract the live certificate chain, verify it against trust and hostname, and compute the days to expiry with openssl commands.

When to use this

Mobile clients fail TLS while desktops succeed. The served chain misses an intermediate that desktops have cached, and the alerts blame the app.

Prerequisites

  • openssl 1.1.1 or newer
  • DNS or /etc/hosts access to reach the endpoint
  • The intermediate CA file for offline verification

Steps

1. Capture the chain exactly as the server presents it.

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

Expected output: The leaf subject, issuer, and validity dates print, confirming which certificate clients actually receive first.

2. Count the certificates in the served chain.

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"

Expected output: A count of 1 means the server sends only the leaf; a count of 2 or 3 indicates intermediates are included, which is the common mobile-client fix.

3. Verify the chain against the intermediate file.

openssl verify -CAfile intermediate.pem -untrusted intermediate.pem cert.pem

Expected output: A result of OK proves the leaf chains correctly through the intermediate you hold; any other error names the exact break (unable to get local issuer, self-signed, expired).

4. Check hostname coverage including wildcards and SANs.

openssl x509 -in cert.pem -noout -ext subjectAltName | grep -o "DNS:[^,]*" | head -5; echo | openssl s_client -connect example.com:443 -servername api.example.com 2>&1 | grep -E "verify return|Verification"

Expected output: The SAN list prints alongside the handshake verification status, catching the classic wildcard example.com vs *.example.com mismatch that breaks api subdomains.

5. Compute days remaining and wire it into monitoring.

END=$(openssl x509 -in cert.pem -noout -enddate | cut -d= -f2); echo "expires: $END"; echo $(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 )) days left

Expected output: The expiry date and remaining days print, defining the threshold at which renewal alerting should fire, for example at 21 days.

Troubleshooting

Problem

Desktops work but mobile fails

Solution

The chain relies on cached intermediates on the desktop; serve the full chain including intermediates, then re-run the chain count to confirm 2 or more certificates.

Problem

verify says unable to get local issuer certificate

Solution

You are missing the issuing intermediate; fetch it from the CA bundle or the AIA extension URL, add it to the chain, and re-verify.

Problem

Handshake fails before certificate output appears

Solution

The failure is protocol or SNI level: try -tls1_2 explicitly, confirm the servername matches the vhost, and check that the port actually speaks TLS.

Limitations

  • openssl verifies chain and hostname, not revocation; OCSP or CRL checks need additional tooling and are often blocked by privacy pass-throughs.
  • A passing handshake today says nothing about renewal; the days-left computation must become a scheduled check.

Read the chain the server actually sends

TLS failures get blamed on application code because nobody reads the certificates. Four openssl commands establish what was served, whether it chains, whether it covers the hostname, and how long it lives, turning a mobile-versus-desktop mystery into a missing-intermediate answer.

# What chain does the server really present?
openssl s_client -connect example.com:443 \
  -servername example.com -showcerts </dev/null 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"
# 1   <-- leaf only; mobile clients fail here

# Serve the full chain, then verify offline
openssl verify -CAfile intermediate.pem \
  -untrusted intermediate.pem cert.pem
# cert.pem: OK

# Hostname actually covered (the wildcard trap)
openssl x509 -in cert.pem -noout -ext subjectAltName
# DNS:example.com, DNS:*.example.com

The missing intermediate is the classic split

Browsers cache intermediates from other sites, so a server that omits them still works on a desktop that has visited other sites on the same CA. Mobile runtimes and command-line clients start with an empty cache and fail. The served-chain count is the single number that diagnoses this.

Verification is a habit, not a one-off

Chain shape, hostname coverage, and expiry all drift independently: reissues change SAN lists, rotations change chains. The days-left arithmetic belongs in monitoring with a 21-day threshold, and the chain count belongs in post-deploy checks after every certificate touch.