Core Question
Core Question
What does a TLS record actually protect, what stays visible on the outside, and what does "the tag doesn't match" tell you versus what it doesn't tell you?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name the two fields on
Recordthat ride outside the seal, and the two that are sealed - explain what the integrity tag is computed over, and why the sequence number is part of that computation
- state the one failure outcome
unprotect()can actually return, and which three distinct real-world causes all collapse into it - explain why this collapse is a deliberate design choice, not a missing feature
Read Order
Read Order
- Read the module comment at the top of the file
- Read
Record - Read
UnprotectOutcomeandUnprotectResult - Read
_tag_for() - Read
protect() - Read
unprotect() - Run
examples/tls/session_07_walkthrough.py
Read It Like Code
Read It Like Code
Record(
content_type,
seq,
ciphertext,
tag,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
content_type | Rides outside the seal. A record's kind (handshake, application data, alert) is visible before the record is opened. |
seq | Rides outside the seal, but is folded into the tag computation. Visible, and also load-bearing for integrity. |
ciphertext | In this toy, the plaintext left readable — see Toy Model Boundary. In real TLS this is the encrypted payload. |
tag | The seal itself: a sha256 hash over (key, seq, plaintext). Recomputing it is the only way to open a record. |
Decision Flow
Decision Flow
unprotect(record, key): expected = tag_for(key, record.seq, record.ciphertext) record.tag != expected -> BAD_TAG, plaintext=None record.tag == expected -> OK, plaintext=record.ciphertext
Reading Lens
Reading Lens
The important move in this session is to stop thinking of "encrypted" and "authenticated" as two separate guarantees and start asking:
- what is visible on a
Recordbefore you ever callunprotect()? - what three inputs feed
_tag_for(), and what happens to the tag if any one of them changes? - when
unprotect()returnsBAD_TAG, which of those three inputs was wrong? Can you tell from the outcome alone?
Toy Model Boundary
Toy Model Boundary
Real TLS protects records with an AEAD cipher (e.g. AES-GCM) that both encrypts and authenticates in one step. Here the "ciphertext" is the plaintext left readable, and the integrity tag is a sha256 hash over (key, seq, plaintext). That keeps the envelope metaphor honest without pretending to be real cryptography — the module comment says so directly, and this lesson repeats it rather than hiding it.
content_type is never encrypted in this toy model either. Real TLS 1.3 hides the true content type inside the encrypted payload and puts a fixed application_data type on the wire (RFC 8446 SS5.1); this module keeps content_type in the clear on Record so the envelope's "outside vs. inside" boundary stays easy to read at a glance.
Code Landmarks
Code Landmarks
The module comment
States the toy substitution before any code does. Read this before Record, not after.
_tag_for()
Three inputs, one hash: key, seq, plaintext. Every other function in this file is a thin wrapper around calling this and comparing.
protect()
The docstring says it plainly: sealing a record binds its sequence number into the tag, so replay or reorder breaks it too. There is no separate sequence-number check anywhere in this file — seq is enforced entirely through the tag.
unprotect()
The reading target. One if, one comparison, two possible outcomes — OK or BAD_TAG. UnprotectOutcome declares a third value, WRONG_KEY, that this function never returns.
Failure Questions
Failure Questions
Use the source file to answer these:
UnprotectOutcomedeclaresWRONG_KEYas a value. Under what condition doesunprotect()actually return it?- If you call
unprotect()with the right key but the wrongseq, what outcome do you get, and which line of_tag_for()explains why? - Two records have the same
ciphertextandtagbut differentseq. Can both successfullyunprotect()with the same key? Trace through_tag_for()to justify your answer. - Does
content_typeever appear inside the material hashed by_tag_for()? What does that imply about tampering withcontent_typealone? protect()takescontent_typeas a keyword argument with a default. What is that default, and where doesrun_handshake()(Session 09) rely on it?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/tls/session_07_walkthrough.py
The walkthrough seals one record and opens it, then breaks the seal three different ways — tampered ciphertext, wrong key, wrong sequence number — and shows that all three land on the same BAD_TAG outcome with plaintext=None. A final scenario protects a second record at the next sequence number to show each seal stands on its own.
Done When
Done When
The learner can say all of the following without looking at notes:
- "content_type and seq ride on the outside of the envelope; the tag seals the inside, and seq is baked into the tag anyway."
- "BAD_TAG is the only failure this function returns — tampering, the wrong key, and a replayed sequence number are indistinguishable from the caller's side."
- "That collapse is intentional: revealing which one failed would leak information to an attacker probing the channel."
References
References
- RFC 8446 Section 5.1 (Record Layer)
- RFC 8446 Section 5.2 (Record Payload Protection)
Continue