⏱️ Lectura: 12 min
Every time you open a page over HTTPS, your browser and the server negotiate an encrypted connection in a fraction of a second: that’s the TLS 1.3 handshake, and since 2018 it has done that job with half the network round trips its predecessor needed. Previously, TLS 1.2 needed two full round trips just to agree on keys; TLS 1.3 resolves it in one, and on reconnections it can start at zero.
📑 En este artículo
- TL;DR
- What the TLS 1.3 handshake is and why it matters
- How the TLS 1.3 handshake works in detail
- Practical examples
- How to enable TLS 1.3 in production
- Real use cases
- Common mistakes and best practices
- Comparison: TLS 1.2 vs TLS 1.3 vs mTLS vs QUIC
- Going deeper: the key schedule and the record layer
- Frequently Asked Questions
- References
The protocol, standardized in RFC 8446, isn’t a minor tweak to TLS 1.2: it rewrote the handshake from scratch. It removed static RSA key exchange, dropped cipher suites with known vulnerabilities (RC4, 3DES, unauthenticated CBC), and reduced the full handshake from two round trips to just one.
TL;DR
- You’ll understand why TLS 1.3 uses a single round trip (1-RTT) instead of the two required by TLS 1.2.
- You’ll be able to inspect with openssl s_client which version and cipher any HTTPS server uses.
- You’ll know how to configure 0-RTT in Nginx and why it should be limited to idempotent requests.
- You’ll be able to distinguish ECDHE from RSA as a key exchange method and why only ECDHE provides forward secrecy.
- You’ll be able to generate a keylog from your browser and decrypt TLS 1.3 traffic in Wireshark.
- You’ll learn the most common mistakes when migrating from TLS 1.2 to 1.3 in production.
What the TLS 1.3 handshake is and why it matters
TLS (Transport Layer Security) is the protocol that encrypts almost all HTTPS traffic on the internet: without it, anyone intercepting the cable or the wifi access point could read passwords, session cookies, and card numbers in plain text. The TLS 1.3 handshake is the initial phase of that connection, the exchange of messages where client and server agree on which cipher to use and derive the symmetric keys that will protect the rest of the session.
The protocol’s history traces back to SSL in the 90s; TLS 1.0 and 1.1 were formally deprecated, and modern browsers today don’t even offer them as an option anymore. TLS 1.3 is the version that runs by default in Chrome, Firefox, Safari, and Edge since 2018-2020.
Why this matters in practice: every round trip that the TLS 1.3 handshake saves translates into real latency for the user, especially on mobile networks with 100-200ms of RTT to the server. And by forcing every key exchange to use ephemeral Diffie-Hellman (ECDHE), TLS 1.3 guarantees forward secrecy: even if someone steals the server’s private key next year, they can’t decrypt traffic captured today, because the session secret never traveled over the network and isn’t derived solely from that private key.
How the TLS 1.3 handshake works in detail
The handshake starts when the client (your browser, or a curl request) sends a ClientHello message. Unlike TLS 1.2, this message already includes a bet: the key_share extension with an ephemeral public key for one or more Diffie-Hellman groups, typically x25519 or secp256r1. The client guesses which group the server will accept and sends the key right away, without waiting for prior confirmation.
If the server supports that group, it responds with a single combined message: ServerHello (with its own key_share), EncryptedExtensions, Certificate, CertificateVerify, and Finished. With the server’s public key and its own, both sides compute the same shared secret using Diffie-Hellman, without that secret ever traveling over the network. From there, an HKDF function derives the symmetric session keys.
The client validates the certificate, computes its own Finished, and in the same packet can already send the first encrypted HTTP request. That’s 1-RTT: one trip out (ClientHello) and one back (server response) before application data starts flowing.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello + key_share (ECDHE)
S-->>C: ServerHello + key_share + Certificate + Finished
Note over C,S: both derive session keys with HKDF
C->>S: Finished + encrypted application data
Note over C,S: connection established in 1 round trip
When the client has already connected to the same server before and saved a session ticket, it can skip the initial ECDHE exchange and send encrypted application data in the same ClientHello packet: that’s 0-RTT. It’s fast, but it brings a real security problem that we cover in the common mistakes section.
Practical examples
Before touching a server’s configuration, it helps to see the TLS 1.3 handshake working live. These examples go from the simplest inspection to the most complete one.
Inspecting the handshake with openssl
The most direct command to confirm which version a server negotiated is openssl s_client:
openssl s_client -connect example.com:443 -tls1_3 -brief
The output shows a line Protocol version: TLSv1.3 and the negotiated cipher, for example TLS_AES_128_GCM_SHA256. If the server doesn’t support TLS 1.3, the connection fails outright instead of silently downgrading, which is useful for detecting outdated configurations.
Confirming the version from curl
curl -v --tlsv1.3 https://example.com/ 2>&1 | grep "SSL connection"
With curl 7.52 or later, that line prints something like SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384. It’s the fastest check for a monitoring script to validate that no endpoint fell back to TLS 1.2.
Forcing TLS 1.3 on a Node.js server
const https = require('node:https');
const fs = require('node:fs');
const server = https.createServer({
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem'),
minVersion: 'TLSv1.3',
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('encrypted connection using TLS 1.3');
});
server.listen(443);
The minVersion: 'TLSv1.3' field makes Node reject any client that tries to negotiate TLS 1.2 or earlier, instead of silently accepting it. It’s the simplest way to audit, in a staging environment, which old clients still depend on insecure versions.
Decrypting TLS 1.3 traffic in Wireshark
To debug a real handshake, Chrome and Firefox can dump session keys to a file if the SSLKEYLOGFILE environment variable exists:
export SSLKEYLOGFILE=$HOME/tls-keys.log
google-chrome https://example.com
Then, in Wireshark: Edit > Preferences > Protocols > TLS > (Pre)-Master-Secret log filename, pointing to that file. Wireshark automatically decodes the encrypted packets and shows the ClientHello, the key_share, and the application data in plain text, useful for confirming that an API is really using the cipher you think it is.
How to enable TLS 1.3 in production
These steps apply to an Nginx server with a certificate already issued, for example with Let’s Encrypt:
- Confirm the installed OpenSSL version: TLS 1.3 requires OpenSSL 1.1.1 or later, with
openssl version. - In Nginx’s
serverblock, edit the protocols directive:ssl_protocols TLSv1.2 TLSv1.3;(keep 1.2 as a fallback while you migrate old clients). - Set the preferred key exchange groups:
ssl_ecdh_curve X25519:prime256v1;. - If you want to allow 0-RTT only on idempotent endpoints (GET, never on a payment POST):
ssl_early_data on;. - Reload the configuration:
nginx -t && systemctl reload nginx. - Verify with the openssl command from the previous section, pointing to the real domain.
💡 Tip: keep TLS 1.2 active for a few more weeks and check the access logs for the $ssl_protocol variable in Nginx: if 1.2 connections no longer show up, you can disable it completely.
Real use cases
Saving a round trip in the TLS 1.3 handshake matters more the farther the client is from the server. In an e-commerce checkout with users on mobile networks with 150ms of latency, every new connection saves that second round trip that TLS 1.2 used to cost, before any data even starts loading.
Mobile APIs with frequent reconnections (apps moving in and out of the background) benefit from 0-RTT mode to resume a session without waiting for the full handshake, as long as the first request is read-only.
QUIC, the transport protocol behind HTTP/3, doesn’t use TLS as a separate layer: it embeds the TLS 1.3 handshake directly into its connection negotiation, so everything you learn here also applies to how an HTTP/3 connection starts over UDP.
Common mistakes and best practices
The most commonly cited mistake in security forums is enabling 0-RTT without filtering which requests it can receive. Since a ClientHello carrying 0-RTT data can be resent by an attacker who captured the packet (a replay), any non-idempotent endpoint that receives that first packet risks executing twice. RFC 8446 explicitly warns about this in its security considerations section.
⚠️ Watch out: never process a state-changing request with 0-RTT data (payments, password changes, any non-idempotent POST): an attacker can replay the same captured packet and trigger the action twice.
Another frequent problem is clock skew between client and server: TLS 1.3 session tickets include an obfuscated timestamp, and if the server’s clock is out of sync, it can reject valid tickets and force unnecessary full handshakes on every reconnection.
Old middleboxes (corporate proxies, firewalls, some load balancers) sometimes don’t recognize the new TLS 1.3 extensions and cut the connection. That’s why the standard includes a compatibility mode: the server can mimic TLS 1.2 fields in its messages so these devices don’t block the traffic.
📌 Note: if you migrate an internal API to mTLS over TLS 1.3, rotate client certificates with the same discipline as server ones: an expired client certificate breaks the connection just like an expired server certificate does.
Comparison: TLS 1.2 vs TLS 1.3 vs mTLS vs QUIC
Not every variant fits the same scenario. This table summarizes when each one makes sense:
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| TLS 1.2 | Legacy clients that don’t support 1.3 (some IoT devices, very old browsers) | Broad compatibility | 2 round trips, allows cipher suites without forward secrecy if misconfigured |
| TLS 1.3 | Any new web service or public API | 1-RTT, mandatory forward secrecy, smaller attack surface | 0-RTT requires filtering non-idempotent requests |
| mTLS (mutual TLS 1.3) | Service-to-service communication within an internal network or service mesh | Authenticates the client in addition to the server | Requires distributing and rotating client certificates |
| QUIC (HTTP/3) | Apps with unstable connections or heavy packet loss | TLS 1.3 handshake built into the transport, no head-of-line blocking | Blocked by some firewalls that only allow TCP/443 |
Going deeper: the key schedule and the record layer
Beneath the visible handshake, TLS 1.3 derives its keys through a chain of calls to HKDF-Extract and HKDF-Expand known as the key schedule. It starts with an Early Secret derived from a constant value (or from the Pre-Shared Key if there’s session resumption), continues with the Handshake Secret once the ECDHE secret is known, and ends at the Master Secret, from which the application traffic keys are derived.
flowchart TD
A["ECDHE shared secret"] --> B["Handshake Secret (HKDF-Extract)"]
B --> C["Handshake keys (HKDF-Expand)"]
B --> D["Master Secret (HKDF-Extract)"]
D --> E["Application traffic keys"]
subgraph "TLS 1.3 key derivation"
A
B
C
D
E
end
Every application message travels encrypted with AEAD (combined authentication and encryption), typically AES-128-GCM, AES-256-GCM, or ChaCha20-Poly1305 on devices without hardware acceleration for AES. Unlike TLS 1.2, CBC modes no longer exist in the standard: every encrypted record includes its own authentication tag, so an attacker can’t alter a single byte without the connection detecting it and cutting it off.
For session resumption, TLS 1.3 offers two Pre-Shared Key modes: psk_ke, which reuses the previous secret without a new ECDHE exchange (faster, but without new forward secrecy), and psk_dhe_ke, which combines the ticket with a new ECDHE exchange to preserve forward secrecy even on reconnection. Most browsers prefer psk_dhe_ke by default.
You can confirm which cipher a real connection ended up using with openssl s_client -connect example.com:443 -tls1_3 | grep Cipher, or by inspecting the socket in Node with tlsSocket.getCipher(), which returns something like { name: 'TLS_AES_256_GCM_SHA384', version: 'TLSv1.3' }.
📖 Summary on Telegram: View summary
Your next step: run openssl s_client -connect your-domain.com:443 -tls1_3 -brief against your own server right now and confirm whether it’s already negotiating TLS 1.3 or still falling back to 1.2.
Frequently Asked Questions
Do I need to change my certificate to switch to TLS 1.3?
No. The X.509 certificate is the same for TLS 1.2 and 1.3; what changes is the negotiation protocol and the cipher suites, not the server’s cryptographic identity.
Which browsers support TLS 1.3?
Chrome, Firefox, Safari, and Edge support it by default since 2018-2020; the real risk lies in old embedded clients, outdated mobile SDKs, or corporate middleboxes.
Why doesn’t TLS 1.3 allow RSA for key exchange?
With static RSA, if someone captures encrypted traffic today and steals the server’s private key in the future, they can decrypt everything they captured. ECDHE generates a different ephemeral key on every connection, so that retroactive risk doesn’t exist.
Is 0-RTT inherently insecure?
It isn’t insecure by itself, but it should only be used for idempotent requests. RFC 8446 documents the replay risk and leaves the mitigation (limiting it to GET, using single-use tickets) up to the server.
How does TLS 1.3 relate to HTTP/3 and QUIC?
HTTP/3 runs over QUIC, and QUIC embeds the TLS 1.3 handshake within its own connection establishment: there’s no separate TLS layer on top of TCP, everything happens in the same negotiation over UDP.
References
- RFC 8446: the IETF’s official TLS 1.3 specification.
- MDN Web Docs: reference guide on TLS and its role in web security.
- OpenSSL Documentation: reference for commands like
s_clientused in this article. - Wikipedia: Transport Layer Security: history and evolution of the protocol from SSL to TLS 1.3.
📱 Do you like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
0 Comments