Step 1 of 5 · Reading · ~2 min
Read
TLS 1.3 Handshake
The Key Schedule Tree
TLS 1.3 derives roughly a dozen secrets from two inputs: an optional PSK and a Diffie-Hellman shared secret. RFC 8446 §7.1 lays this out as a tree of HKDF-Extract and Derive-Secret calls. You will build the whole tree in this lesson.
The diagram
0
|
v
PSK -> HKDF-Extract = Early Secret
|
+-> Derive-Secret(., "ext binder" | "res binder", "")
+-> Derive-Secret(., "c e traffic", ClientHello)
+-> Derive-Secret(., "e exp master", ClientHello)
v
Derive-Secret(., "derived", "")
|
v
DHE -> HKDF-Extract = Handshake Secret
|
+-> Derive-Secret(., "c hs traffic", CH..SH)
+-> Derive-Secret(., "s hs traffic", CH..SH)
v
Derive-Secret(., "derived", "")
|
v
0 -> HKDF-Extract = Master Secret
|
+-> Derive-Secret(., "c ap traffic", CH..server Finished)
+-> Derive-Secret(., "s ap traffic", CH..server Finished)
+-> Derive-Secret(., "exp master", CH..server Finished)
+-> Derive-Secret(., "res master", CH..client Finished)
Three "salt" extractions (Early, Handshake, Master) with the previous level's "derived" output as salt. Each level then sprouts traffic / exporter / resumption branches.
Why the tree shape
If two unrelated keys come from the same Extract, compromise of one cannot reveal the other — they go through independent Expand-Label calls with different labels and different transcript-hash contexts.
If a level changes (you mix in DHE, or you mix in a fresh 0 in front of the Master Secret), then ALL downstream secrets change, even though the upstream parent is unchanged. This is what gives application traffic forward secrecy relative to handshake traffic.
Reduction to code
Then each traffic secret is fanned out into a (key, iv, finished_key) triple via HKDF-Expand-Label with labels "key", "iv", "finished". From one DHE output you get the whole protocol's symmetric keying.
RFC 8448 §3 ground truth (SHA-256 / X25519 / no PSK)
Inputs:
- DHE =
8bd4054fb55b9d63fdfbacf9f04b9f0d35e6d63f537563efd46272900f89492d - H(CH..SH) =
860c06edc07858ee8e78f0e7428c58edd6b43f2ca3e6e95f02ed063cf0e1cad8 - H(CH..server Finished) =
9608102a0f1ccc6db6250b7b7e417b1a000eaada3daae4777a7686c9ff83df13
Expected:
- Handshake Secret =
1dc826e93606aa6fdc0aadc12f741b01046aa6b99f691ed221a9f0ca043fbeac - c_hs_traffic =
b3eddb126e067f35a780b3abf45e2d8f3b1a950738f52e9600746a0e27a55a21 - s_hs_traffic =
b67b7d690cc16c4e75e54213cb2d37b4e9c912bcded9105d42befd59d391ad38 - Master Secret =
18df06843d13a08bf2a449844c5f8a478001bc4d4c627984d5a41da8d0402919 - c_ap_traffic =
9e40646ce79a7f9dc05af8889bce6552875afa0b06df0087f792ebb7c17504a5 - s_ap_traffic =
a11af9f05531f856ad47116b45a950328204b4f44bfb6b3a4b4f1f3fcb631643
If your implementation matches those hex values byte-for-byte against the RFC, your key schedule is correct.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…