Change:
ClientHelloInner.  It MUST NOT be sent in either ClientHelloOuter or ClientHelloInner. <ins> Note that it is possible to implement decoding of the EncodedClientHelloInner in a way that creates a denial-of-service vulnerability.  Specifically, the server needs to check that each extension in the OuterExtensions list appears in the ClientHelloOuter.  The naive strategy would require O(N*M) time, where N is the number of extensions in the ClientHelloOuter and M is the number of extensions in the OuterExtensions list.  Malicious clients could exploit this behavior in order to cause excessive work for the server, possibly making it unavailable.  This problem can be mitigated by representing OuterExtensions in a way that allows it to be searched more quickly.  For example, the runtime can be improved to O(N*log(M)) by sorting the OuterExtensions and using binary search to access it. </ins> 5.2. To prevent a network attacker from modifying the reconstructed

Discussion:
A naive implementation of EncodedClientHelloInner decoding would require a quadratic amount of work, which clients might be able to exploit to DoS the server. This adds an implementation note intended to guide servers to mitigate this problem.
cc/ NAME :)
NAME let's do that change in a separate PR for now.
A naive implementation of decoding the inner ClientHello can easily go quadratic when processing outerextensions. Thus far, nothing in TLS has required searching for O(N) extensions, only O(implemented extensions). It's likely a lot of TLS implementations handle the extensions list linearly most of the time. outerextensions is now the first time you have to search for extensions you may not have implemented. A naive implementation would then go quadratic, which is a server DoS risk. It doesn't need to go quadratic (for instance, you can sort ClientHelloOuter's extensions and then binary search per lookup), but it's probably worth at least calling this out, if not switching to a simpler decoding scheme. (For a simpler decoder, we already require CHInner group the compressed extensions consecutively. We could do the same with CHOuter, at which point we only need to reference one byte slice, be it by offset/length or some goofier scheme.)
Switching to a simpler scheme: yes please. Just dropping the compression idea entirely: even better:-) Given this optimisation is really mostly for PQ schemes, I'd argue it'd be better to just drop the current idea and have a more constrained one, e.g. using a bit mask as the value of outer_extensions, so implementation know exactly which extensions can ever be copied from the outer CH. And I'd further argue we can leave defining all of that until later too. The current scheme also imposes quite a testing burden. At least with OpenSSL most extensions have some side-effect and testing that all combinations of those are handled correctly for the inner CH isn't trivial. (That's partly down to old code being a bit awful but I guess won't be simple for any code base.) Cheers, S.
Not sure I follow the concern with side effects. Are you processing the extensions as part of decompression? In order to make the handshake work, you'd have to construct the ClientHello by bytes anyway.

Thus it seems the natural implementation would be to resolve ECH early, before ClientHello processing, then compute the inner ClientHello as bytes, then run the handshake with the resulting ClientHello. This aligns nicely with the model in split mode, etc. The issue I have in mind is simply that the decoding process, a pure function from a pair of byte strings to a byte string, is easy to accidentally make inefficient.
It occurs to me this might cause some conflicts between wanting ECH-ful and ECH-less ClientHellos to look similar (don't stick out) and pressures on ClientHello extension order. The order should be irrelevant, but compat concerns have cropped up before. (If only we required extensions be sorted...) Another possibility for a simpler decode would be if outer_extensions listed indexes into the extension list, rather than extension IDs. Though we would need to answer silly questions like whether it's an index into ClientHelloOuter or ClientHelloOuterAAD.
Since this is more of an implementation detail, let's just add a warning to handle this responsibly (i.e. the sort and binary search).
Yet another way to address this is with a simple hash map, e.g.: URL My vote is to leave the encoding scheme as-is and simply leave a warning.
NAME NAME could one of you please propose text for that warning?
Another DoS risk to be careful of: implementations typically enforce size limits on TLS messages so they aren't forced to buffer 2^24 bytes. Whatever size limit is enforced on ClientHello needs to also be enforced on the inner ClientHello before allocating memory for it. In particular, if ClientHelloOuter has a single giant extension, and it is referenced multiple times in OuterExtensions, you can blow up the server's resource usage. (Though I can't think of any case without repeating an extension where the resulting size grows dramatically.)
Is there any valid reason why an extension would appear multiple times in a ClientHello? If not, maybe we should specify that the server must abort if OuterExtensions references an extension more than once.
Seems reasonable. I think the combination of "no duplicate extensions" and "no referencing the ECH extension" imply that sizeof(ClientHelloInner) < sizeof(ClientHelloOuter).
Keeping this opening to track NAME and NAME comments on CH sizes. NAME could you please send a PR if you have cycles?

Discussed in 110: Will propose text to address the second vector and then close this issue.
LGTM, though I guess we should either update it to resolve this other vector, or just make the PR not quite close the issue yet: URL (This other vector is even more fun because you can make a quadratically-sized ClientHello!)LGTM pending nits and suggestions from NAME