skip to content

In TLS 1.3, what does a resuming client put in its pre_shared_key(41) extension, and what does a binder prove?

level: middleimportance: should knowfreq 50%

answer

  1. the ticket names the key, it is not the key
  2. age travels obfuscated
  3. a MAC over the truncated ClientHello
  4. possession plus binding to this hello
  5. that extension must come last

basics

~20 s

It carries one or more PskIdentity entries - each an opaque ticket plus an obfuscated_ticket_age - and a matching PskBinderEntry list. A binder is a MAC keyed from the pre-shared key, proving possession and tying the offer to this exact ClientHello.

solid answer

~40 s

The `pre_shared_key(41)` extension has two parallel lists. Each `PskIdentity` is a ticket the server issued in an earlier `NewSessionTicket`, together with `obfuscated_ticket_age` - the client's measured age of that ticket plus the `ticket_age_add` value the server put in the ticket message, so an observer cannot correlate two offers by age. Each `PskBinderEntry` is a MAC over the `ClientHello` truncated immediately before the binder list, keyed from the pre-shared key that identity names. That proves the client actually holds the key rather than a copied blob, and binds the offer to this hello, so it cannot be lifted into another one. Because the binder covers everything before it, `pre_shared_key(41)` must be the last extension in the hello. The client also sends `psk_key_exchange_modes(45)`, and the server echoes the index of the identity it chose.

code

pseudocode · 15 lines
pseudocode
on ClientHello containing pre_shared_key:
    for each offered identity at index i:
        state = open ticket(identity[i].ticket)
        if state missing or past its lifetime: continue

        psk      = derive(state.resumption_master_secret, state.ticket_nonce)
        age      = (identity[i].obfuscated_ticket_age - state.ticket_age_add) mod 2^32
        expected = mac(binder_key(psk), ClientHello truncated before binders)

        if constant_time_equal(expected, binders[i]):
            selected_identity = i
            break

    if no identity selected:
        run a full handshake

go deeper

for a junior

Know that the client sends an opaque ticket the server issued earlier, not the secret itself, and that a short value alongside it proves the client really holds the secret.

for a middle

Explain the two parallel lists, what a binder is computed over, why the extension has to come last, and how the server signals which offer it took.

for a senior

Separate the failure modes: a ticket that cannot be opened, a binder that does not verify, and an age that does not match are three different diagnoses with three different responses.

for a principal

Own the mode decision: resuming without a fresh key share is cheaper and makes ticket-key custody the ceiling on how much a later disclosure reveals.

## The object being offered is not the key TLS 1.3 separates two things that TLS 1.2 blurred. The **ticket** is an identity: an opaque blob the server issued in a `NewSessionTicket` message, meaningful only to the server. The **pre-shared key** is a secret the client computed for itself when it received that ticket, derived from the connection's `resumption_master_secret` together with the `ticket_nonce` carried in the same message, using `HKDF-Expand-Label`. One connection can issue several tickets, and because each carries its own `ticket_nonce`, each yields a different pre-shared key. So "the client sends the ticket" is right, and "the ticket is the key" is wrong - the client never puts the key on the wire in any form. ## What is inside pre_shared_key(41) The extension holds two equal-length lists: - **identities** - a `PskIdentity` per offer, each with the opaque ticket and an `obfuscated_ticket_age`; - **binders** - a `PskBinderEntry` per offer, in the same order. `obfuscated_ticket_age` is the client's measurement of how long ago it received that ticket, in milliseconds, plus the random `ticket_age_add` the server put in the `NewSessionTicket`, taken modulo 2^32. The server can subtract its own `ticket_age_add` and recover the age; a passive observer sees a value it cannot interpret, so it cannot use age to link two offers of the same ticket. ## What a binder is and what it proves A binder is a MAC computed with a key derived from the pre-shared key, over the `ClientHello` **truncated immediately before the binder list itself** - it cannot cover itself. Two properties follow: 1. **Possession.** Only a party holding the pre-shared key can compute a binder that verifies. Copying the ticket alone is not enough. 2. **Binding.** The MAC covers every other byte of this hello: its randoms, its extensions, its `key_share(51)`. An offer cannot be cut out of one hello and pasted into another. This is also why the specification requires `pre_shared_key(41)` to be the **last** extension in the `ClientHello`: only there does the truncation leave everything else inside the MAC. What a binder is emphatically **not** is a freshness proof. A byte-for-byte copy of a `ClientHello` carries a byte-for-byte valid binder - which is the whole reason 0-RTT needs separate anti-replay machinery. ## The server's side of the exchange On receipt the server walks the offered identities, finds one it can open, derives the same pre-shared key, recomputes the binder over the truncated hello and compares it in constant time. If one matches, it puts that offer's index in the `selected_identity` field of the `pre_shared_key(41)` extension it echoes in its `ServerHello`. If none matches - expired ticket, wrong protection key, bad binder - it simply omits the extension and runs a full handshake. ## The companion extension A client offering a pre-shared key must also send `psk_key_exchange_modes(45)`, which states whether it is willing to resume with the pre-shared key alone (`psk_ke`) or with a fresh Diffie-Hellman exchange alongside it (`psk_dhe_ke`). This is not a detail: - with `psk_dhe_ke` the client also sends a `key_share(51)`, and the resumed connection's traffic keys depend on a secret that exists only for this connection; - with `psk_ke` alone, the traffic keys depend only on the pre-shared key, so anything that later discloses that key - or the protection key wrapping the ticket that names it - exposes the whole connection. So "does a resumption still do a key exchange?" has one honest answer: only if the offered modes asked for one and the server selected a share. ## Reading a failure Because the pieces are separable, the failures are too. A ticket the server cannot open produces a full handshake. A ticket it can open whose binder does not verify is a protocol error, not a miss - it means the hello was altered in flight or the client derived the wrong key. An offer whose recovered age is far from the server's own measurement is a stale or replayed hello, and that check matters most when early data is on the table.

  • Why must pre_shared_key(41) be the last extension in a ClientHello?
    Because a binder is computed over the hello truncated immediately before the binder list, and a MAC cannot cover itself. Putting the extension last means everything else in the hello - randoms, key_share(51), server_name(0) - falls inside the MAC, so nothing can be altered without breaking verification.
  • Does a resumed TLS 1.3 connection still perform a Diffie-Hellman exchange?
    Only if the client offered psk_dhe_ke in psk_key_exchange_modes(45), sent a key_share(51), and the server selected it. With psk_ke alone the traffic keys derive from the pre-shared key only, so later disclosure of that key exposes the resumed connection.
  • What is ticket_nonce for?
    It makes every ticket issued on one connection name a different pre-shared key. The key is derived from resumption_master_secret with that nonce, so obtaining the key behind one ticket does not hand over the keys behind the others issued alongside it.
  • What does obfuscated_ticket_age hide, and from whom?
    From a passive observer: the client adds the random ticket_age_add the server supplied, so the value on the wire cannot be read as an age or used to link two offers. The issuing server subtracts its own value and recovers the age for a freshness check.

saying these in an interview costs you the question

  • Calls the binder a signature made with the server's private key
  • Says the ticket blob is the pre-shared key itself
  • Claims every resumed TLS 1.3 connection runs a fresh Diffie-Hellman exchange
  • Reads obfuscated_ticket_age as the ticket's remaining lifetime
  • Thinks offering several identities resumes several of them
  • Believes the binder detects a replayed ClientHello