Core Question
Core Question
Where does each TLS secret actually come from, and why can none of them be computed out of order?
Outcome
Outcome
By the end of this session, the learner should be able to:
- explain
derive()as a stand-in for HKDF-Expand-Label, and state exactly what it is not - walk the chain early_secret -> handshake_secret -> master_secret and say what new input (if any) is mixed in at each stage
- explain why
client_hs_trafficandserver_hs_trafficare two different values derived from the samehandshake_secret - explain why
advance_to_master()raises if called beforeadvance_to_handshake()
Read Order
Read Order
- Read the module-level comment at the top of the file
- Read
derive() - Read
KeySchedule - Read
start_schedule() - Read
advance_to_handshake() - Read
advance_to_master() - Run
examples/tls/session_03_walkthrough.py
Read It Like Code
Read It Like Code
KeySchedule(
early_secret,
handshake_secret=None,
master_secret=None,
client_hs_traffic=None,
server_hs_traffic=None,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
early_secret | Seeded once, from the PSK, at construction time. Never None. |
handshake_secret | None until advance_to_handshake() runs. Mixes the (EC)DHE shared key into early_secret. |
master_secret | None until advance_to_master() runs. Derived from handshake_secret alone - no new external input. |
client_hs_traffic | Derived from handshake_secret with the label "client hs traffic". Set alongside handshake_secret. |
server_hs_traffic | Derived from handshake_secret with the label "server hs traffic". Same secret, different label, different value. |
Decision Flow
Decision Flow
start_schedule(psk) -> early_secret = derive(psk, "early secret")
advance_to_handshake(schedule, shared_key) -> handshake_secret = derive(early_secret, "handshake secret", shared_key)
client_hs_traffic = derive(handshake_secret, "client hs traffic")
server_hs_traffic = derive(handshake_secret, "server hs traffic")
advance_to_master(schedule) -> if handshake_secret is None: raise ValueError
master_secret = derive(handshake_secret, "master secret")Reading Lens
Reading Lens
The important move in this session is to stop thinking of "the key" as one value and start asking:
- which secret is this one derived FROM?
- what label distinguishes it from every other secret derived from the same input?
- is there new external material mixed in at this stage, or is it derived from internal state alone?
Toy Model Boundary
Toy Model Boundary
derive() is a single sha256 hexdigest over the concatenation of secret, label, and context. The module comment says plainly that this is a toy stand-in for HKDF-Extract / HKDF-Expand-Label (RFC 8446 SS7.1) and makes no cryptographic strength claim. Do not read anything here as real key derivation: there is no HMAC, no expand-with-length-prefix, no separation between Extract and Expand, and no key material of a fixed cryptographic size - just a hex string standing in for one.
There is also no actual (EC)DHE math anywhere in this module. shared_key in advance_to_handshake() is just a string the caller supplies; nothing here performs a Diffie-Hellman computation to produce it. The lesson is the *shape* of the schedule - what depends on what, and in what order - not the cryptography that fills each step in a real implementation.
Code Landmarks
Code Landmarks
derive()
One line of real logic: hashlib.sha256(f"{secret}|{label}|{context}".encode()).hexdigest(). Same three inputs always produce the same output - this is what makes every stage of the schedule reproducible and testable.
KeySchedule
A mutable dataclass, unlike ClientHello/ServerHello in Session 01. The schedule is built up in place across three function calls, so it cannot be frozen - each advance_* function mutates fields on the instance it is given.
start_schedule()
The only place early_secret is ever set. It calls derive() once, with the label "early secret" and no context. An empty-string PSK is legal - the default psk="" produces a real digest.
advance_to_handshake()
Three derive() calls in a row, all keyed off handshake_secret for the last two. Note client_hs_traffic and server_hs_traffic are computed from the *same* handshake_secret - only the label string differs, and that alone is enough to make them different digests.
advance_to_master()
Guarded by an explicit if schedule.handshake_secret is None: raise ValueError. This is the one place in the file that enforces order - you cannot skip a stage.
Failure Questions
Failure Questions
Use the source file to answer these:
advance_to_handshake()sets three fields on the schedule. Which of the three usesshared_keydirectly as an argument toderive(), and which two do not?- If you call
start_schedule(psk="a")twice, do the twoearly_secretvalues match? What single property ofderive()guarantees your answer? advance_to_master()callsderive(schedule.handshake_secret, "master secret")with no third argument. What doescontextdefault to insidederive()when it is omitted this way?- Construct a fresh
KeyScheduleby calling onlystart_schedule(). What exception, if any, do you get from callingadvance_to_master()on it directly, and which line raises it? client_hs_trafficandserver_hs_trafficare both derived fromhandshake_secret. Given thatderive()is a pure function of its three arguments, what is the only thing that can make these two outputs differ?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/tls/session_03_walkthrough.py
The walkthrough shows derive() is deterministic and label-sensitive, runs the same psk/shared_key pair through two independent schedules to confirm identical master secrets, then changes only the psk to show every downstream secret changes, confirms client_hs_traffic != server_hs_traffic, and finally calls advance_to_master() on a schedule with no handshake secret to show the guard raising.
Done When
Done When
The learner can say all of the following without looking at notes:
- "
derive()is sha256 over (secret, label, context) - an honest stand-in for HKDF-Expand-Label, not real cryptography." - "Every secret in the schedule is derived from the one before it, plus a label, plus sometimes new external material - never from scratch."
- "Changing the psk changes early_secret, which changes handshake_secret, which changes master_secret and both traffic secrets - the chain propagates all the way down."
References
References
- RFC 8446 Section 7.1 (Key Schedule)
Continue