TLS 1.3 Handshake and Shared Traffic Keys
TLS 1.3 uses an ephemeral Diffie–Hellman exchange to create a fresh shared secret, then uses a hash of the handshake transcript to derive traffic keys and confirm that both sides saw the same messages. The result is an encrypted connection with forward secrecy and built-in detection of handshake tampering.
The problem it solves
Before this exchange, a client and server have no shared symmetric key. Symmetric encryption is fast, but it requires both parties to already possess the same secret. Public-key certificates can authenticate a server, but using a certificate key directly to encrypt all application traffic is inefficient and, if that key is later stolen, can expose recorded sessions.
TLS 1.3 separates these jobs:
- Ephemeral Diffie–Hellman creates a new shared secret for this connection. “Ephemeral” means the corresponding private values are temporary and discarded afterward.
- Certificates and signatures authenticate the server, when certificate authentication is being used.
- Transcript hashing records the exact handshake messages both sides processed.
- Finished messages provide key confirmation: each side proves it derived the expected secrets and saw the same transcript.
The combination means that an attacker who modifies, removes, reorders, or injects handshake messages cannot silently cause the endpoints to use different parameters or keys.
1. The client offers a key exchange
The client starts with a ClientHello. Among other fields, it includes:
supported_versions, identifying TLS 1.3 as a version it can use;- a list of supported cryptographic groups, such as elliptic-curve groups;
- one or more key shares, which contain the client’s ephemeral Diffie–Hellman public keys for selected groups;
- random bytes and other negotiation parameters.
For an elliptic-curve group, the client generates a private value a and computes a public value A. It sends A, not a.
The server chooses a compatible group and sends a ServerHello containing its own ephemeral public key. If its private value is b, its public value is B. Both sides can now calculate the same Diffie–Hellman result:
client: shared = DH(a, B)
server: shared = DH(b, A)
An observer sees A and B, but is not expected to be able to calculate shared. The private values a and b are not sent over the network.
The exact operation depends on the selected group, but the important property is that both endpoints independently compute the same result. TLS 1.3 feeds that result into a key schedule rather than using it directly as an encryption key.
If the server does not have a suitable key share, it can send a HelloRetryRequest, asking the client for a different group. The retry itself is included in the transcript hash, so it cannot be treated as an invisible negotiation step.
2. The key schedule turns secrets into separate keys
TLS 1.3 uses HKDF, a hash-based key-derivation construction, to turn the Diffie–Hellman result and other inputs into several independent secrets. The schedule has stages for early data, handshake traffic, and application traffic.
In the simplest certificate-authenticated exchange without a pre-shared key, the early secret starts from an empty input. The Diffie–Hellman result is then mixed into the schedule to produce a handshake secret. From that secret, TLS derives separate client and server handshake traffic secrets.
Each traffic secret is expanded into an encryption key and an initialization vector. The client-to-server and server-to-client directions use different values, so compromising one direction’s state does not mean both directions use the same key.
TLS 1.3 uses labels such as c hs traffic, s hs traffic, c ap traffic, and s ap traffic when expanding secrets. These labels are domain separation: they ensure that a value intended for one purpose cannot accidentally become the key for another purpose.
The key schedule also incorporates a hash of the handshake messages seen so far. This connects each derived secret to the exact negotiation that produced it.
3. The transcript hash binds the negotiation
A transcript hash is a running hash of the serialized TLS handshake messages, in order. Conceptually, it is similar to hashing a log file after every new record is appended:
transcript_hash = Hash(ClientHello || ServerHello || ...)
The real protocol handles the retry case and message encoding precisely, but the essential idea is this ordered record of what the endpoints processed.
The transcript includes security-relevant choices such as the selected protocol version, cipher suite, key-exchange group, and extensions. If an attacker changes one of those messages in transit, the peers’ transcript hashes diverge.
With certificate authentication, the server sends a Certificate and a CertificateVerify. CertificateVerify is a digital signature over the handshake transcript so far. The client verifies that signature using the public key in the certificate. This proves that the party controlling the certificate’s private key participated in this particular handshake, rather than merely presenting a certificate copied from somewhere else.
Diffie–Hellman alone does not authenticate the server: a man-in-the-middle could create one exchange with the client and another with the server. The certificate signature, together with the transcript, prevents that substitution when certificate validation succeeds.
4. Finished confirms the keys and transcript
After the encrypted handshake messages, each endpoint sends a Finished message. This is the key-confirmation step.
The sender derives a finished key from its handshake traffic secret, then computes a MAC— a keyed integrity check—over the transcript hash. The receiver independently derives the expected finished key and verifies the MAC.
In simplified form:
verify_data = MAC(finished_key, transcript_hash)
The actual TLS 1.3 construction uses the negotiated hash and precisely specified HKDF labels, but this model captures the purpose. A valid Finished proves that the sender possesses the relevant derived secret and agrees on the handshake transcript.
If a network attacker altered the ServerHello, certificate, key share, or any earlier handshake message, the transcript hash would change. The attacker cannot recompute a valid Finished value without the derived secret. Verification fails, and the connection is aborted instead of proceeding with apparently valid encryption.
Once both Finished messages are accepted, the key schedule derives application traffic secrets from the transcript and handshake state. Those secrets protect HTTP data and are normally rotated with TLS record limits or key updates.
Downgrade detection
A downgrade attack tries to make capable endpoints use an older, weaker TLS version. TLS 1.3 addresses this in two connected ways.
First, version negotiation uses the supported_versions extension rather than relying only on the older legacy version field. The selected version appears in the ServerHello, which is covered by the transcript and therefore by Finished verification.
Second, when a TLS 1.3-capable server deliberately negotiates TLS 1.2, it places a defined marker in the last bytes of its server random value. A TLS 1.3-capable client that receives such a marker knows that an intermediary may have suppressed TLS 1.3 negotiation and can reject the connection. The marker is not a secret and is not the cryptographic proof by itself; it is a signal for detecting this specific fallback situation.
Together, ephemeral Diffie–Hellman supplies fresh shared material, the transcript hash binds that material to the negotiation, and Finished messages confirm both the secrets and the message history. That is why the TLS error often seen in logs—such as a bad_record_mac, decrypt_error, or handshake_failure—can indicate not just bad encryption data, but a failed consistency or tampering check during connection setup.