What actually happens during a TLS handshake, and why does certificate validation fail silently in ways that are hard to debug?
Quick Answer
A TLS handshake is a negotiation where the client and server agree on a protocol version and cipher suite, the server proves its identity by presenting a certificate signed by a trusted authority, and both sides derive a shared symmetric encryption key without ever transmitting that key directly, using asymmetric key exchange. Validation failures are hard to debug because they can happen at many independent checks — hostname mismatch, expired certificate, untrusted chain, revoked certificate, unsupported cipher — and most client libraries collapse all of these into one generic error message rather than reporting which specific check actually failed.
Detailed Answer
Think about a high-security video call between two strangers who need to agree on which video conferencing app and quality settings to use, have one person show government-issued ID to a witness the other side trusts proving they are who they claim to be, and agree on a private code word for the rest of the conversation that nobody eavesdropping on the call setup could have figured out, even though the whole setup conversation happened in the open. All of that negotiation happens before either person actually says anything meaningful to the other.
TLS was designed to accomplish exactly this over a network connection that might be tapped by anyone: negotiate shared parameters, prove identity, and establish a secret key, all before any application data like an HTTP request is exchanged. The reason it's built as a multi-step negotiation rather than one direct message is that clients and servers may support different protocol versions and cryptographic algorithms, and the identity and trust piece, certificates, needs to be independently verifiable by the client without any prior direct relationship with the server, which is what makes HTTPS work reliably between total strangers on the internet.
The handshake proceeds: the client sends a ClientHello, listing supported TLS versions, cipher suites, and critically the SNI, Server Name Indication, telling the server which hostname it's trying to reach, since one IP address can host many TLS certificates. The server responds with a ServerHello, its chosen version and cipher, plus its certificate chain. The client then validates that chain, checking the signature path up to a trusted root, checking the certificate's notBefore and notAfter validity window, checking the certificate's subject or SAN matches the requested hostname, and optionally checking revocation status via OCSP or CRL. Both sides then perform a key exchange, commonly ECDHE, elliptic-curve Diffie-Hellman, which lets both sides compute the same shared secret without ever transmitting it, and send Finished messages confirming the handshake's integrity before any actual HTTP data flows across the connection.
In production, TLS handshake failures show up as elevated connection error rates on a service like checkout-worker calling an external payment gateway, and the operational challenge is that failure can originate from any single check in that validation chain: an expired certificate, a hostname that doesn't match, common after a service is renamed or a load balancer's certificate doesn't cover a newly added domain, an untrusted or incomplete chain, a revoked certificate, or simply no overlapping cipher suite between an old client and a server that's dropped support for outdated ciphers for security reasons.
The non-obvious gotcha: many HTTP client libraries and language runtimes report all of these distinct failure modes through one generic exception type, something like SSL handshake failed or certificate verify failed, without surfacing which specific check tripped. This means the fastest real debugging path is almost always bypassing the application entirely and running openssl s_client -connect host:443 -servername host directly, which prints the actual certificate details, chain, and specific validation error, rather than trying to interpret a vague error bubbling up from deep inside an application's HTTP library.