Change:
extension. OuterExtensions consists of one or more ExtensionType values, each of <del> which reference an extension in ClientHelloOuter, and a digest of the complete ClientHelloInner. </del> <ins> which reference an extension in ClientHelloOuter. </ins> When sending ClientHello, the client first computes ClientHelloInner, including any PSK binders.  It then computes a new value, the EncodedClientHelloInner, by first making a copy of ClientHelloInner. <del> It then MAY substitute extensions which it knows will be duplicated in ClientHelloOuter.  To do so, the client computes the digest of the entire ClientHelloInner message as: where "inner" is the ClientHelloInner structure and "Extract", "Expand", and "Nh" are as defined by the KDF API in I-D.irtf-cfrg- hpke.  Then, the client removes and replaces extensions from EncodedClientHelloInner with a single "outer_extensions" extension. Removed extensions MUST be ordered consecutively in ClientHelloInner. The list of outer extensions, OuterExtensions.outer_extensions, includes those which were removed from EncodedClientHelloInner, in the order in which they were removed. </del> <ins> It then replaces the legacy_session_id field with an empty string. The client then MAY substitute extensions which it knows will be duplicated in ClientHelloOuter.  To do so, the client removes and replaces extensions from EncodedClientHelloInner with a single "outer_extensions" extension.  Removed extensions MUST be ordered consecutively in ClientHelloInner.  The list of outer extensions, OuterExtensions, includes those which were removed from EncodedClientHelloInner, in the order in which they were removed. </ins> Finally, EncodedClientHelloInner is serialized as a ClientHello structure, defined in Section 4.1.2 of RFC8446.  Note this does not include the four-byte header included in the Handshake structure. The client-facing server computes ClientHelloInner by reversing this <del> process.  It scans EncodedClientHelloInner for an "outer_extensions" extension.  If there is none, the ClientHelloInner is the EncodedClientHelloInner.  Otherwise, it replaces the extension with the corresponding sequence of extensions in the ClientHelloOuter.  If any referenced extensions are missing, the server MUST abort the connection with an "illegal_parameter" alert.  The server then computes the digest of the reconstructed ClientHelloInner.  If the digest does not equal OuterExtensions.inner_digest, then the server MUST abort the connection with a "decrypt_error" alert. </del> <ins> process.  First it makes a copy of EncodedClientHelloInner and copies the legacy_session_id field from ClientHelloOuter.  It then looks for an "outer_extensions" extension.  If found, it replaces the extension with the corresponding sequence of extensions in the ClientHelloOuter.  If any referenced extensions are missing or if "encrypted_client_hello" appears in the list, the server MUST abort the connection with an "illegal_parameter" alert. </ins> The "outer_extensions" extension is only used for compressing the ClientHelloInner.  It MUST NOT be sent in either ClientHelloOuter or ClientHelloInner. <ins> 5.2. To prevent a network attacker from modifying the reconstructed ClientHelloInner (see flow-clienthello-malleability), ECH authenticates ClientHelloOuter by deriving a ClientHelloOuterAAD value.  This is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension removed.  ClientHelloOuterAAD is then passed as the associated data parameter to the HPKE encryption. Note the decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the unauthenticated portion of ClientHelloOuter is not incorporated into ClientHelloInner. </ins> 6. 6.1.

Discussion:
The first commit shaves 33 bytes (or 49 with SHA-384) from the ciphertext by replacing inner_digest with ClientHelloOuterAAD. This also allows for more compact ClientHelloInner encodings when non-extension fields are expected to match. The second commit shaves a further 32 bytes. We could probably further optimize the encoding but I've left it alone here. In particular, if we merge , all non-extension fields may as well be implicit. We could then replace the encryption payload with a plain extensions block.
I guess that doesn't quite work, but we could replace it with the new random value, and an extensions block. Something like: Anyway, a topic for a later PR, depending on how strongly we want to match cipher suites for . The size savings are a bit more marginal beyond that.
I like this change a lot. At some point while implementing ECH, I noted that it would be nice to have an explicit binding of CHOuter to the handshake in case of ECH acceptance. This provides that.
Rebased and review comments addressed. The PR should also no longer be in a confusing state if you look at the diff.
We previously decided not to bind the outer ClientHello to the ECH decryption, because doing so is rather a hassle. We'd need something similar to the TLS 1.3 PSK binder mess, with the added complexity of ordering relative to the PSK binders themselves. Additionally, while the binding helps with some of the don't stick out properties, there were other attacks on distinguishability anyway. So, instead, we mitigated the inner ClientHello malleability with OuterExtensions.innerdigest and left the outer ClientHello unbound. However, we've since decided not to allow PSKs in the outer ClientHello. Perhaps we should revisit this. Suppose the AD parameter to the ECH decryption were the entire outer ClientHello, minus the ECH extension. I believe this means we no longer need OuterExtensions.innerdigest. That would give an immediate 33-byte savings. It would then unlock a further 32 byte savings by amending to say: Client always encodes an empty legacysessionid in ECH plaintext. Client-facing server copies outer legacysessionid to inner ClientHello as part of decompression.

(The reason we can't do this easily right now is that, if the inner ClientHello lacks outerextension, nothing's authenticating legacysessionid. Since the field in TLS 1.3 is all but ignored, it's "obvious" that the malleability doesn't matter, but it's nicer to not need to think about such things.) From looking at Chrome's ClientHellos and working through what we'd send (drop 1.2-only extensions, compress most), it looks like the inner ClientHello will be maybe 130-140 bytes, plus a padded servername. The 65 bytes above would almost halve the overhead. In some sense what's going on here is our 16-byte tag on the AEAD in the ECH ciphertext is going to waste. We didn't authenticate everything that goes into reconstructing the inner ClientHello (outerextensions), so instead we authenticate another hash (OuterExtensions.innerdigest) and check that afterwards. (To that end, if we do this, we probably need to say the server MUST reject encryptedclienthello showing up in outer_extensions, that way all inputs to decompression are authenticated. Equivalently, decompression is done relative to the ECH-extension-less AD input rather than the original outer ClientHello.)
I like this idea. How do you think the AD ought to be structured? The ClientHelloOuter, but with an an empty "encryptedclienthello" extension?
Yeah, that's seems the simplest to me. Filling it with zeros, as NAME pointed out to me, requires predicting the length of the encrypted inner ClientHello. This isn't strictly impossible (ADs don't change ciphertext length), but it seems needlessly fussy and ECH is already requiring we be able re-encode ClientHellos, lengths and all. Note this assumes there are no computed PSK binders in the outer ClientHello because otherwise there's a circular dependency. (If we wanted to allow that, the AD would need to zero/trim the binders or so.)
Another benefit is it stops the attacker from probing information about compressed extensions. Currently an attacker can learn which extensions are compressed in the inner ClientHello by removing them from the outer ClientHello and seeing if the server still responds.

This isn't a huge deal, as compressed extensions are assumed public, but this way we remove that vector and leave only the fuzzier signal from the ciphertext length.
On second thought, a lot of implementations will need to do that prediction anyway. The workaround for the old F5 bug means we'll need to know the eventual size for the padding extension. (On the other hand, you can probably assume anyone doing ECH is not running those things. On the other hand, turning off the workaround would stick out.) Ah well. We did it for PSK already. (I wonder if we can do drop that workaround now that TLS 1.0 and 1.1 are gone...)
What's the "old F5 bug"? What's the value in authenticating the length of the "encryptedclienthello" extension? This is authenticated by the handshake already, no?
URL URL It's why we have a padding extension. There isn't. It's just a question of what's easier to deal with. Filling it with zeros means that, when computing the AD on the receiving side, you don't need to fully reconstruct a modified outer ClientHello with updated length prefixes. We avoided this when computing the PSK binder in RFC8446. It's a mild hassle but not a huge deal. More importantly, we've thoroughly given up on that elsewhere in the spec. The F5 padding business also means computing this AD will likely take some length prediction anyway, but so it goes.
I agree, it's not a big hassle.
Rather than make the extension empty, can we just omit the and fields? (The and should arguably be included, a la ).
That works, though it's even more fussy for the client to construct, since it needs to half-encode the ECH extension before fully encoding it. is already bound into HPKE itself. is less clear, but I thought our model already said all this didn't matter? Isn't the binding here just to replace and provide malleability? If there's something more going on, I'd like us to have some principled answer here, so we can answer questions like... what about the handshake transcript, which may not be empty on HRR? If we do that, what's going on with echhrr_key?

The principle I was following is: include as much as possible in the AD. Yeah, it's more of a pain, and we've gotten away with no binding whatsoever up to now, so maybe we don't need it. I'm happy to start with an empty ECH extension and then consider the other parts later, if that'll help.
I don't feel very strongly either way.
Let's just do the simpler thing now, then! :-) The following errata report has been verified for RFC8996, "Deprecating TLS 1.0 and TLS 1.1". You may review the report below and at: URL Status: Verified Type: Editorial Reported by: Gaëtan Leurent Date Reported: 2024-02-03 Verified by: RFC Editor Section: 10.2 Original Text [Bhargavan2016] Bhargavan, K. and G. Leuren, Corrected Text [Bhargavan2016] Bhargavan, K. and G. Leurent, Notes The last name "Leurent" is misspelt is the references. RFC8996 (draft-ietf-tls-oldversions-deprecate-12) Title : Deprecating TLS 1.0 and TLS 1.1 Publication Date : March 2021 Author(s) : K. Moriarty, S. Farrell Category : BEST CURRENT PRACTICE Source : Transport Layer Security Area : Security Stream : IETF