In TLS 1.3, which field carries the version a peer actually negotiates, and why is ClientHello's legacy_version frozen at 0x0303?
answer
- the message's version field is a decoy
- negotiation moved into an extension
- extension 43: client lists, server names one
- 0x0304 never appears in legacy_version
- no echo means pre-1.3 rules applied
basics
~20 sIn TLS 1.3 the supported_versions extension carries the real version list and the server's single choice; ClientHello's legacy_version stays frozen at 0x0303 because implementations in the path rejected unfamiliar values there, while an unknown extension is ignored instead.
solid answer
~50 sTLS 1.3 negotiates the version in the `supported_versions(43)` extension, not in the version field at the front of the message. A client writes `0x0303` into `legacy_version` - the value that means TLS 1.2 - and lists every version it will accept, most preferred first, inside the extension; TLS 1.3 is `0x0304` there. A server that selects TLS 1.3 sends its own `supported_versions` in the `ServerHello` carrying exactly one value, the version it chose, and does the same in a `HelloRetryRequest`. That rule is useful read backwards: a client that offered `0x0304` and gets a `ServerHello` with no `supported_versions` knows the peer answered under the pre-1.3 rules, so TLS 1.2 is the best on the table. The old field was frozen because implementations sitting in the path had been seen rejecting a hello whose version field held a value they did not recognise, whereas unrecognised extensions are already required to be ignored.
code
json · 14 lines{
"client_hello": {
"legacy_version": "0x0303",
"extensions": {
"supported_versions": ["0x0304", "0x0303"]
}
},
"server_hello": {
"legacy_version": "0x0303",
"extensions": {
"supported_versions": "0x0304"
}
}
}go deeper
Recall that a TLS client advertises which versions it will accept and the server picks one of them, and that in TLS 1.3 the advertisement is an extension rather than the number at the front of the message.
Explain the split: legacy_version frozen at 0x0303, supported_versions carrying the client's ordered list and the server's single selected value, and the server bound to choose a version the client listed.
Use the rule in reverse when a connection misbehaves: a ServerHello with no supported_versions means the peer answered under pre-1.3 rules, which tells you which end to inspect before changing any configuration.
Read the frozen field as a design lesson: a protocol only evolves if the elements between endpoints can pass what they do not understand, so the extensible namespace, not the fixed one, is where new negotiation belongs.
## Two different things share the word "version" Every TLS message begins with a two-byte version field, and every TLS connection has a version the two peers agreed on. Until TLS 1.3 those were the same thing: a client wrote the highest version it supported into the `ClientHello` version field, the server wrote its choice into the matching field of the `ServerHello`, and that pair *was* the negotiation. TLS 1.3 broke the link. The field is still present, it is still two bytes, and it no longer decides anything - which is why the specification renamed it `legacy_version`. ## What the client sends A TLS 1.3 client writes `legacy_version 0x0303` - the value that means TLS 1.2 - and puts the versions it is actually prepared to use into the `supported_versions(43)` extension, as a list in preference order with the most preferred first. In that list TLS 1.3 is `0x0304`, TLS 1.2 is `0x0303`, and TLS 1.1 and TLS 1.0 are `0x0302` and `0x0301`. A client willing to take either modern version therefore sends the pair `0x0304, 0x0303` in the extension and a frozen `0x0303` in the field. Read only the field, and a TLS 1.3 client is indistinguishable from a TLS 1.2 client. That is the point: nothing in the path has to understand the new version to pass the hello through unharmed. ## What the server sends back - A server that selects TLS 1.3 **MUST** send its own `supported_versions` extension in the `ServerHello`, containing exactly one value: the version it selected. Its `legacy_version` is frozen at `0x0303` too. - A `HelloRetryRequest` carries the extension the same way, because it has the same shape as a `ServerHello`. The version is therefore settled before the client sends its second `ClientHello`. - The server **MUST** select a version the client actually listed. The client's ordering expresses a preference; it does not bind the server, and implementations commonly take the highest version they enable. - If nothing in the client's list is enabled, the server aborts the handshake with a fatal `protocol_version(70)` alert. The rule about the `ServerHello` is worth reading backwards. If a client offered `0x0304` and the answer arrives with no `supported_versions` at all, the peer answered by the pre-1.3 rules. Whatever a data sheet claims, TLS 1.3 was not selected, and the version in force is whatever the legacy fields say - at most TLS 1.2. ## Where each value appears | Value | Where it is written | What it decides | |---|---|---| | `legacy_version 0x0303` in `ClientHello` | first field of the hello | nothing; frozen for every TLS 1.3 client | | `supported_versions(43)` in `ClientHello` | extension, ordered list | the set the client will accept | | `supported_versions(43)` in `ServerHello` or `HelloRetryRequest` | extension, one value | the negotiated version | | `TLSPlaintext.legacy_record_version` | record header | nothing; receivers ignore it | ## Why the negotiation moved into an extension The version field had become unusable for its own job. Implementations along the path - and some endpoints - had been observed dropping or refusing a hello whose version field named a value they did not recognise, so advertising a *new* version in the field was precisely the thing that made a connection fail. An extension does not have that problem: a receiver that does not recognise an extension is required to ignore it, so the same hello reaches an old peer as a well-formed TLS 1.2 hello and reaches a new peer as a TLS 1.3 offer. The trade is that the version is no longer where a naive reader looks for it. Anything that classifies traffic by the two bytes at the front of a hello is reading a constant. ## What this changes when you are diagnosing a connection 1. Do not conclude anything from the version field of a hello or a record; look at the `supported_versions` extension on each side. 2. A `ServerHello` **with** the extension names the selected version directly - a single value, not a list. 3. A `ServerHello` **without** it, in answer to a client that offered `0x0304`, is the observable signal that the peer never considered TLS 1.3. 4. A fatal `protocol_version(70)` alert means the two lists did not intersect within what each side enables - a configuration fact on one of the two ends, not a fault in the path. ## Version context All of the above is TLS 1.3 behaviour. In TLS 1.2 and earlier there was no `supported_versions` extension at all: the version fields of the two hellos were the negotiation, and a server selected the lower of its own maximum and the client's offer. A peer that speaks only those versions still negotiates that way, which is exactly why a TLS 1.3 client has to keep the old field filled in with a value such a peer accepts.
- Besides ClientHello and ServerHello, where does supported_versions appear in a TLS 1.3 handshake?In a `HelloRetryRequest`. It has the same shape as a `ServerHello` and carries `supported_versions` with the single selected version, so the version is settled before the client sends its second `ClientHello`. The client repeats its own version list unchanged in that second hello.
- What is the version field in the TLS record header for, once TLS 1.3 is negotiated?Nothing a receiver may act on. `TLSPlaintext.legacy_record_version` is written as `0x0303` - an initial `ClientHello` record is also permitted to carry `0x0301` - and receivers ignore it. The negotiated version never appears in a record header.
- A client lists TLS 1.3 first and TLS 1.2 second. Which one must a server that implements both select?Either, as far as the ordering goes. The server MUST select a version that appears in the client's list, and the list is ordered by the client's preference rather than as an instruction; implementations commonly take the highest version they enable. What the server may not do is negotiate a version the client never offered.
saying these in an interview costs you the question
- Says the negotiated version is whatever the ClientHello version field names
- Thinks a TLS 1.3 client advertises 0x0304 in legacy_version
- Believes the version is read from the record header
- Treats supported_versions in a ServerHello as optional when 1.3 was selected
- Thinks the extension carries one version from the client rather than a list