KeyboardWarrior.co.ukclient-side utility arcade

Security

TLS certificates and what the browser checks

What a browser checks before the padlock appears: the hostname, the dates, the chain to a trusted root, and the openssl commands that catch a bad certificate.

9 min read

A padlock is a decision, not a picture. Before drawing it, the browser has asked three questions: did a certificate arrive, does a chain of signatures end at a root this browser already trusts, and does that certificate name the host in the address bar. Encryption can succeed while the third answer is no. That is a proxy with a certificate of its own, and the warning is the point of the check.

This note is how to read that decision with OpenSSL 3, the openssl on current Debian and Ubuntu. DNS and how a browser finds a website stops when the address is known. Nginx explained is where the certificate files are installed and renewed. This one starts once a TCP connection to port 443 exists.

openssl version should say OpenSSL. macOS often ships LibreSSL under the same command name, and it rejects some of the flags below.

What the handshake is deciding

The client starts with a ClientHello. The Server Name Indication extension inside it is the hostname, so a server that holds many certificates can pick one. OpenSSL 3 sends that extension by default when -connect is a hostname: the help text marks -servername as the default, and a connection to example.com without the flag still presented that site's certificate. A bare address has no name to send. On 10 November 2026, example.com's address answered a nameless handshake with sslv3 alert handshake failure. The same address with -servername example.com presented the certificate.

The server replies with a chain. The first certificate is the leaf, the one that carries the site's name. After it come intermediates, which the client does not already have. The root is supposed to live in the client's trust store. A server may send a root as well. A client that already trusts its own copy does not need that extra one.

The client then checks, roughly in this order:

  • Each certificate's signature verifies with the next public key up the chain.
  • The dates contain "now".
  • The leaf is not a certificate authority, and it is allowed to authenticate a TLS server.
  • The hostname matches a subject alternative name.

Any one of those can fail on its own. A trusted chain with the wrong name is still a warning. A matching name signed by an authority nobody trusts is still a warning. TLS 1.3 is the protocol you will usually see, printed as Protocol version: TLSv1.3. The certificate checks are the same idea on TLS 1.2. The protocol version is not the padlock.

The name that has to match

The common name, printed as CN, is the old place to put a hostname. The names that count are in the subject alternative name extension. Each DNS name is one entry. An IP address is a different kind of entry, and a DNS name does not match an address that was typed.

Two rules decide a match.

If a DNS alternative name is present, the common name is ignored. A certificate whose common name is ignored.example and whose only alternative name is real.example matches real.example and rejects ignored.example. OpenSSL reports the rejection as verify error:num=62:hostname mismatch.

If there is no DNS alternative name at all, OpenSSL falls back to the common name, and openssl s_client -verify_hostname can return success. Chrome does not fall back. It has required a subject alternative name since version 58, in 2017. A common-name-only certificate that OpenSSL accepts can still be a browser error. Public certificate authorities put the names in the SAN. If you mint a certificate yourself, put the name there too.

A wildcard is one label, and only the leftmost label. *.example.com matches www.example.com and foo.example.com. It does not match example.com. It does not match a.b.example.com, because that name is two labels under example.com. A certificate whose only alternative name is DNS:*.example.com produces error 62 for the apex. The apex needs its own DNS:example.com entry.

Name *.example.com only Why
www.example.com matches one label in place of the star
foo.example.com matches the same rule, for any single label
example.com no match a star does not cover the name above it
a.b.example.com no match the star is not allowed to contain a dot

Those four rows are what OpenSSL 3.0.13 returns for a certificate that contains only that wildcard.

The public certificate for example.com, checked on 10 November 2026, listed both DNS:example.com and DNS:*.example.com. The apex matched the explicit entry, reported as Verified peername: example.com. www.example.com matched the wildcard, reported as Verified peername: *.example.com. a.b.example.com was error 62. A later issuance can change the list. Read the certificate you are served, not this paragraph.

Read a live certificate

s_client speaks TLS and then waits for you to type. Give it an empty standard input, or it sits there until the terminal is closed.

shell
echo | openssl s_client -connect example.com:443 -servername example.com -brief

A healthy answer looks like this. The cipher suite moves over time. The lines that matter are the protocol, the peer, and the verification.

text
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Peer certificate: CN = example.com
Verification: OK

That Verification: OK is the chain, the signatures, and the dates. It is not the hostname. s_client does not compare the certificate to a name unless you pass -verify_hostname. A match adds a Verified peername line. A miss is error 62, and the output can still say CONNECTION ESTABLISHED. The bytes were encrypted. The name check failed.

shell
echo | openssl s_client -connect example.com:443 \
  -servername example.com \
  -verify_hostname example.com \
  -brief

-verify_return_error closes the connection when verification fails, and the process exits 1. Without that flag, a hostname mismatch still exits 0 once the handshake has completed. A green shell prompt is not a passed check. Confirmed on OpenSSL 3.0.13: the same mismatch exited 0 without the flag and 1 with it.

The names and the dates are fields on the leaf. x509 reads the certificate that s_client wrote to standard output. Diagnostics stay on standard error, and the pipe below discards them, so leave standard error attached when the certificate comes back empty.

shell
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

On 10 November 2026 that printed:

text
subject=CN = example.com
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notBefore=Sep 26 22:49:11 2026 GMT
notAfter=Dec 25 22:56:35 2026 GMT
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:*.example.com

A renewal replaces the dates. The shape does not. notBefore and notAfter are GMT, not the zone set on the laptop. A clock a few hours slow can call a fresh certificate not yet valid: error 9, certificate is not yet valid. A clock set forward, or a certificate nobody renewed, is error 10, certificate has expired. timedatectl shows whether a systemd host thinks its clock is synchronised. Fix the clock before you reissue.

The leaf should also say it is not a certificate authority, and that it may authenticate a TLS server. A certificate with CA:TRUE, or one issued only for signing email, is the wrong object even when the name looks familiar.

shell
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -ext basicConstraints,extendedKeyUsage

For example.com on that same day the extension block was CA:FALSE and TLS Web Server Authentication.

The chain the server has to send

Save the leaf on its own and ask the trust store about that file:

shell
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 > /tmp/example-leaf.pem
openssl verify /tmp/example-leaf.pem

That failed, on a connection whose handshake had succeeded:

text
CN = example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error /tmp/example-leaf.pem: verification failed

Error 20 means the trust store has nothing that signed this certificate. The store holds roots, not the intermediate that signed example.com. The handshake still printed Verify return code: 0 (ok), because s_client was handed the intermediates in the same connection. -showcerts prints every certificate the server sent, PEM block after PEM block, leaf first.

Save the later blocks and pass them with -untrusted. openssl verify then prints /tmp/example-leaf.pem: OK. Nothing was wrong with the live site. The leaf file was an incomplete chain, which is also what a server sends when it is configured with the leaf alone. In that second case s_client reports error 20 as well, and the browser reports an untrusted authority. Compare the two commands before you call the site broken.

Let's Encrypt writes cert.pem as the leaf and fullchain.pem as the leaf followed by its intermediates. The server wants fullchain.pem. Nginx is where that path is set. A name that only redirects still has to be listed: the check runs for the host that was typed, and the redirect happens afterwards. A CNAME does not change that name. The alias becomes an address, then the typed name is compared with the SAN.

A chain that does not end at a trusted root is a different fault from a short chain. A self-signed leaf is error 18, self-signed certificate. Installing that file as a private trust anchor is a local decision. Sending it to the public internet produces a warning in every browser that does not have your root.

The private key has to be the pair

A certificate contains a public key. The server has to hold the matching private key. A renewed certificate installed next to last year's key fails during the handshake, or it loads and then presents a certificate the key cannot prove.

Compare the public half of both files. The comparison works for RSA and for ECDSA. The older modulus comparison does not: it is RSA only, and the example.com leaf above is ECDSA, printed as Signature type: ECDSA.

shell
openssl x509 -in cert.pem -noout -pubkey \
  | openssl pkey -pubin -outform DER | openssl sha256
openssl pkey -in privkey.pem -pubout -outform DER | openssl sha256

The two digests have to be identical. If they differ, the files are not a pair. Point the server at the key that was created with this certificate, or issue a new pair. Do not copy a private key into the web root, or into git, to get it onto the box. SSH keys is the permission side of a private key file. A certificate is public. The key beside it is not.

Pitfalls

  • Verification: OK without -verify_hostname has not looked at the name. Error 62 is the name check, and the connection can still be established.
  • s_client exits 0 after a verification error unless you pass -verify_return_error. Do not treat a green prompt as a pass.
  • A wildcard is one leftmost label. *.example.com is not the apex, and it is not a.b.example.com.
  • The common name is not the list browsers use once a DNS alternative name exists. A certificate with no alternative name is not one Chrome will accept for a public site.
  • openssl verify on a leaf you saved reports error 20 when the intermediate is absent from the trust store. That happens for a healthy server. s_client is the command that sees the chain the server actually sent. cert.pem is the wrong file to point nginx at. fullchain.pem is the one that carries the intermediates.
  • Dates are GMT. A skewed clock looks like an expiry, or like a certificate that is not yet valid. Check the clock before you revoke or reissue.
  • An address in the bar sends no Server Name Indication, and the certificate is checked against that address. A DNS name on the certificate does not satisfy https://203.0.113.10/. The DNS note makes the same split from the lookup side.
  • The typed name is the one that is checked, including a host that only redirects. example.com does not cover www.example.com unless the SAN says so.
  • HSTS removes the button that continues past a warning, once the browser has stored Strict-Transport-Security for that host or the host is preloaded. The certificate has to be fixed. There is no HTTP exception left to try.
  • Mixed content is not a certificate failure. A page loaded over HTTPS that then requests http:// images can drop the padlock while s_client says the certificate is fine.
  • The wrong -servername, or the wrong host on -connect, inspects a different certificate. Read the Peer certificate line before the dates.

Run the brief command with -verify_hostname set to the name a visitor types, then read the alternative names and the GMT dates. If the browser calls the authority invalid and s_client reports error 20, the chain file is short an intermediate. The name, the clock, and the chain are three faults. Issuing a new certificate fixes only one of them.