skip to content

questions

5

After a TLS 1.3 handshake completes, in what unit is application data protected, and what limits that unit's size?

level: juniorimportance: must knowfreq 62%

answer

  1. protection happens one record at a time
  2. a stream is cut into fragments
  3. plaintext fragment ceiling is 2^14
  4. ciphertext adds at most 256 bytes
  5. oversized record raises record_overflow(22)

basics

~20 s

TLS 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 s

Once 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 lines
pseudocode
function 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)
    return

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

A TLS 1.3 subtitle feed stops arriving and the socket reports end of file - what must the receiver have seen for that to be an orderly close?

level: middleimportance: must knowfreq 66%

basics

~20 s

A close_notify(0) alert is the only thing that marks an orderly end of data in TLS. A transport end of file with no close_notify is a truncated stream, and the receiver cannot tell a finished sender from a cut connection.

open as a page

TLS 1.3 puts no record sequence number on the wire over a stream transport, so how is each record's nonce formed?

level: middleimportance: should knowfreq 46%

basics

~20 s

Each side keeps a 64-bit record counter for reading and one for writing, both starting at zero. The counter is left-padded to the length of the static write IV and XORed with it, producing a distinct nonce for every record.

open as a page

A TLS 1.3 connection carrying one continuous stream stays open for hours - how are its record-protection keys rotated without a new handshake?

level: seniorimportance: should knowfreq 36%

basics

~20 s

With a KeyUpdate message, sent inside the protected stream after the handshake. It rotates only the sender's own write keys and resets that direction's record counter; its request_update field, set to update_requested, asks the peer to rotate its direction too.

open as a page

Why does every protected TLS 1.3 record show application_data(23) in its header, and where is the real content type?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

TLS 1.3 moved the real content type inside the encrypted payload. The outer header's opaque_type reads application_data(23) for every protected record; the receiver decrypts, strips trailing zero octets, and reads the last non-zero byte as the type.

open as a page