Core Question
Core Question
What does a complete TLS 1.3 handshake look like when hello, negotiation, chain verification, hostname matching, key schedule, ticket issuance, and one protected record are all wired into two functions instead of eight separate demos?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name every module this file imports and which earlier session taught it
- trace
run_handshake()top to bottom and say, at each step, which earlier session's function just ran - explain why a ticket HIT in
run_resumed_handshake()never callsverify_chain()ormatch_hostname(), and how the trace proves the skip - explain why
run_resumed_handshake()falls back torun_handshake()on anything other than a ticket HIT - read a
HandshakeOutcomeand say whether the failure happened at negotiation, chain verification, hostname matching, or the record layer
Read Order
Read Order
- Read the module comment and
SHARED_KEY/TICKET_LIFETIME - Read
ToyTlsConfig,ToyTlsClient,ToyTlsServer,HandshakeOutcome - Read
_fail() - Read
run_handshake()top to bottom, mapping each block back to its session (see Parts List) - Read
run_resumed_handshake(), comparing it step for step againstrun_handshake() - Run
examples/tls/session_09_walkthrough.py - Read the printed traces and label each line with its session
Read It Like Code
Read It Like Code
ToyTlsConfig(
versions,
cipher_suites,
chain,
trust_store,
)
ToyTlsClient(name, config, trace)
ToyTlsServer(name, config, tickets, trace)
HandshakeOutcome(
completed,
alert,
trace,
ticket_name,
)Parts List
Parts List
Every function handshake_loop.py imports was taught by an earlier session in this track. The capstone's only new code is the wiring between them, plus SHARED_KEY and TICKET_LIFETIME.
| Import | Session that taught it | What it contributes to run_handshake() / run_resumed_handshake() |
|---|---|---|
messages.ClientHello, ServerHello | 01 | The hello and its reply, built and sent as plain values. validate_client_hello() from Session 01 is not called here — the loop builds a well-formed hello directly and never exercises HelloValidity. |
negotiate.NegotiationOutcome, choose_suite, choose_version | 02 | Version and cipher-suite agreement, first two negotiation steps in run_handshake(). |
key_schedule.KeySchedule, start_schedule, advance_to_handshake, advance_to_master | 03 | client_schedule / server_schedule (and resumed_schedule); walked from early secret to master secret on both full and resumed paths. |
chain.Certificate, ChainVerdict, verify_chain | 04 | Chain verification against trust_store, run once per full handshake — and structurally skipped on a ticket HIT. |
hostname.HostnameVerdict, match_hostname | 05 | Hostname matching against the leaf certificate's subject — likewise skipped on a ticket HIT. |
resumption.TicketOutcome, TicketStore, issue_ticket, lookup_ticket | 06 | server.tickets; every full handshake issues a ticket, every resumption attempt looks one up first. |
record.UnprotectOutcome, protect, unprotect | 07 | One application-data record sealed by the client and opened by the server on both the full and resumed paths, keyed by the schedule's master_secret. |
alert.Alert, alert_for_negotiation_outcome, alert_for_chain_verdict, alert_for_hostname_verdict, alert_for_unprotect_outcome | 08 | Every failure exit in _fail() carries a typed Alert produced by one of these four mapping functions. |
Decision Flow
Decision Flow
run_handshake(client, server, trust_store, now):
1. build ClientHello from client.config, send it (01)
2. choose_version(...) not CHOSEN -> fail: HANDSHAKE_FAILURE (02, 08)
3. choose_suite(...) not CHOSEN -> fail: HANDSHAKE_FAILURE (02, 08)
4. build ServerHello, send it (01)
5. verify_chain(...) not TRUSTED -> fail: per chain table (04, 08)
6. match_hostname(...) NO_MATCH -> fail: UNRECOGNIZED_NAME (05, 08)
7. advance both schedules: early -> handshake -> master (03)
8. issue_ticket(...) on the server (06)
9. protect() one record, unprotect() it on the server (07)
not OK -> fail: BAD_RECORD_MAC (08)
10. completed=True, ticket_name set
run_resumed_handshake(client, server, ticket_name, now):
1. lookup_ticket(...) (06)
not HIT -> fall back to run_handshake() in full, unconditionally
HIT -> trace "chain verification SKIPPED", skip steps 5-6 above entirely
2. start_schedule(psk=ticket.master_secret), advance to master (03)
3. protect() / unprotect() one record, same as full path (07)
not OK -> fail: BAD_RECORD_MAC (08)
4. completed=True, ticket_name unchangedReading Lens
Reading Lens
The important move in this session is to stop reading run_handshake() as one long function and start asking, at every call:
- which of the eight imported modules is doing the actual work on this line, and which session taught it?
- if this line's result is a failure verdict, which
alert_for_*function turns it into anAlert, and what doesclassify()(Session 08) say happens next? - on the resumed path, which two calls from the full path are structurally absent — not skipped by an
if, but never written at all?
Toy Model Boundary
Toy Model Boundary
This module does not perform a real key exchange. SHARED_KEY is a single constant string standing in for what a real handshake would derive from (EC)DHE — every handshake in this codebase, full or resumed, mixes in the exact same "shared secret," so nothing here demonstrates forward secrecy or per-connection key uniqueness. The handshake is a single round trip: one ClientHello, one ServerHello, no HelloRetryRequest and no second flight. There is no 0-RTT: run_resumed_handshake() still performs a full round trip and derives keys before either side sends application data, unlike real TLS 1.3 early data. chain[0] is treated as the SAN list in run_handshake() (san_names = (leaf.subject,)) — there is no separate Subject Alternative Name field on Certificate, so hostname matching runs against the same string used to build the ClientHello's server_name. And as in Sessions 07 and 08, every failure path collapses several possible real causes into one typed Alert; the capstone does not change that behavior, only chains it end to end.
Code Landmarks
Code Landmarks
The module comment above SHARED_KEY
Two constants, one line each, both worth reading before the functions: SHARED_KEY names the toy substitution explicitly; TICKET_LIFETIME = 3600 is the only place this file decides how long a ticket is good for.
_fail()
Every failure exit in this file — five of them across both functions — routes through this one helper. It appends the same trace line to both client.trace and server.trace, then returns a HandshakeOutcome with completed=False. Read this once; every early return below it does the same thing.
run_handshake()'s ordering
Version and suite negotiation happen before chain verification, which happens before hostname matching, which happens before the key schedule advances. Every check that can fail runs before any cryptographic material is derived — nothing is wasted deriving keys for a handshake that was always going to abort.
run_resumed_handshake()'s early return
if lookup.outcome is not TicketOutcome.HIT: ... return run_handshake(...). A resumption attempt that doesn't find a valid ticket is not a distinct failure mode — it silently becomes an ordinary full handshake. The trace shows the fallback line, but the HandshakeOutcome it returns is indistinguishable in shape from a first-time full handshake.
The two trace lines that only exist on the resumed path
"chain verification SKIPPED: resuming from ticket's master secret" is appended to *both* client.trace and server.trace immediately after a ticket HIT, in the exact spot where verify_chain() and match_hostname() would have run on the full path. There is no corresponding line — SKIPPED or otherwise — on the full-handshake path, because there is nothing to skip there.
Failure Questions
Failure Questions
Use the source file to answer these:
run_handshake()computeshostnamefromclient.config.chain[0].subject if client.config.chain else "example.com"before sending theClientHello. What hostname does a client with an emptychaintuple offer, and what wouldmatch_hostname()be checking against on the server side in that case?_fail()builds its trace line fromalert.level.valueandalert.description.value. Trace a no-suite-overlap handshake: whichalert_for_*function is called, and what two-word phrase appears in the resulting trace line?run_resumed_handshake()callsstart_schedule(psk=lookup.ticket.master_secret)instead ofstart_schedule()with no argument. Compare this torun_handshake()'sstart_schedule()call — what does thepskparameter change about the resultingearly_secret, perkey_schedule.py?- Both
run_handshake()andrun_resumed_handshake()callprotect("application data", ..., seq=0). If a real connection reusedrun_handshake()'sapplication_keyfor a second record without incrementingseq, what would Session 07'sunprotect()do — and is that scenario exercised anywhere in this file? ToyTlsServer.ticketsis a freshTicketStore()by default viafield(default_factory=TicketStore). If two separateToyTlsServerinstances are used — one for the full handshake, a different one for the resumption attempt — what outcome doeslookup_ticket()return, and which branch ofrun_resumed_handshake()runs as a result?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/tls/session_09_walkthrough.py
The walkthrough runs a full handshake to completion and checks both traces are populated, runs a no-suite-overlap configuration to HANDSHAKE_FAILURE, runs an expired-certificate server to CERTIFICATE_EXPIRED, then resumes the first handshake's session using its issued ticket — checking the resumed handshake completes, that its trace contains a SKIPPED line, and that the resumed portion of the trace contains no "verifies chain" entry at all.
Done When
Done When
The learner can say all of the following without looking at notes:
- "Every field and every function in this file belongs to an earlier session; this module only wires them together in two functions."
- "run_handshake() checks negotiation, then chain, then hostname, then derives keys — every possible abort happens before any key material exists."
- "A ticket HIT doesn't skip chain and hostname verification with an if — it takes a different code path that never calls those functions at all, and the trace says so."
References
References
- RFC 8446 Section 2 (Protocol Overview — the full handshake this loop compresses)
- RFC 8446 Section 4.2.11 / Section 2.2 (Pre-Shared Key / resumption, cross-referenced from Session 06)
- RFC 8446 Section 4.1 (Key Exchange Messages, cross-referenced from Sessions 01-02)
- RFC 8446 Section 4.4.2 (Certificate, cross-referenced from Session 04)
- RFC 8446 Section 7.1 (Key Schedule, cross-referenced from Session 03)
- RFC 8446 Section 5.1 / 5.2 (Record Layer, cross-referenced from Session 07)
- RFC 8446 Section 6 (Alert Protocol, cross-referenced from Session 08)
Continue