How the TLS Handshake Actually Works
A practical walkthrough of the TLS handshake: ClientHello, ServerHello, certificate validation, key exchange, traffic keys, Finished messages, TLS 1.2 vs TLS 1.3, and common handshake failures.
- HTTPS
- TLS
- Security
- Networking
On this page
In the previous article, we looked at what TLS certificates are, how certificate chains are built, and how a client decides whether to trust a server.
The next question is what actually happens when that connection starts.
When an application calls an HTTPS endpoint such as:
https://api.payments.northstarbank.example/v1/accountsthe HTTP request is not simply sent across the network immediately.
Before application data can be sent safely, the client and server first need to establish a secure connection. During that setup, they need to agree on how the connection will be protected, the server needs to prove its identity, and both sides need to establish the cryptographic keys that will protect the traffic sent afterward.
That setup process is the TLS handshake.
If you landed on this article first, you can still follow the flow. But if terms such as root certificate, intermediate certificate, certificate chain, or trust store are unfamiliar, I would read How HTTPS and TLS Certificates Work first.
This article builds directly on those concepts rather than explaining them again from the beginning.
The main flow here is the normal certificate-authenticated full TLS 1.3 handshake. TLS 1.3 also supports PSK-based resumption modes, which we will touch on later, but separating those cases keeps the core handshake easier to reason about.
From Certificate Trust to the TLS Handshake
At the end of the previous article, we had a simplified trust chain like this:
Server
↓
Leaf Certificate
↓ signed by
Intermediate Certificate
↓ signed by
Root Certificate
↓
Already trusted by the client's trust storeThat explains why a client may trust a certificate.
But it does not explain when that certificate appears, how the client receives it, or what else is happening around certificate validation.
Those things happen during the TLS handshake.
A useful high-level view is:
Application wants an HTTPS connection
↓
TLS handshake begins
↓
Parameters negotiated and shared key material established
↓
Server authenticated
↓
Handshake verified
↓
TLS connection ready
↓
HTTP application data travels through TLSThe certificate is not the TLS connection itself.
It is one part of the process used to establish that connection.
This becomes important later when we look at SSL pinning. Pinning does not replace TLS. It adds another trust decision during server validation.
What Does the TLS Handshake Need to Achieve?
From the application's point of view, the goal may look simple:
const response = await fetch(
"https://api.payments.northstarbank.example/v1/accounts",
);The application wants a response.
Underneath that call, the TLS layer has several problems to solve first.
The client needs to know who it is talking to
Suppose the application intends to connect to:
api.payments.northstarbank.exampleReceiving a response from some server is not enough.
The client needs evidence that the server on the other side is actually authorized to represent that hostname.
That is where the certificate and certificate chain from the previous article come into the picture.
Both sides need encryption keys
The client and server also need keys that can protect the application traffic.
The public key inside the server certificate is not normally used to encrypt every HTTP request and response.
Modern TLS establishes shared key material during the handshake and derives symmetric traffic keys from it.
TLS handshake
↓
Shared cryptographic state
↓
Traffic keys
↓
Protected application trafficBoth sides need to agree on how the connection will work
TLS supports different protocol versions, cipher suites, signature algorithms, key-exchange groups, and extensions.
The client and server need to find a configuration they both support.
A modern client might support:
TLS 1.3
TLS 1.2If the server supports TLS 1.3 as well, they can continue using TLS 1.3.
That negotiation begins with ClientHello.
The Handshake Starts with ClientHello
When the client starts the TLS handshake, it sends a ClientHello.
Despite the name, this is not just a greeting.
It is closer to the client saying:
Here are the TLS capabilities I support, along with the information you need to continue establishing this connection.A simplified ClientHello looks like this:
Client
↓
ClientHello
┌────────────────────────────────────────────┐
│ Supported TLS versions │
│ • TLS 1.3 │
│ • TLS 1.2 │
│ │
│ Supported cipher suites │
│ Supported signature algorithms │
│ Client key share │
│ SNI: api.payments.northstarbank.example │
│ Other TLS extensions │
└────────────────────────────────────────────┘
↓
ServerEverything inside the box belongs to the ClientHello message. They are not separate handshake steps.
At this point, the server has not yet proven its identity.
The client is mainly describing what it supports and providing information needed to continue.
Why does ClientHello contain the hostname?
A single server, reverse proxy, CDN, or load balancer can serve several HTTPS domains from the same infrastructure.
For example:
api.payments.northstarbank.example
auth.northstarbank.example
accounts.northstarbank.examplemay eventually reach the same infrastructure.
The server therefore needs to know which hostname the client is trying to reach so it can select the appropriate TLS configuration and certificate.
This is commonly carried in the Server Name Indication, or SNI, extension.
Client
↓
ClientHello (contains the requested hostname through SNI)
↓
ServerThis happens before the normal HTTP request is sent.
The hostname is therefore already available to the TLS layer before application data such as this is transmitted:
GET /v1/accounts HTTP/1.1
Host: api.payments.northstarbank.exampleServerHello: What the Server Chooses
Once the server receives the ClientHello, it knows what the client supports.
It responds with a ServerHello.
The distinction is simple:
ClientHello offers possibilities.
ServerHello selects a compatible configuration.
Client
↓
ClientHello
┌────────────────────────────────────────────┐
│ TLS 1.3, TLS 1.2 │
│ Supported cipher suites │
│ Supported signature algorithms │
│ Client key share │
│ SNI: api.payments.northstarbank.example │
└────────────────────────────────────────────┘
↓
Server
↓
ServerHello
┌────────────────────────────────────────────┐
│ Selected TLS version │
│ Selected cipher suite │
│ Server key share │
└────────────────────────────────────────────┘If the client and server cannot agree on a compatible TLS version or cryptographic configuration, the handshake cannot continue.
In TLS 1.3, the key-share information exchanged in ClientHello and ServerHello is also enough for both sides to start deriving handshake secrets.
That means much of the remaining TLS 1.3 handshake can already be encrypted.
What Happens After ServerHello in TLS 1.3?
A simplified server-side TLS 1.3 sequence is:
ClientHello
↓
ServerHello
↓
Handshake keys derived
↓
EncryptedExtensions
↓
Certificate
↓
CertificateVerify
↓
FinishedEncryptedExtensions carries additional negotiated parameters that do not belong in ServerHello.
We do not need to inspect every extension here. The messages that matter most for understanding server authentication are Certificate, CertificateVerify, and Finished.
After validating the server's flight, the client sends its own Finished message. In the normal server-authenticated full handshake, that client Finished completes the handshake from the client's side before it sends ordinary application data.
Certificate: Who Is This Server?
The Certificate message carries the server's certificate chain.
For our API, the chain might conceptually look like:
api.payments.northstarbank.example
↓ signed by
Northstar TLS Intermediate CA
↓ signed by
Trusted Root CAAs discussed in Article 1, the trusted root normally does not need to be sent by the server because the client is expected to already have it in its trust store.
The server typically sends its leaf certificate and the intermediate certificates needed to construct the trust path.
This is where the trust model from Article 1 becomes part of the live TLS connection.
CertificateVerify: Does the Server Actually Have the Private Key?
Receiving the correct certificate is not enough by itself.
Certificates are public information.
Anyone connecting to an HTTPS server can obtain its public certificate.
So copying the certificate does not prove that someone also possesses the corresponding private key.
That is what CertificateVerify is for.
The server uses its private key to sign data derived from the handshake transcript.
The client verifies that signature using the public key associated with the certificate.
Server certificate (contains the public key)
↓
Server signs handshake data using its private key
↓
CertificateVerify
↓
Client verifies the signature using the public keyIf the signature verifies correctly, the client has cryptographic evidence that the server participating in the handshake possesses the private key associated with the certificate.
The distinction is useful:
Certificate (binds an identity to a public key)
CertificateVerify (proves possession of the matching private key)How the Client Validates the Server
The client also performs the normal certificate checks discussed in Article 1.
For a connection to:
https://api.payments.northstarbank.example/v1/accountsthose checks include things such as:
Certificate validity period
Hostname
Certificate chain
Trusted root
Permitted certificate usage
Acceptable signatures and algorithms
Platform-specific security policySuppose the server presents a certificate valid only for:
api.internal.northstarbank.exampleThe certificate may otherwise be valid.
It may be signed by a trusted CA.
It may still be within its validity period.
Its chain may verify correctly.
But it does not represent the hostname the application intended to reach.
Hostname validation should fail.
A simplified decision process is:
Certificate received
↓
Currently valid?
↓
Hostname matches?
↓
Certificate chain valid?
↓
Chain reaches a trusted root?
↓
Usage and algorithms acceptable?
↓
Certificate acceptedThis is also the general area where SSL pinning will later introduce another trust check.
Authentication and Key Agreement Solve Different Problems
It is easy to mix these two parts of TLS together.
They happen during the same handshake, but they solve different problems.
Certificate authentication (verifies who the server is)
Key agreement (establishes shared secret material)
Traffic keys (protect application traffic)The certificate does not provide the final encryption key for the connection.
Modern TLS uses a separate key-agreement process.
How Both Sides Establish Shared Key Material
In the certificate-authenticated TLS 1.3 handshake described here, the client and server use ephemeral Diffie-Hellman key agreement, commonly ECDHE.
We do not need the elliptic-curve mathematics here.
The important property is this:
the client and server can derive the same shared secret without transmitting that final secret across the network.
Each side generates temporary key material:
Client
┌──────────────────────────┐
│ Temporary private value │
│ Temporary public value │
└──────────────────────────┘
Server
┌──────────────────────────┐
│ Temporary private value │
│ Temporary public value │
└──────────────────────────┘Each endpoint keeps its private value secret and exchanges only the public value.
Conceptually:
Client private value + Server public value
↓
Shared secret
Server private value + Client public value
↓
Same shared secretThe shared secret itself is never sent across the network.
Someone observing the connection may see the public key-share values, but that does not give them the private values needed to derive the same secret.
Where the Key Share Actually Appears
This is one place where an overly simple TLS diagram can become misleading.
In TLS 1.3, key agreement is not usually a separate message that happens after the certificate.
The client normally includes its key share inside ClientHello.
The server returns its key share inside ServerHello.
So once those messages have been exchanged:
ClientHello
↓
ServerHello
↓
Handshake secrets derived
↓
Encrypted handshake messages continueThe key agreement has already started in the first exchange.
From the Shared Secret to Traffic Keys
The result of the ECDHE agreement is not simply taken and used as one encryption key for everything.
TLS runs the shared secret through a defined key schedule.
At a high level:
ECDHE shared secret
↓
TLS key derivation
↓
Handshake traffic keys
↓
Application traffic keysBoth sides perform the required calculations independently.
If the handshake has proceeded correctly, they arrive at compatible cryptographic state.
The traffic keys themselves do not need to be transmitted across the network.
That is worth keeping separate from the certificate public key and the ephemeral key-share values. They all involve keys, but they have different roles.
Why Ephemeral Keys Matter
The temporary nature of the key agreement gives modern TLS an important security property: forward secrecy.
Imagine an attacker records encrypted traffic today.
Later, perhaps months or years afterward, the server's long-term certificate private key is compromised.
With ephemeral ECDHE, possessing that long-term private key should not be enough to reconstruct the temporary secrets used for previously completed sessions.
Long-term certificate key (authenticates the server)
Ephemeral key agreement (establishes per-session secrets)
Session established
↓
Session ends
↓
Temporary session secrets discardedThe long-term certificate key proves identity.
The ephemeral key-agreement values help establish per-session secrets.
Those are different jobs.
Finished: Did We Both See the Same Handshake?
Near the end of the handshake, the endpoints exchange Finished messages.
By this point, connection parameters have been negotiated, key material has been established, the server has authenticated itself, certificate validation has taken place, and both sides have derived cryptographic state.
The Finished message verifies the integrity of the handshake.
Server Finished
↓
Client verifies server Finished
↓
Client Finished
↓
Server verifies client Finished
↓
Handshake completeIf important handshake data had been modified unnoticed in transit, the expected verification state would not match.
The handshake would fail.
So Finished is not simply the protocol equivalent of:
"Okay, I'm done."It is cryptographic confirmation that the endpoint reached the expected handshake state.
When Does the Actual API Request Get Sent?
Now we can return to the application call:
const response = await fetch(
"https://api.payments.northstarbank.example/v1/accounts",
);The code looks simple because the networking stack hides most of what happens underneath it.
For a new connection, a simplified flow is:
Application initiates HTTPS request
↓
DNS resolution
↓
Transport connection established
↓
TLS handshake
↓
TLS connection established
↓
HTTP application data sent through TLS
↓
Protected response returnedBy this point, we already know what happens inside the TLS handshake, so there is no need to expand ClientHello, ServerHello, certificate validation, and key agreement again.
The HTTP request may logically contain something like:
GET /v1/accounts HTTP/1.1
Host: api.payments.northstarbank.example
Authorization: Bearer ...but those HTTP bytes travel through the TLS-protected connection.
That is why saying HTTP request here does not mean the application has switched back to plain HTTP.
HTTPS is HTTP transported over TLS.
For HTTP/1.1 and HTTP/2, TLS commonly sits over TCP.
HTTP/3 works differently because it runs over QUIC, with TLS 1.3 integrated into QUIC's connection establishment.
That deserves its own discussion. For this article, it is enough to remember that:
TCP
↓
TLS
↓
HTTP/1.1 or HTTP/2is a useful model for many HTTPS connections, but not every modern one.
What Changed from TLS 1.2 to TLS 1.3?
TLS 1.3 is not simply TLS 1.2 with a different version number.
The handshake was significantly redesigned.
A simplified comparison is:
Simplified handshake comparison
TLS 1.2 TLS 1.3
ClientHello ClientHello (includes key share)
↓ ↓
ServerHello ServerHello (includes key share)
↓ ↓
Certificate Handshake keys derived
↓ ↓
Server key-exchange messages EncryptedExtensions
↓ ↓
Client key exchange Certificate
↓ ↓
Finished CertificateVerify
↓ ↓
Application traffic Finished
↓
Application trafficThis is deliberately simplified. The exact TLS 1.2 handshake varies by cipher suite; for example, some configurations include a ServerKeyExchange message, while RSA key exchange follows a different flow.
But this comparison still highlights the differences that matter most in practice.
TLS 1.3 usually needs fewer round trips
One of the main goals of TLS 1.3 was to reduce connection setup latency.
The client can provide its key share directly in ClientHello.
The server can return its key share in ServerHello.
That allows both sides to derive key material earlier.
Across a high-latency connection, saving a round trip can make a meaningful difference.
More of the TLS 1.3 handshake is encrypted
In TLS 1.3, once ServerHello has been processed and handshake traffic keys are derived, later handshake messages are protected.
That includes certificate-related messages sent afterward.
This is one reason TLS 1.3 traffic looks different from TLS 1.2 when inspecting packet captures.
TLS 1.3 removed static RSA key exchange
TLS 1.2 supported several key-exchange approaches.
Some TLS 1.2 configurations used RSA key exchange, where the server's long-term RSA key played a direct role in establishing the connection secret.
TLS 1.3 removed static RSA key exchange. In the certificate-authenticated full handshake described in this article, the client and server use ephemeral Diffie-Hellman key agreement, commonly ECDHE, which provides forward secrecy.
TLS 1.3 also supports PSK-based modes for resumption. A PSK-only handshake is a different case and does not add a fresh Diffie-Hellman exchange.
TLS 1.3 cipher suites are simpler
A TLS 1.2 cipher-suite name could describe several choices together:
Key exchange
Authentication
Bulk encryption
MAC / hashTLS 1.3 separates some of those decisions.
A TLS 1.3 cipher suite mainly identifies the authenticated-encryption algorithm and the hash used by the TLS key schedule.
Key exchange and authentication are negotiated separately.
Legacy cryptography was removed
Older TLS versions accumulated many cryptographic options over time.
TLS 1.3 deliberately removed a number of outdated mechanisms rather than carrying every historical combination forward.
From an engineering point of view, that reduces the number of obsolete configurations that can accidentally remain enabled.
Why TLS 1.2 Still Matters
TLS 1.2 is still something engineers encounter in production.
A real environment may contain older operating systems, legacy enterprise software, embedded devices, reverse proxies, old middleware, or third-party integrations that do not support TLS 1.3.
A server may therefore support both:
TLS 1.3
TLS 1.2A modern client can negotiate TLS 1.3 while another compatible client may still use TLS 1.2.
That is why both versions still appear in server configuration, logs, security scans, and debugging tools.
The useful takeaway is that TLS 1.2 and TLS 1.3 can behave differently during connection setup, and you may need to understand both when debugging a real system.
Where a TLS Handshake Can Fail
A TLS handshake is not guaranteed to succeed just because the server is reachable.
Several things can fail before the application request reaches the API.
Application starts HTTPS request
↓
Network connection succeeds
↓
TLS handshake begins
↓
Handshake fails
↓
Application receives a TLS / connection errorCommon causes include incompatible TLS versions, certificate problems, trust failures, and cryptographic verification failures.
No compatible TLS version
Imagine an old server supports only:
TLS 1.0
TLS 1.1while the client permits only:
TLS 1.2
TLS 1.3There is no compatible protocol version.
The handshake cannot continue.
Expired certificate
A server may still be online and reachable while its certificate has expired.
For example:
Host:
api.payments.northstarbank.example
Certificate validity:
2025-09-01 → 2026-09-01
Current date:
2026-09-06DNS can still work.
The transport connection can still succeed.
The server process can still be healthy.
TLS certificate validation should still reject the connection.
Hostname mismatch
Suppose the application requests:
api.payments.northstarbank.examplebut receives a certificate valid only for:
api.internal.northstarbank.exampleEven if the certificate is otherwise valid and chains to a trusted CA, it does not represent the hostname the client requested.
The handshake should fail.
Broken certificate chain
A server can also be misconfigured and fail to provide an intermediate certificate needed to construct the trust path.
Leaf Certificate
↓ signed by
Intermediate CA
↓ signed by
Trusted Root CAIf the client cannot build the required chain, validation can fail even though the leaf certificate itself looks correct.
Untrusted certificate
Development and internal environments sometimes use self-signed certificates or certificates issued by private Certificate Authorities.
That may be intentional.
But if the client's trust store does not trust that CA, the certificate is not trusted by that client.
CertificateVerify or Finished verification fails
TLS also verifies cryptographic proofs inside the handshake.
If CertificateVerify fails, the server has not successfully demonstrated possession of the expected private key.
If Finished verification fails, the handshake state is not what the endpoint expects.
Either failure terminates the connection.
The Failure May Happen Before Your API Sees Anything
This is one of the most useful points to remember when debugging HTTPS.
Suppose the application executes:
await fetch(
"https://api.payments.northstarbank.example/v1/accounts",
);and TLS fails.
The request may never reach:
API gateway
Controller
Authentication middleware
Authorization logic
Business logicbecause the failure happened before HTTP application data was accepted.
So when an HTTPS request fails, it helps to separate two questions:
Did the network connection fail?
Did TLS reject the connection before HTTP started?
That distinction becomes even more important once pinning is added, because pinning introduces another possible rejection point during server validation.
A Full TLS Handshake Does Not Happen for Every API Call
So far, we have talked about establishing a new connection.
That does not mean every API request performs the entire handshake again.
An application may make several requests to the same server:
GET /v1/accounts
GET /v1/transactions
POST /v1/transfers
GET /v1/profileNetworking stacks try to reuse established connections where possible.
First request
↓
Transport connection established
↓
TLS handshake
↓
Secure connection ready
↓
Multiple HTTP requests reuse the connectionHTTP/2 makes this especially useful because multiple HTTP streams can share a single connection.
So ten API calls to the same origin do not necessarily mean ten TLS handshakes occurred.
Whether a connection is reused depends on things such as connection pooling, keep-alive behavior, HTTP version, idle timeouts, server configuration, and network changes.
What Happens After the Connection Closes?
Eventually, an established connection disappears.
The application may have been idle.
The server may close it.
The device may move between networks.
The connection pool may discard it.
When the client connects to the same server again, TLS can perform another full handshake.
But TLS also supports session resumption.
The basic idea is:
We connected securely before. Can we establish another secure connection without repeating all of the expensive work?
In TLS 1.3, resumption uses previously established keying material through pre-shared-key mechanisms, commonly together with session tickets supplied by the server.
Initial connection
↓
Full TLS handshake
↓
Secure session established
↓
Resumption information provided
↓
Connection closes
Later...
Client reconnects
↓
Previous resumption state used
↓
Abbreviated handshake
↓
Secure connection established fasterThe protocol details are more involved than that, but the practical point is simple:
TLS does not always need to repeat all the work of a completely fresh handshake.
What About TLS 1.3 0-RTT?
TLS 1.3 can also support 0-RTT early data for some resumed connections.
It can allow the client to send application data before the resumed handshake has fully completed.
Previous secure session exists
↓
Client has valid resumption state
↓
Client reconnects
↓
Early application data may be sentThat can reduce latency, but it comes with an important tradeoff.
0-RTT data has replay risks.
An operation such as:
POST /v1/transferstherefore needs very different consideration from an operation whose application semantics are explicitly designed to tolerate replay.
0-RTT is an optimization for certain resumed TLS 1.3 sessions, not the normal shape of every HTTPS request.
Putting the TLS 1.3 Handshake Together
By this point, we do not need to expand every message again.
A useful end-to-end view is:
Application starts HTTPS request
↓
DNS resolution
↓
Transport connection established
↓
ClientHello
↓
ServerHello
↓
Handshake secrets derived
↓
EncryptedExtensions
↓
Certificate
↓
Certificate validation
↓
CertificateVerify
↓
Server Finished
↓
Client Finished
↓
TLS handshake complete
↓
HTTP application data travels through TLSThat is the mental model I would keep.
ClientHello starts negotiation and carries the client's initial TLS capabilities and key share.
ServerHello selects the connection parameters and returns the server's key share.
The certificate identifies the server.
CertificateVerify proves possession of the matching private key.
Certificate validation determines whether the client trusts that identity.
The key-agreement process establishes shared secret material.
The server and client Finished messages verify that both sides reached the expected cryptographic handshake state.
After the client sends its Finished message, normal client application traffic can use the protected connection.
A Practical Way to Remember the Handshake
If you forget the individual TLS messages later, remember the problems being solved:
Negotiate and establish shared key material (ClientHello + ServerHello)
↓
Authenticate the server (Certificate + CertificateVerify)
↓
Verify the handshake (Finished messages)
↓
Protected application trafficThat mental model is more useful than memorizing a list of TLS message names without understanding why each one exists.
Where SSL Pinning Fits Next
At this point, normal TLS already gives us a lot.
The client can validate the server's certificate, verify its chain, confirm the hostname, establish shared traffic keys, verify the handshake, and protect application traffic.
So the next question is an obvious one:
If TLS already validates the server certificate, why would an application need SSL pinning?
The answer is not that normal TLS is broken.
Pinning changes the trust decision.
With standard certificate validation, the client accepts a server certificate when it satisfies the normal validation rules and builds a valid trust path to a trusted Certificate Authority.
A pinned application adds another restriction.
Normal TLS validation (is the certificate valid and trusted?)
↓
Certificate accepted
↓
Pin validation (is this an identity the app specifically expects?)
↓
Continue or rejectThat extra restriction can protect against situations where normal CA-based trust would otherwise accept a certificate that the application owner did not intend to trust.
But that additional control also creates operational risk.
Certificates rotate.
Keys change.
Backup pins matter.
A bad pinning configuration can block every legitimate connection to the backend.
That is why understanding the normal TLS handshake first matters.
In the next article, we will look at what SSL pinning actually changes, where that extra check happens, what it protects against, and why mobile applications are one of the places where pinning is commonly considered.