TLS Encrypted Client Hello
Extracted elements (31)
ECH does not provide forward secrecy for ClientHelloInner because the server's ECH key is static (long-lived DNS key). The exposure window is bounded by the key lifetime, so servers are RECOMMENDED to rotate ECH keys regularly.
ECH supports two topologies: Shared Mode (client-facing and backend server are the same entity) and Split Mode (client-facing server relays to a separate backend). In Split Mode, the client-facing server never sees the plaintext of the inner connection, and the spec assumes the inter-server channel is authenticated and correlation-resistant.
GREASE ECH (Section 6.2) is included so that ECH-capable clients without an ECHConfig still send a fake ECH extension, providing 'cover' for real ECH connections and disincentivizing middleboxes from differentially blocking the extension. This addresses the RFC 8744 requirement to 'not stick out'.
TLS-terminating middleboxes that comply with RFC 8446 Section 9.3 will ignore the encrypted_client_hello extension and generate their own ClientHello for the plaintext server_name (the public name), preserving interoperability. A non-authoritative proxy cannot forge a signal to disable ECH.
After ECH rejection, if the server supplied valid retry_configs, the client MUST abort the current connection with an ech_required alert after handshake completion and SHOULD retry on a new transport connection using the retry configurations. The client MUST NOT fall back to unencrypted ClientHellos and MUST NOT use IP addresses from a new DNS query for the retry.
Client-facing servers SHOULD allocate distinct config_id values for each ECHConfig in their known set, using rejection sampling (randomly selecting until no collision with known values), to avoid performance penalties from trial decryption when multiple configs are active.
ClientHelloOuter is authenticated as HPKE associated data by constructing ClientHelloOuterAAD—a serialized ClientHello matching ClientHelloOuter but with the payload field of the encrypted_client_hello extension replaced by zeros—binding the outer to the inner and preventing attacker modification.
Clients SHOULD validate ECHConfig.contents.public_name before use and SHOULD ignore any ECHConfig whose public_name is not a valid dot-separated sequence of LDH labels or whose final label would be interpreted as an IPv4 literal (all-digit label or 0x/0X prefix).
If all candidate ECHConfig values fail decryption, the client-facing server MUST proceed with ClientHelloOuter and, if configured with any ECHConfigs, MUST include the encrypted_client_hello extension in EncryptedExtensions with retry_configs set to one or more up-to-date ECHConfig structures.
If the client-facing server accepts ECH, when it receives the second ClientHelloOuter after HelloRetryRequest it MUST verify that cipher_suite and config_id are unchanged and that enc is empty; failure MUST result in an illegal_parameter abort. Decryption of the second payload reuses the original HPKE context.
If the client has no compatible ECHConfig, it SHOULD send a GREASE encrypted_client_hello extension with random config_id, a supported cipher suite, a randomly generated encapsulated key, and a random payload of L+C bytes matching the size of a real ECH payload with padding.
In the absence of an application profile specifying otherwise, a compliant ECH implementation MUST support the HPKE cipher suite: KEM DHKEM(X25519, HKDF-SHA256), KDF HKDF-SHA256, and AEAD AES-128-GCM.
The server MUST abort with an illegal_parameter alert if any padding byte in EncodedClientHelloInner is non-zero, if any referenced outer extension is missing, if any extension is referenced more than once in OuterExtensions, if encrypted_client_hello is itself referenced, or if the outer extensions are out of order.
When constructing ClientHelloInner for ECH, the client MUST NOT offer TLS 1.2 or below, MUST NOT offer to resume any TLS 1.2-or-below session, MUST include the encrypted_client_hello extension of type inner, and MUST NOT choose a cipher suite or ECH version not advertised in the selected ECHConfig.
When ECH is rejected, the client MUST verify the server certificate is valid for ECHConfig.contents.public_name, interpreting it as a DNS-based reference identity per RFC 9525. If the server requests a client certificate, the client MUST respond with an empty Certificate message. The TLS implementation MUST NOT report the connection as successful to the application.
When offering a PSK in ClientHelloInner, clients SHOULD send a GREASE pre_shared_key extension in ClientHelloOuter with randomized identities and binders of matching length to avoid revealing to observers that a PSK extension is present without solicitation. The client MUST NOT use this extension to advertise a real PSK to the client-facing server.
A malicious client-facing server can distribute unique per-client ECHConfig values as tracking vectors. Clients can mitigate by flushing DNS and ECHConfig state when changing networks. ECHConfig rotation rate is also a trade-off: too-frequent rotation reduces the anonymity set size.
ECH uses an ECHConfig structure to publish the server's HPKE public key and associated metadata. The structure includes a version (0xfe0d), a HpkeKeyConfig (config_id, kem_id, public_key, cipher_suites), maximum_name_length, public_name (the client-facing server's DNS name), and an extensions list.
For HelloRetryRequest, the backend server signals ECH acceptance via an encrypted_client_hello extension carrying hrr_accept_confirmation = HKDF-Expand-Label(HKDF-Extract(0, ClientHelloInner1.random), "hrr ech accept confirmation", transcript_hrr_ech_conf, 8), computed over the HRR with the extension payload initially set to 8 zero bytes.
The backend server signals ECH acceptance by computing accept_confirmation = HKDF-Expand-Label(HKDF-Extract(0, ClientHelloInner.random), "ech accept confirmation", transcript_ech_conf, 8) and overwriting the last 8 bytes of ServerHello.random with this value, where the transcript includes a ServerHello with those 8 bytes initially set to zero.
The ech_outer_extensions extension (type 0xfd00) inside EncodedClientHelloInner allows the client to reference extensions already present in ClientHelloOuter rather than repeating them. The referenced extensions MUST appear contiguously and in the same relative order in ClientHelloOuter.
The recommended padding scheme pads the SNI extension by max(0, M - D) bytes using ECHConfig.maximum_name_length M, or M+9 bytes if no SNI is present. After per-extension padding, the total EncodedClientHelloInner length is rounded up to the next multiple of 32 bytes to reduce length fingerprinting.
RFC 9849 creates the new ECH Configuration Extension registry (Section 11.3). Extension types with the high-order bit set are mandatory; clients MUST ignore ECHConfig structures containing unrecognized mandatory extensions. A set of Reserved codepoints is defined for server GREASE use and will never be assigned.
RFC 9849 registers the encrypted_client_hello extension (type 0xfe0d) and the ech_outer_extensions extension (type 0xfd00) in the IANA TLS ExtensionType Values registry, and registers the ech_required alert in the TLS Alert Registry.
ECH configurations delivered via unauthenticated DNS can be stripped or replaced by an attacker who controls DNS, but this does not worsen the situation significantly since such an attacker can already reroute connections. DNSSEC and encrypted DNS transports (DoH, DoT, DoQ) are recommended mitigations.
ECH's three primary security goals are: (1) security preservation—ECH does not weaken TLS; (2) handshake privacy—connections to any server name in an anonymity set are indistinguishable from connections to any other name in that set; and (3) downgrade resistance—attackers cannot force a non-ECH connection. These properties were formally proven in the ECH-Analysis paper.
To prevent a packet amplification attack, the server enforces that ech_outer_extensions references must appear contiguously, not be duplicated, and not reference encrypted_client_hello itself. Additionally, server implementations SHOULD construct ClientHelloInner in linear time to avoid quadratic denial-of-service from naive extension expansion.
ECH acceptance is determined from the server's first response: if TLS < 1.3 is negotiated, ECH is rejected. If ServerHello, the client checks accept_confirmation against the last 8 bytes of ServerHello.random. If HelloRetryRequest, the client checks for an encrypted_client_hello extension; an 8-byte match against hrr_accept_confirmation indicates acceptance; absent extension means rejection.
EncodedClientHelloInner wraps ClientHelloInner with a zeros padding field: struct { ClientHello client_hello; uint8 zeros[length_of_padding]; }. The legacy_session_id field is set to the empty string before encryption, and all padding bytes MUST be zero.
The ECHConfigList is a length-prefixed sequence of ECHConfig structures (ECHConfig ECHConfigList<4..2^16-1>), ordered from most to least preferred, allowing servers to advertise multiple ECH versions and parameter sets simultaneously.
The encrypted_client_hello extension (type 0xfe0d) carries an ECHClientHello with an outer or inner type. The outer variant includes cipher_suite, config_id (1 byte), enc (HPKE encapsulated key, empty on HRR), and payload (encrypted EncodedClientHelloInner). The inner variant has an empty payload.