TT Lab
Get started
Learn Learning paths Courses

Networking Fundamentals

TLS — Why the Browser Works and curl Fails

Continue in TT Lab

In a nutshell

TLS verification is linking the certificate chain the server sent up to the root in the trust store, and browsers fill in a broken chain on their own, but tools like curl do not.

Why this was needed

You deployed a new certificate, and in the browser the padlock is normal, but when called from the backend it fails like this.

curl: (60) SSL certificate problem: unable to get local issuer certificate

The most common conclusion at this point is "curl doesn't know the latest CA", and so people add -k or turn off verification. This is a measure that covers up the cause and usually sends the server into production with its configuration still wrong.

How it works

The handshake. TLS 1.3 finishes in one round trip. The ClientHello carries the supported versions, the key share, the host name to connect to (SNI), and the list of application protocols (ALPN), and every message after the ServerHello is encrypted. Three facts that directly affect operations come out of this.

First, with TLS 1.3 you cannot pull out the server certificate from a packet capture alone. To check the certificate, you must use a client-side tool.

Second, SNI is still plaintext. In today's setups where several sites sit on one IP, the server looks at the SNI to decide which certificate to present. With openssl s_client, if you leave out -servername, the SNI is not sent, and you see the wrong certificate of the default virtual host and reach a wrong conclusion.

Third, the client sends the Finished and the first request in succession, so in mutual TLS, if the client certificate is rejected, it appears not as a handshake error but as a connection drop right after the first request.

Chain verification. The server's responsibility is to send all the intermediate certificates except the root together. If an intermediate certificate is missing, the client has to find the issuer in the trust store, but the store has only roots and no intermediates, so the chain breaks.

Then why does the browser succeed? Inside the end-entity certificate there is a URL (the AIA extension) from which the issuer certificate can be downloaded, and Chrome and Safari download directly from that URL if they cannot find the issuer during verification. OpenSSL, on the other hand, does not follow this by default. curl, wget, Python requests, Go, and Node mostly fall under this.

So the following proposition holds. If it works in the browser but fails in curl, it is the server, not the client, that is wrong. The browser is merely covering for the server's mistake, and in exchange it pays an extra round trip cost on every first connection.

Checking takes one command. Connect with openssl s_client -connect 호스트:443 -servername 호스트 -showcerts (put the host name where the Korean word is) and count how many appear in the Certificate chain. If only one end-entity certificate appears, the cause is confirmed at that moment.

What it looks like in the field

The cause is almost determined by the error message.

What to check in the quiz that follows

Check which side to fix when the browser and curl results diverge, and what you can tell from the number of certificates in the chain.