Internet Protocols · Protocol Design

Reserved Values and the Practice of Keeping Extension Points Alive

How unallocated code points, GREASE values, and deliberate noise keep implementations honest about skipping unknown fields, and what that costs on the wire.

An extension mechanism that is never used decays. A parser that has never seen an unknown field in its test suite, and rarely in its lifetime, will contain code paths that were never exercised, and those paths will be wrong. The failure is not hypothetical: the history of TLS contains multiple cases where a peer sent a value that was legal but unusual and a widely deployed implementation rejected the connection, and the resulting pressure pushed implementers toward permissive parsing that in turn broke the rules.

The response, formalised for TLS as GREASE and adopted in various forms elsewhere, is to make legal-but-unallocated values a normal part of traffic. The sender emits values from reserved ranges on purpose. The receiver must ignore them, because the specification says to. If the receiver fails, the failure is discovered during development rather than when a real extension is deployed.

Reserved ranges as a structural device

A registry allocates code points. Values that are not allocated are, by default, undefined, and a specification has to say what a receiver does with an undefined value. Three options exist: treat it as an error, ignore it, or treat it as reserved and require the receiver to reject messages that contain it in a position where it must be understood.

The third option is the one that makes GREASE possible. A range of values is designated as reserved for exactly this purpose, and the specification states that a receiver must ignore them if they appear in a position where ignoring is defined, and must not treat them as an error. TLS reserves sixteen two-byte values of the form 0x?a?a, where the two bytes are equal and the low nibble of each is 0xa: 0x0a0a, 0x1a1a, 0x2a2a, and so on through 0xfafa. The pattern is easy to generate from a small table and impossible to mistake for an allocated code point.

The pattern also has a useful property for the sender: the values are contiguous enough to be generated by a small table but spread enough that a receiver cannot optimise them away with a range check on a single interval. If all reserved values were contiguous, an implementation could special-case the interval and never exercise the general skip path. Spread values force the general path.

HTTP/2 and HTTP/3 use the same idea with a different arithmetic. HTTP/2 requires receivers to ignore settings identifiers they do not recognise, and HTTP/3 reserves frame types and stream types of the form 0x1f * N + 0x21 for non-negative integers N, a progression that skips through the type space rather than sitting in a block. That pattern cannot be identified by a contiguous range check, so a parser must compute it, which is the point: the general path is exercised rather than special-cased.

What the sender is actually buying

The immediate benefit is test coverage in the field. A client that sends a reserved extension on every connection ensures that every server it talks to exercises the skip path at least once per connection, and any server that fails will fail for many clients, producing pressure to fix it. That is a network-wide forcing function, and it works because the cost of compliance is low and the cost of non-compliance is visible.

The secondary benefit is that it detects over-strict parsers before they become a compatibility problem. A server that rejects unknown extensions will reject GREASE, and the failure appears during deployment of the server rather than during deployment of the first real extension that needs to be ignored. The ordering matters: a failure that happens when nobody depends on the feature is cheap; the same failure when a feature is being rolled out is expensive.

The third benefit is subtler. Reserved values give implementers a way to test their own handling without coordinating with a peer. A test harness can send a message with a reserved extension and assert that the connection proceeds normally. Without reserved values, the test would have to invent a private code point, and private code points are exactly what registries are designed to prevent.

There is a cost, and it is small but real. Reserved values consume bytes on the wire and branches in the parser. A TLS ClientHello carrying a few reserved extensions is a few dozen bytes larger, and the server does a few extra iterations of a loop. For a handshake that already costs a round trip, this is noise. For a protocol with a tight per-message budget, it would not be.

The interaction with criticality

GREASE and the critical bit interact in a way that is easy to get wrong. A reserved value marked essential would require a receiver to fail the message, which defeats the purpose. The specification therefore requires that reserved values be sent in a form that is ignorable: in TLS, the reserved extensions are sent without any semantic claim, and the reserved cipher suites and signature algorithms are placed in lists where an unselected value has no effect.

The general rule is that the noise must be placed where ignoring is already the defined behaviour. A reserved value in a position that requires action cannot be ignored, so it cannot be used as noise. That constrains where GREASE can be applied: it works for lists of options, for extension blocks, for settings frames, and for any field whose semantics are “select one” or “process what you recognise”.

A protocol that has no such position has no place to put noise. That is a design smell: it means every field is load-bearing, and there is no room for a future field that a receiver might not understand. The fix is structural, not additive: some part of the message has to be redefined as an ignorable list.

Ossification is the thing being prevented

The mechanism behind GREASE’s necessity is ossification, where an implementation or a middlebox becomes unable to accept anything outside the set of messages it has seen. A firewall that parses TLS ClientHello to extract the server name will, if written carelessly, reject a ClientHello with an extension it does not recognise, because its parser was written against a fixed layout. Once enough of those devices exist, sending a new extension breaks connectivity for the users behind them, and the extension is effectively unusable regardless of what the endpoints support.

The dynamic is a ratchet. Each new middlebox narrows the set of acceptable messages; each narrowing makes the next extension harder to deploy; the difficulty of deployment makes protocol designers add escape hatches, and the escape hatches make middleboxes more suspicious. Breaking the ratchet requires either making the middlebox’s job unnecessary or making its strictness fail early.

GREASE addresses the second path. It makes strictness fail during development, when the middlebox vendor can still fix it. It does not help with middleboxes that are already deployed and will never be updated. For those, the only remedies are encryption of the parts that middleboxes should not read, or a change in transport that makes the parsing impossible. The TLS 1.3 design chose encryption: the handshake is encrypted after the ServerHello, so a middlebox sees only the first flight. That choice is examined in middlebox ossification and frozen invariants.

The complementary approach is to make the protocol’s extension points deliberately variable in ways that carry no information. Randomised padding, randomised record sizes, and randomised ordering all serve the same purpose: they make a middlebox’s assumptions unreliable without requiring the middlebox to be updated. The cost is bandwidth and, for padding, a small amount of CPU.

# GREASE values: two equal bytes, each ending in 0xa -> 0x0a0a .. 0xfafa
GREASE_BYTES = [0x0a, 0x1a, 0x2a, 0x3a, 0x4a, 0x5a, 0x6a, 0x7a,
                0x8a, 0x9a, 0xaa, 0xba, 0xca, 0xda, 0xea, 0xfa]

def grease_values(width):
    """Yield reserved values of the given byte width; every byte ends in 0xa."""
    for b in GREASE_BYTES:
        v = b
        for _ in range(width - 1):
            v = (v << 8) | b
        yield v

def is_grease(value, width):
    """True if the value is 'width' bytes and every byte has low nibble 0xa."""
    if value < 0 or value.bit_length() > width * 8:
        return False
    return all(((value >> (8 * i)) & 0xff) & 0x0f == 0x0a
               for i in range(width))

The generator is deliberately trivial. A sender that has to compute an expensive value will not bother, and a receiver that has to do more than a nibble check will be tempted to special-case the pattern and skip the general path.

When the noise becomes a signal

Any value a sender chooses randomly can leak information if the choice is correlated with something else. A GREASE value chosen from a small set, with a distribution that differs between implementations, becomes a fingerprint. The specification’s answer is to require that the value be selected randomly per connection and that implementations not reuse a value across connections.

There is a stronger version of the problem for padding. Padding that is added only when a message would otherwise be a particular size reveals the size it was padding to, and padding that is added with a length drawn from a distribution reveals the distribution. The defence is to pad to a size that is itself drawn from a distribution independent of the content, which is what traffic-analysis-resistant designs attempt.

The general principle is that noise must be indistinguishable from the real variation the protocol already has. If the protocol has no natural variation, adding noise creates a signal, because the noise is the only thing that varies. That is why GREASE works in TLS, where the extension list already varies by implementation and configuration, and why it would not work as well in a protocol with a fixed message shape.

The registry side of the bargain

Reserved values only work if the registry keeps them reserved. The Internet Assigned Numbers Authority maintains registries for TLS extension types, cipher suites, HTTP/2 settings, and DNS record types, and each registry has an allocation policy. The policies differ: some ranges are first-come-first-served, some require specification and expert review, some are reserved for private use.

A private-use range is a pressure valve. It lets an implementation experiment without consuming a public code point, and it keeps the experiment out of the public registry until it is proven. The cost is that private values are not interoperable, so an experiment that wants to be deployed has to move to a public code point, which means a change in the encoding and a transition period where both are accepted.

The failure mode of registries is exhaustion combined with squatting. A one-byte space with 256 values, a first-come policy, and a habit of allocating values for vendor-specific features will run out, and the remaining values will be contested. The design response is to size the space generously at the start, to separate the experimental range from the standards range, and to require that allocated values come with a specification that a third party could implement. Those are policy decisions, but they determine whether the wire format’s extension points remain usable a decade after the protocol is published.

The strongest form of the bargain is a periodic audit of three things: whether the reserved values are still being sent, whether they are still being ignored, and whether any implementation has started to depend on their absence. A reserved value that receivers have begun to reject is a signal that the ossification has already happened, and the remaining choices are to re-educate, to encrypt around the problem, or to accept that the extension point is closed. Tolerant parsing and its limits covers why the re-education path is narrower than it appears.

References

  1. RFC 8701: Applying Generate Random Extensions And Sustain Extensibility (GREASE) to TLS Extensibility — IETF
  2. RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — IETF
  3. RFC 9170: Long-Term Viability of Protocol Extension Mechanisms — IETF
  4. RFC 9114: HTTP/3 — IETF
  5. RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) — IETF