skip to content

A TLS 1.3 server wants a named group the client sent no key_share for — what happens next on the wire?

level: middleimportance: must knowfreq 55%

answer

  1. The guess missed, not the negotiation
  2. Server names a group, sends no value
  3. Second ClientHello, one extra round trip
  4. First hello folded into message_hash(254)
  5. Only one retry per handshake

basics

~20 s

The server must answer with HelloRetryRequest, naming the group it wants in key_share(51). The client generates a fresh share for that group and sends a second ClientHello, and the handshake costs one extra round trip.

solid answer

~40 s

This is a missed guess, not a failed negotiation: the group was in the client's `supported_groups(10)`, just not among the entries in `key_share(51)`. The server must send `HelloRetryRequest`, whose `key_share(51)` extension carries only a `selected_group` identifier and **no** server public value. The client generates a key pair for that group and re-sends its ClientHello, identical to the first apart from a short list of permitted edits — the replaced `key_share(51)` entry chief among them. Because two ClientHello messages now exist, the transcript substitutes a synthetic `message_hash(254)` message holding the hash of the first one. Only one retry is allowed per handshake, and if the client's response would change nothing it aborts with `illegal_parameter(47)`.

code

pseudocode · 15 lines
pseudocode
ClientHello #1
  supported_groups(10): x25519(0x001D), secp256r1(0x0017)
  key_share(51): { group = x25519(0x001D), key_exchange = ... }

HelloRetryRequest          // server prefers secp256r1(0x0017)
  key_share(51): { selected_group = secp256r1(0x0017) }
  // no server key_exchange value in this message

ClientHello #2             // identical, except:
  key_share(51): { group = secp256r1(0x0017), key_exchange = ... }

transcript = message_hash(254) over Hash(ClientHello #1)
          || HelloRetryRequest
          || ClientHello #2
          || ServerHello ...

go deeper

for a junior

Recall that the retry means the client guessed the wrong named group, and that it costs one more exchange before anything useful is sent.

for a middle

Explain what the retry message does and does not carry — a group identifier, no public value — and which parts of the second ClientHello are allowed to differ from the first.

for a senior

Read a persistent retry as a standing mismatch in offered groups rather than a fault, and account for the latency it adds to every connection an estate opens.

for a principal

Weigh pre-generating shares for more groups against the bytes and key-generation work in every first flight, across clients you do not control.

A TLS 1.3 client puts its key exchange in the very first flight by **guessing** which named group the server will pick. `HelloRetryRequest` is what the protocol does when that guess misses. It is easy to misread as a negotiation failure; it is the opposite — the two sides agree on a group, the client simply has not yet generated a public value for it. ## The three ways a client's guess can land | Situation | Server's response | Cost | |---|---|---| | Server's group is among the client's `key_share(51)` entries | ServerHello with its own share | none — one round trip | | Server's group is in `supported_groups(10)` but has no share | `HelloRetryRequest` naming that group | one extra round trip | | No group in common at all | abort: `handshake_failure(40)` or `insufficient_security(71)` | connection ends | Only the middle row is a retry. The bottom row cannot be repaired by asking again, because the client has already stated everything it is willing to use. ## What HelloRetryRequest carries The message's `key_share(51)` extension holds a single `selected_group` field — one named group identifier. It carries **no** `key_exchange` bytes: the server has not generated its own ephemeral key pair yet, and will not until it has a matching client value to combine it with. The server may also attach a `cookie(44)` extension (a TLS extension, not an HTTP cookie) so that it can discard all state about the first ClientHello and reconstruct it from what the client echoes back. ## What the second ClientHello may change The second ClientHello must be the first one again, with a short list of permitted edits: - the `key_share(51)` list is replaced by a single entry for the group the server named; - a `cookie(44)` extension is echoed if the server sent one; - `early_data(42)`, if it was offered, is removed; - padding and the pre-shared-key binders are recomputed for the new message. Everything else — the version list, `supported_groups(10)`, `server_name(0)`, the offered cipher suites — is carried over unchanged. This is what makes the retry auditable: a client cannot use it to quietly present a different, weaker ClientHello. If what the server asked for would produce no change at all — it named a group the client already sent a share for, or one absent from `supported_groups(10)` — the client aborts with `illegal_parameter(47)` instead of answering. A server may send only one `HelloRetryRequest` per handshake; a second one is an error the client will not answer. ## The transcript substitution Every secret in TLS 1.3 is derived over a running hash of the handshake messages, so both sides must hash **exactly** the same byte sequence. With a retry there are now two ClientHello messages, and the specification does something deliberate: the first ClientHello is removed from the transcript and replaced by a synthetic handshake message of type `message_hash(254)` whose body is the hash of that first ClientHello. The transcript therefore reads: 1. `message_hash(254)` containing `Hash(ClientHello1)` 2. `HelloRetryRequest` 3. `ClientHello2` 4. `ServerHello`, and the rest of the handshake. There are two consequences. The first ClientHello is still **bound** into every derived secret, so nobody can alter it after the fact and have the `Finished` messages still match. And a server that wants to hold no per-connection state can forget the original bytes entirely: it needs only the hash, which is small enough to carry in the `cookie(44)` it hands the client. ## What it costs, and when it is worth paying One extra round trip before any application data moves, plus a second key-pair generation on the client. For a vote-tally transmitter that opens a connection once per precinct upload, that is invisible. For a client opening many short connections to the same endpoint, the retry is pure latency paid on every one of them — and it is avoidable in principle, because the client chooses which groups to pre-generate shares for. The interesting operational signal is a **persistent** retry: it means the client's preference order and the server's disagree every single time, which is a mismatch in offered groups rather than a transient fault.

  • Why does the transcript replace the first ClientHello with a message_hash(254) message instead of keeping the original bytes?
    Two reasons. The first ClientHello stays cryptographically bound into every derived secret, so it cannot be altered after the fact without breaking the `Finished` messages. And a server that wants to keep no per-connection state can discard the original bytes and carry only the hash — small enough to travel inside the `cookie(44)` it hands back.
  • What does a server do when its own named groups and the client's supported_groups(10) have nothing in common?
    It aborts, with `handshake_failure(40)` or `insufficient_security(71)`. A retry would be pointless: `supported_groups(10)` already lists everything the client will accept, so asking again cannot produce a group that was never on offer.
  • May a server send a second HelloRetryRequest in the same handshake?
    No — one per handshake. A client that receives a second one aborts rather than answering. The client also aborts, with `illegal_parameter(47)`, if the group it was asked for would leave its ClientHello unchanged: a group it already sent a share for, or one missing from its own `supported_groups(10)`.

Dialling the wrong extension does not get you told the line is dead; you get told the right extension and pay for one more call.

saying these in an interview costs you the question

  • Thinks HelloRetryRequest means the two sides share no named group.
  • Says HelloRetryRequest carries the server's ephemeral public value.
  • Believes the client may send a freely different second ClientHello.
  • Claims the retry is free because TLS 1.3 is one round trip.
  • Thinks the first ClientHello is simply dropped from the transcript.
  • Expects a server to keep retrying until the client complies.