Original text:
match a value provided in the corresponding "ECHConfig.cipher_suites" list. <del> A cryptographic hash of the "ECHConfig" structure from which the ECH key was obtained, i.e., from the first byte of "version" to the end of the structure.  This hash is computed using the hash function associated with "cipher_suite", i.e., the corresponding HPKE KDF algorithm hash. </del>  The HPKE encapsulated key, used by servers to decrypt the corresponding "encrypted_ch" field.

Reviewer comments:
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.
Let's close this issue and leave the mechanism as is.
Hrm, before closing I think it might be worth noting, in the spec, the stronger security property this provides.
I think we should also consider binding the CHInner to a hash of the whole CHOuter (minus the ClientEncryptedCH extension). That would have the same size overhead, but would give us a stronger property: modifying anything in CHOuter (not just one of the copied extensions) invalidates the whole thing.
The point of hashing outer extensions is to provide integrity of the ClientHelloInner. What would be the point in hashing more of the ClientHelloOuter? (Not a rhetorical question :) It seems conceptually different, and I'm curious what the security benefit might be.)
If we authenticate the ClientHelloOuter, we'd be authenticating all the inputs that go into the ClientHelloInner, so I think that would also provide integrity. I expect Ben is suggesting this in the context of URL, where GREASE can additionally be made a little stronger if we did that. Getting that to interact well with any PSK binders in ClientHelloOuter is tricky though, unless we decide to ban resumption attempts in outer ClientHellos. (Probably best to keep the GREASE-related conversation in one place. Though I see I didn't help on that front because some discussion is also in . Ah, GitHub.)
Yep, I'm just echoing NAME observation. See also URL
Today, the only thing that gets copied into ClientHelloInner is extensions, so I don't see how it's an improvement in terms of our primary security goal. (Not sure about the "don't stick out" benefit.)
I don't understand how hashing all of CHOuter is going to work. In split mode, the server never sees any of CHOuter.
Yes, the hash would be checked when reconstructing the inner ClientHello (as here), and would not be present in the reconstructed ClientHello.
There's not much discussion here. Unless there's objection, shall we leave the mechanism as-is and close out this issue? cc/ NAME
Works for me.
From Section 6.1: This assumes that the KDF specifies some underlying hash function. From the HPKE spec, it seems like all that's required of the KDF is that it's an "extract-then-expand" KDF: URL In fact, the currently assigned code points are all for HKDF: URL But there's no requirement that the KDF always have some underlying hash function. As an alternative to HKDF, one can imagine a dedicated primitive that satisfies the extract-then-expand API but is not constructed from a hash function.
Impacted fields: ClientEncryptedCH.record_digest URL
We can probably replace with and be done with it.
Putting on my theorist hat, I'd say that the extract function is a "randomness extractor" and not a collision resistant hash function. We want the latter in this context. From a practical stand point, using the extract function is more hashing than we need. I'd suggest doing away with (see ) and figuring something else out for . Could we simply require HKDF be used with HPKE?
Sure, but in practice randomness extractors use a collision resistant hash or PRF (CBC) under the hood. I don't think we can require HKDF for HPKE. That seems too restrictive. We might just say that ECH requires HKDF-based HPKE ciphersuites, as HKDF is already paramount for TLS 1.3, and then just reference HKDF's hash function.
addresses this issue.
As I mentioned on the list, we can use the KDF in place of a hash.
I tend to agree. I'll create a new PR.
Have a look at , NAME
Resolved in . Closing.

Revised text:
match a value provided in the corresponding "ECHConfig.cipher_suites" list.  <ins> Equal to "Expand(Extract("", config), "ech_config_digest", Nh)", where "config" is the "ECHConfig" structure and "Extract", "Expand", and "Nh" are as specified by the cipher suite KDF. (Passing the literal """" as the salt is interpreted by "Extract" as no salt being provided.) </ins> The HPKE encapsulated key, used by servers to decrypt the corresponding "encrypted_ch" field.