← Week 2: TLS 1.3 Handshake

Day 10: The TLS 1.3 Key Schedule

Phase 1 · May 23, 2026

← Week 2: TLS 1.3 Handshake

Agenda (2–3 hours)

  • Read (60 min): RFC 8446 §7.1 (Key Schedule), §7.3 (Traffic Key Calculation)
  • Study (60 min): Full key schedule diagram; trace through with RFC 8448 test vectors
  • Challenge (60 min): Implement and verify the full key schedule in Rust
← Week 2: TLS 1.3 Handshake

Full Key Schedule (RFC 8446 §7.1)

0 (or PSK)
│
├─ HKDF-Extract(salt=0, IKM=PSK) → Early Secret (ES)
│      ├─ Derive-Secret(ES, "ext binder" | "res binder", "") → binder keys
│      ├─ Derive-Secret(ES, "c e traffic", ClientHello) → client_early_traffic_secret
│      └─ Derive-Secret(ES, "e exp master", ClientHello) → early_exporter_master_secret
│
├─ Derive-Secret(ES, "derived", "") → ES_derived
├─ HKDF-Extract(salt=ES_derived, IKM=DHE) → Handshake Secret (HS)
│      ├─ Derive-Secret(HS, "c hs traffic", CH..SH) → client_hs_traffic_secret
│      └─ Derive-Secret(HS, "s hs traffic", CH..SH) → server_hs_traffic_secret
│
├─ Derive-Secret(HS, "derived", "") → HS_derived
└─ HKDF-Extract(salt=HS_derived, IKM=0) → Master Secret (MS)
       ├─ Derive-Secret(MS, "c ap traffic", CH..SF) → client_app_traffic_secret_0
       ├─ Derive-Secret(MS, "s ap traffic", CH..SF) → server_app_traffic_secret_0
       ├─ Derive-Secret(MS, "exp master",   CH..SF) → exporter_master_secret
       └─ Derive-Secret(MS, "res master",   CH..CF) → resumption_master_secret
← Week 2: TLS 1.3 Handshake

Traffic Key Calculation (§7.3)

From a traffic secret, derive actual encryption keys:

[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
[sender]_write_iv  = HKDF-Expand-Label(Secret, "iv",  "", iv_length)

Per-record nonce:

nonce = write_iv XOR (padded sequence_number)

Sequence number increments with each record. Nonce uniqueness is guaranteed
as long as sequence numbers don't wrap (which TLS forbids — key update first).

← Week 2: TLS 1.3 Handshake

Transcript Hash

Derive-Secret(Secret, Label, Messages) uses the hash of all handshake messages
sent so far
as the "Messages" input.

This means:

  • Keys are cryptographically bound to the specific messages exchanged
  • An attacker can't replay messages from one session to another
  • A Finished message is unforgeable without knowing the traffic secret

The transcript includes ClientHello, ServerHello, EncryptedExtensions, Certificate,
CertificateVerify — up to whatever point the label specifies.

← Week 2: TLS 1.3 Handshake

Key Update (Post-Handshake)

Application traffic secrets can be updated in-flight:

application_traffic_secret_N+1 = HKDF-Expand-Label(
    application_traffic_secret_N, "traffic upd", "", Hash.length)

Triggered by a KeyUpdate handshake message. Either side can initiate.
Used for long-lived connections to limit the amount of data encrypted under one key.

← Week 2: TLS 1.3 Handshake

Challenge Assignment

Implement the complete TLS 1.3 key schedule in Rust for the no-PSK (DHE-only) case.

Using RFC 8448 §3 test vectors:

  1. Verify Early Secret
  2. Verify client_hs_traffic_secret and server_hs_traffic_secret
  3. Verify client_hs_write_key and client_hs_write_iv
  4. Verify client_app_traffic_secret_0 and server_app_traffic_secret_0

Your implementation should be a set of pure functions, no network code.
This is the foundation for understanding every key in a TLS session.

← Week 2: TLS 1.3 Handshake

Resources

  • RFC 8446 §7.1, §7.3: Key Schedule and Traffic Keys
  • RFC 8448 §3: Test vectors (x25519, AES-128-GCM-SHA256)
  • hkdf, sha2, hmac crates