A TLS 1.3 server wants a named group the client sent no key_share for — what happens next on the wire?
answer
- The guess missed, not the negotiation
- Server names a group, sends no value
- Second ClientHello, one extra round trip
- First hello folded into message_hash(254)
- Only one retry per handshake
basics
~20 sThe 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 sThis 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 linesClientHello #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
Recall that the retry means the client guessed the wrong named group, and that it costs one more exchange before anything useful is sent.
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.
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.
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.