Code change:
When sending ClientHello, the client first computes ClientHelloInner, including any PSK binders, and then MAY substitute extensions which it knows will be duplicated in ClientHelloOuter.  To do so, the <del> client computes a hash of the entire ClientHelloInner message with the same hash as for the KDF used to encrypt ClientHelloInner.  Then, the client removes and replaces extensions from ClientHelloInner with a single "outer_extensions" extension.  The list of "outer_extensions" include those which were removed from </del> <ins> client computes a hash 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 ClientHelloInner with a single "outer_extensions" extension.  The list of "outer_extensions" include those which were removed from </ins> ClientHelloInner, in the order in which they were removed.  The hash contains the full ClientHelloInner hash computed above.

Related discussion:
text: Per NAME suggestion, this replaces computations involving the "hash underlying the KDF" with evaluation of the KDF itself. This respects the API defined in the HPKE draft. This supersedes .
Hey NAME and NAME I rolled up your changes! Please have another look before merging.
Rebased and rolled up.
Thanks -- let's let this stew until Friday, at which point we can merge.
Rebased.
Field is computed as "digest of the complete ClientHelloInner" (including presumably, the "outer_extensions" extension). I don't understand the analytical value of this hash. Can anyone explain its reasoning?
The hash is intended to prevent the attacker from influencing the reconstructed CHInner by manipulating CHOuter. Consider the contrived example of an extension which is separately protected but only works for SNI-A and not SNI-B. E.g., it's constructed by doing E(K_sni, SNI). If that extension were included in CHOuter and then imported into CHInner via this mechanism, then an attacker could substitute it and use a reaction attack to determine whether the CH referred to A or B. However, with the hash (which may be not well-specified), this attack gets caught by the client-facing server and the handshake is rejected without leaking information.
My initial understanding of this mechanism was incorrect. What happens is that the hash is computed over the ClientHelloInner that is consumed by the backend server. This is intended to ensure that if ECH accepts, then the backend server consumes the CH intended by the client. (PR URL makes this more clear, I think.) I wonder if this mechanism is redundant. Agreement on the CH between the client and backend server is provided by the binding of the transcript hash to the handshake traffic secret, so any attempt to manipulate the ClientHelloInner by fiddling with the ClientHelloOuter should be detected. On the other hand, the binding of the outer extensions to ClientHelloInner ensures agreement on the CH prior to the backend server consuming it. This is a strictly stronger property, which may or may not be useful. I agree that the attack works, but the example seems really contrived :) I'd be interested if there are more natural extensions for which this extra layer of security would be useful.
From the interim meeting, the consensus seems to be that we should err on the conservative side.