After a TLS 1.3 handshake completes, in what unit is application data protected, and what limits that unit's size?
answer
- protection happens one record at a time
- a stream is cut into fragments
- plaintext fragment ceiling is 2^14
- ciphertext adds at most 256 bytes
- oversized record raises record_overflow(22)
basics
~20 sTLS protects one record at a time, not a byte stream. The sender cuts data into fragments of at most 2^14 bytes, and each fragment becomes an independently encrypted record whose protected length may not exceed 2^14 + 256 bytes.
solid answer
~40 sOnce the keys are in force, everything TLS sends travels inside records. The sender fragments the caller's data into `TLSPlaintext` fragments of at most `2^14` bytes, protects each one on its own, and emits a `TLSCiphertext` whose `length` field may not exceed `2^14 + 256` - the extra allowance covers padding, the one inner content-type byte and the authenticated-encryption expansion. A receiver that sees a longer record must end the connection with `record_overflow(22)`. The sender is free to split one application write across several records or coalesce several small writes into one, so a record boundary tells the application nothing about where its own messages begin and end. A receiver may ask for a smaller ceiling with the `record_size_limit` extension.
code
pseudocode · 8 linesfunction send_application_data(bytes, max_plaintext):
limit = minimum(max_plaintext, 2^14)
for each fragment of bytes with size at most limit:
record = protect(fragment, content_type = application_data)
if length(record.ciphertext) > 2^14 + 256:
reject("record exceeds the protected ceiling")
write_to_transport(record)
returngo deeper
Remember that TLS protects records, not a stream, and that the plaintext fragment in one record tops out at 2^14 bytes. That single fact explains most of the layer's behaviour.
Explain why the protected record is allowed 256 bytes more than the fragment, what record_overflow(22) is checking and when, and why fragmentation makes record boundaries useless as message boundaries.
Show that you have sized records deliberately: latency to the first usable byte against per-record overhead, and a receiver's buffer declared with record_size_limit rather than assumed.
The trade-off to own is where framing responsibility sits. If a fleet's services rely on record boundaries to delimit messages, that is a latent bug across the estate, not a tuning question.
## The record is the unit of protection A TLS connection does not encrypt a byte stream as one object. Once the handshake has finished and the traffic keys are in force, the sending side takes whatever the application hands it and cuts it into **records**, and every record is protected on its own under the negotiated authenticated-encryption algorithm. Handshake messages and alerts travel the same way: the record layer is the envelope for every byte TLS sends, whatever that byte is for. TLS 1.3 names two structures for that envelope: - **`TLSPlaintext`** - the unprotected form. It carries a content type, a frozen `legacy_record_version` field, a 16-bit length, and the fragment itself. Records sent before the keys are in force, and the dummy `change_cipher_spec(20)` record, appear in this form. - **`TLSCiphertext`** - the protected form. It carries an `opaque_type` byte, the same frozen version field, a length, and the encrypted payload. The five content types a record may carry are `invalid(0)`, `change_cipher_spec(20)`, `alert(21)`, `handshake(22)` and `application_data(23)`. ## Fragmentation and coalescing The sender chooses where to cut, within rules: - One application write may be split across many records, and many small writes may be coalesced into one record. Neither is visible to the receiving application. - **Handshake messages** may share one record or be split across several, but they are not interleaved with other content types and do not span a change of keys. - **An alert record carries exactly one alert**, and an alert is never split across records. - Zero-length `application_data` fragments are permitted, which is one way to pad a stream; zero-length handshake or alert fragments are not. The consequence that catches people out in production is the first one: **a record boundary is not an application message boundary**. If the application needs to know where its own messages end, it frames them itself - a length prefix, a delimiter, or a framing layer such as HTTP. TLS guarantees that the bytes arrive in order and unmodified, not that they arrive in the chunks you wrote them in. ## The two ceilings | structure | maximum | what the number covers | |---|---|---| | `TLSPlaintext` fragment | `2^14` bytes | the caller's data carried in one record | | `TLSCiphertext.length` | `2^14 + 256` bytes | the same data plus padding, the inner content-type byte and the algorithm's expansion | A receiver that is handed a protected record longer than `2^14 + 256` ends the connection with `record_overflow(22)`. That check runs before decryption, on the length field alone, which is what makes it a cheap defence against a peer trying to make the receiver allocate arbitrarily large buffers. ## Declaring a smaller ceiling A receiver with a small buffer can say so. The `record_size_limit` extension states the largest plaintext record **the endpoint that sent the extension is willing to receive**, so the two directions can differ; a TLS 1.3 endpoint may name any value up to `2^14 + 1`, the extra octet being the inner content type that now counts as plaintext. It replaced `max_fragment_length(1)`, which offered only a choice of four fixed sizes from `2^9` to `2^12` bytes and applied the same figure to both directions. That is not only a memory question. A receiver cannot verify or use **any** part of a record until the whole record has arrived, so record size sets a floor on latency: 1. Large records amortise the per-record overhead - the header plus the authentication tag - across more payload, which suits bulk transfer. 2. Small records let the receiver act sooner, which suits a continuous feed. A live-subtitle ingest that emits one record per caption line delivers each line as soon as it lands, at the cost of a fixed overhead per line. 3. Very small records waste bandwidth on overhead and increase the number of records protected under one key, which matters on a long-lived connection. ## What to carry away The record layer is a fragmenting, protecting and size-bounding layer sitting between the application and the transport. It gives you ordered, authenticated bytes and two hard numbers - `2^14` for a plaintext fragment and `2^14 + 256` for a protected record - and it deliberately gives you no information about your own message boundaries.
- What does the record_size_limit extension let a receiver say that max_fragment_length(1) could not?It declares the largest plaintext record the endpoint sending the extension is willing to receive, so the two directions may carry different limits, and it allows any value up to the ceiling. `max_fragment_length(1)` offered only four fixed sizes and imposed the same one on both directions, which made it useless for an endpoint that wanted a small read buffer but large writes.
- Two handshake messages arrive inside one record, and two alerts arrive inside another. Which one is illegal?The alert record. Handshake messages may be coalesced into a single record or split across several, provided they are not interleaved with other content types. Alerts are stricter: one record carries exactly one alert, and no alert is split across records.
- An application writes one 50 KB message and the peer's first read returns 9 KB. Is something wrong?No. The sender had to split the message across at least four records, and the receiver is delivered whatever has arrived and verified so far. Record boundaries are not message boundaries, so an application that needs whole messages frames them itself with a length prefix or a delimiter.
saying these in an interview costs you the question
- Says TLS encrypts the whole connection as one continuous blob
- Assumes one application write always becomes exactly one record
- Treats a record boundary as an application message boundary
- Claims a sender may emit records of any size it likes
- Confuses the plaintext ceiling with the protected-record ceiling