the one hand, the requirements on info seem weaker, but maybe actually this needs to be secret?  Analysis needed.]] <del> [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified integration with the backend server.  Stateless HRR on the client- facing server works fine, however.  See issue #333.]] </del> 6.4. If the client attempts to connect to a server and does not have an 7.1.1. <del> It is an error for the client to offer ECH before the HelloRetryRequest but not after.  Likewise, it is an error for the client to offer ECH after the HelloRetryRequest but not before.  If either of these conditions occurs, then the client-facing server MUST abort the handshake with an "illegal_parameter" alert. </del>  7.2.

A related PR that we probably should make sure flow together: URL
If a server implements ECH decryption via an RPC call, it's possible for the client's ECH keys to be valid for CH1 but invalid for CH2, perhaps due to a race condition that updates the server's keys across these messages. This should be exceedingly rare in practice, so failing the connection with an alert is probably fine, but we should probably be clear about this. cc NAME
There are two ways a accept-then-HRR-then-reject can happen: The client sends a after HRR that differs from the it sent before HRR. The is the same, but the server no longer recognizes it. The latter is the server's fault, and I think the only thing to do is abort with "internalerror". However, I think requiring that the doesn't change is a good idea. I wonder if this is implied by the client behavior? In any case, I think we should have the server abort with "illegalparameter" if the changes over HRR.
Is this any different from, say, the server's cipher suite capabilities changing in between CH1 and CH2 and thus the server not being able to meet what they said with HRR? I guess I've always assumed that, within the context of a single "handshake" from the perspective of the client, we expect the server to behave self-consistently and, if it can't, I guess the server can internal_error and live with failing one or two connections across server restarts.
I think this is a little different, since it can be triggered by changing the set of ECHConfigs the server knows, and not necessarily the set of cipher suites. I agree. And even though the situation is a bit different, I think "internalerror" is appropriate here as well. However, I also think the client SHOULD NOT change the ECHConfig across HRR. If it does, I think the server SHOULD abort with "illegalparamter". Do you think this would be hard for the server to enforce? In particular, a QUIC server might want to handle the HRR path statelessly, and this check requires remembering the previous .
I think that's already true. The client can't change things across HRR unless explicitly allowed. (URL) Exactly what the server enforces on HRR is... all over the place. I'm normally in favor of maximal enforcement, but enforcing all the rules of HRR is such a mess. It's especially messy for folks who do stateless HRR, because any state carried over across the two increases the size of the cookie. So I think we need to be a little careful here. Honestly, I think we didn't really get HRR right. QUIC does not care about stateless HRR. It did in the original design, but that's been dropped. DTLS does, though. And I believe NSS always implements it statelessly? (BoringSSL currently only does stateful.)
It sounds like a weak "MAY" would be most appropriate.
Can you elaborate?
Looking at this again, I suspect the minimal thing that needs to be checked across CH1 and CH2 is that the server cannot change its mind on whether it used ECH in CH2 and, if using ECH, it needs to have remembered echhrrkey (and HPKE cipher suites?) to bind the two. Dunno if it makes sense to just also remember the config_id at that point.
I agree, but I don't think tracking the is sufficient. Suppose the client sends an acceptable in CH1 but a rejecting in CH2. In this case doesn't provide the binding we need, since the client-facing server won't attempt to decrypt the payload in CH2. A conservative way to ensure the server doesn't change its mind is as follows: Require that the does not change across HRR. If it does, then abort with "illegalparameter" alert. Require that the does not change across HRR. If it does, then abort with "illegalparameter" alert. If isn't recognized after HRR --- say because of key rotation -- then abort with "internalerror" alert.  I'm not sure 1. is strictly necessary. If the isn't supported by the ECH configuration, then the server would abort with "illegalparameter". (This is already specified.) Moreover, I don't think we have any agility issues here, given that the derived AEAD key is bound to the cipher suite. However, this does ensure that the server chooses the HPKE cipher suite and KEM key in the same way before and after HRR. This may turn out to be useful.
Right, this is what I mean by "the server cannot change its mind on whether it used ECH in CH2". If you declined ECH in CH1, you ignore ECH in CH2. If you accepted ECH in CH1, failure to handle ECH in CH2 is fatal. But, yeah, if you remember the config used, you can check a bit more accurately, so maybe we should just do that and ask stateless HRRs make an even larger cookie. (Need to be a bit careful about itself because we allow a trial decryption flow too.)
For what it's worth, I'm not a huge fan of CH2 having a fresh HPKE exchange in the first place. That seems an unnecessary asymmetric operation. Were it not for stateless HRR, I think I'd advocate we just make CH2's ECH payload a second pop at the HPKE context's Seal() function (next sequence number), with the rest ignored. But with stateless HRR, we need to serialize the HPKE context into the cookie, which is mildly annoying.
I don't see how you can avoid CH2 having a new payload. Unless you are looking to just perform address validation (DTLS), you only send HRR to correct something in the ClientHello.
I think what NAME means is that the AEAD ciphertext could change without changing the encapsulating key. Hence, the ClientHelloInner can be changed without updating the HPKE shared secret. Wouldn't this leak the HPKE encryption key? The cookie extension is transmitted in the HRR in plaintext, right?
Specifically, HPKE supports multiple sequential encryption calls on a context with an internal sequence number. URL The cookie already needs to encrypted and authenticated by the server like tickets. It certainly needs to be authenticated independent of ECH, otherwise the attacker can manipulate it anyway. And then the existing echhrrkey construction requires encryption too. (If you didn't encrypt the cookie for some reason and only MAC'd it, this design would allow you to, rather than serializing the HPKE encryption key, remember the expected HPKE config, redo the KEM operation, and bump the sequence number up. But if you're remembering some secret for the MAC, may as well encrypt it...)
Right, thanks for again clarifying a misunderstanding of mine about the TLS spec :) Incidentally, I brought up specifying HPKE context serialization here: URL There doesn't seem to be a lot of appetite for it. I suppose we could specify it in ECH, but there are so many use cases for it that I think it would be worth specifying it in the HPKE draft.
Given the uncertainty about stateless HRR pointed out in , let's permit the client-facing server to abort if ECH usage changes after HRR. This allows stateful-HRR servers to enforce this behavior, but leaves it as an "OPEN ISSUE" about how stateful-hRR servers will enforce this. I'll prepare a PR tomorrow or Thursday.

the one hand, the requirements on info seem weaker, but maybe actually this needs to be secret?  Analysis needed.]]  6.4. If the client attempts to connect to a server and does not have an 7.1.1.  <ins> In case a HelloRetryRequest (HRR) is sent, the client-facing server MUST consistently accept or decline ECH between the two ClientHellos, using the same ECHConfig, and abort the handshake if this is not possible.  This is achieved as follows.  Let CH1 and CH2 denote, respectively, the first and second ClientHello transmitted on the wire by the client: If CH1 contains the "encrypted_client_hello" extension but CH2 does not, or if CH2 contains the "encrypted_client_hello" extension but CH1 does not, then the server MUST abort the handshake with an "illegal_parameter" alert. Suppose the "encrypted_client_hello" extension is sent in both CH1 and CH2, If the configuration identifier (see ech-configuration) differs between CH1 and CH2, then the server MUST abort with an "illegal_parameter" alert. [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified integration with the backend server.  Stateful HRR on the client- facing server works fine, however.  See issue #333.]] </ins> 7.2.


case, it does not make any changes to the TLS messages, but just blindly forwards them. <del> If the server sends a NewSessionTicket message, the corresponding PSK MUST be ignored by all other servers in the deployment when not negotiating ECH, including servers which do not implement this specification. </del> 7.1.1. It is an error for the client to offer ECH before the

This clause is a remnant of when we tried to partition the resumption space across ECH negotiation, which was simplified in .

case, it does not make any changes to the TLS messages, but just blindly forwards them.  7.1.1. It is an error for the client to offer ECH before the


To encrypt EncodedClientHelloInner, the client first needs to generate the HPKE encryption context.  It computes the encapsulated <del> key, context, HRR key (see hrr), and payload as: </del>  Note that the HPKE functions Deserialize and SetupBaseS are those which match "ECHConfig.kem_id" and the AEAD/KDF used with "context" ClientHelloInner from EncodedClientHelloInner, as described in outer- extensions. <del> Upon determining the true SNI, the client-facing server then either serves the connection directly (if in Shared Mode), in which case it executes the steps in the following section, or forwards the TLS connection to the backend server (if in Split Mode).  In the latter case, it does not make any changes to the TLS messages, but just blindly forwards them. </del>  7.1.1.

This fixes a few issues with the "Upon determining the true SNI" paragraph: It talkes about determining the "true SNI", but we should probably talk about the ClientHelloInner more generally. It refers to behavior in "the following section" which no longer matches. This is a remnant of before Shared Mode was described as a special case of Split Mode, so remove it. It says the server forwards messages unmodified, which isn't true. It needs to forward the decrypted ClientHello. It also needs to process HRR. I've cited the HRR section for the modified decryption procedure, which doesn't really exist right now, but is handling that.
Yup, this aligns with my understanding!

To encrypt EncodedClientHelloInner, the client first needs to generate the HPKE encryption context.  It computes the encapsulated  <ins> key, context, HRR key (see client-hrr), and payload as: </ins> Note that the HPKE functions Deserialize and SetupBaseS are those which match "ECHConfig.kem_id" and the AEAD/KDF used with "context" ClientHelloInner from EncodedClientHelloInner, as described in outer- extensions.  <ins> Upon determining the ClientHelloInner, the client-facing server then forwards the ClientHelloInner to the appropriate backend server, which proceeds as in backend-server-behavior.  If the backend server responds with a HelloRetryRequest, the client-facing server forwards it, decrypts the client's second ClientHelloOuter using the modified procedure in server-hrr, and forwards the resulting second ClientHelloInner.  The client-facing server forwards all other TLS messages between the client and backend server unmodified. </ins> 7.1.1.


IANA is requested to create the following two entries in the existing registry for ExtensionType (defined in RFC8446): <del> encrypted_client_hello(0xff08), with "TLS 1.3" column values being </del>  set to "CH, EE", and "Recommended" column being set to "Yes". <del> outer_extensions(0xff04), with the "TLS 1.3" column values being </del>  set to "", and "Recommended" column being set to "Yes". 11.2.

cc NAME NAME
Yeah, that's what I figured. I doubt we'll need more than 8, but we can pick a different range if that works?
Yeah, this works for me. I don't see any reason why we should keep the code points dense.
Note to self. Let's pick a set and range that works.

IANA is requested to create the following two entries in the existing registry for ExtensionType (defined in RFC8446):  <ins> encrypted_client_hello(0xfe08), with "TLS 1.3" column values being </ins> set to "CH, EE", and "Recommended" column being set to "Yes".  <ins> outer_extensions(0xfd00), with the "TLS 1.3" column values being </ins> set to "", and "Recommended" column being set to "Yes". 11.2.


"ECHConfig.cipher_suites" list. The configuration identifier, equal to "Expand(Extract("", <del> config), "tls ech config id", 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.) The length of this value SHOULD NOT be less than 16 bytes unless it is optional for an application; see optional-configs. </del>  The HPKE encapsulated key, used by servers to decrypt the corresponding "payload" field. but MAY be held constant for successive connections to the same server in the same session. <del> Set the "config_id" field to a randomly-generated string of "Nh" bytes, where "Nh" is the output length of the "Extract" function of the KDF associated with the chosen cipher suite.  (The KDF API is specified in I-D.irtf-cfrg-hpke.) </del>  Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. is determined by one of the two following methods: Compare ClientECH.config_id against identifiers of known <del> ECHConfigs and select the one that matches, if any, as a candidate. </del>  Collect all known ECHConfigs as candidates, with trial decryption below determining the final selection.

This doesn't bind anything, so no sense wasting bytes. (Note: this is orthogonal to , but it makes more sense with that PR.)
I guess this makes the text in a little odd because "Compare ClientECH.config_id against identifiers of known ECHConfigs and select the one that matches, if any, as a candidate.", is a little less clear. If we want to do this, it maybe should say "and select any that match", even though there'll really just be one match.
(Rebased and made the text talk about multiple potential matches.)
This matches the acceptance signal length, and we've already accepted that probability of collision.
I agree this is fine.

"ECHConfig.cipher_suites" list. The configuration identifier, equal to "Expand(Extract("",  <ins> config), "tls ech config id", 8)", unless it is optional for an application; see optional-configs. "config" is the "ECHConfig" structure.  "Extract" and "Expand" 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 "payload" field. but MAY be held constant for successive connections to the same server in the same session.  <ins> Set the "config_id" field to a randomly-generated 8-byte string. </ins> Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. is determined by one of the two following methods: Compare ClientECH.config_id against identifiers of known  <ins> ECHConfigs and select the ones that match, if any, as candidates. </ins> Collect all known ECHConfigs as candidates, with trial decryption below determining the final selection.


When the client-facing server accepts ECH, it forwards the ClientHelloInner to the backend server, who terminates the <del> connection.  If the ClientHelloInner contains an empty "encrypted_client_hello" extension, then the backend server MUST confirm ECH acceptance by setting ServerHello.random[24:32] to </del>  where HKDF-Expand-Label and HKDF-Extract are as defined in RFC8446. The value of ServerHello.random[0:24] is generated as usual by invoking a secure random number generator (see RFC8446, Section 4.1.2).  8. Unlike most TLS extensions, placing the SNI value in an ECH extension Note that the same bytes of the ServerHello.random are used to implement downgrade protection for TLS 1.3 (see RFC8446, <del> Section 4.1.3).  The backend server's signal of acceptance does not interfere with this mechanism because ECH is only supported in TLS 1.3 or higher. </del>  10.8.

As written, the backend server performs this URL modification unconditionally when it sees an empty ECH extension. Although the client is required to only offer TLS 1.3, the backend server doesn't check this. However, as noted elsewhere in the document, these same 8 bytes are used for the TLS 1.3 downgrade protection mechanism. That mechanism only works if the server never produces a TLS 1.2 ServerKeyExchange signature without the signal, so it's important that the server not signal ECH acceptance. Write this down.
Oops. Good catch!Yup, this is a good idea.

When the client-facing server accepts ECH, it forwards the ClientHelloInner to the backend server, who terminates the  <ins> connection.  If the backend server negotiates TLS 1.3 or higher and the ClientHelloInner contains an empty "encrypted_client_hello" extension, then the backend server MUST confirm ECH acceptance by setting ServerHello.random[24:32] to </ins> where HKDF-Expand-Label and HKDF-Extract are as defined in RFC8446. The value of ServerHello.random[0:24] is generated as usual by invoking a secure random number generator (see RFC8446, Section 4.1.2). <ins> The backend server MUST NOT perform this operation if it negotiated TLS 1.2 or below.  Note that doing so would overwrite the downgrade signal for TLS 1.3 (see RFC8446, Section 4.1.3). </ins> 8. Unlike most TLS extensions, placing the SNI value in an ECH extension Note that the same bytes of the ServerHello.random are used to implement downgrade protection for TLS 1.3 (see RFC8446,  <ins> Section 4.1.3).  These mechanisms do not interfere because the backend server only signals ECH acceptance in TLS 1.3 or higher. </ins> 10.8.


HelloRetryRequest processing apply. Clients bind encryption of the second ClientHelloInner to encryption <del> of the first ClientHelloInner via the derived ech_hrr_key by modifying HPKE setup as follows: </del>  The "info" parameter to SetupPSKS is the concatenation of "tls ech", <del> a zero byte, and the serialized ECHConfig.  Clients then encrypt the second ClientHelloInner using this new HPKE context.  In doing so, the encrypted value is also authenticated by ech_hrr_key.  The rationale for this is described in flow-hrr-hijack. Client-facing servers perform the corresponding process when decrypting second ClientHelloInner messages.  In particular, upon receipt of a second ClientHello message with a ClientECH value, servers set up their HPKE context and decrypt ClientECH as follows: ClientHelloOuterAAD is computed from the second ClientHelloOuter as described in authenticating-outer.  The "info" parameter to SetupPSKR is computed as above. </del>  If the client offered ECH in the first ClientHello, then it MUST offer ECH in the second.  Likewise, if the client did not offer ECH aborts the connection with a "decrypt_error" alert rather than continuing the handshake with the second ClientHelloOuter.  [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified integration with the backend server.  Stateful HRR on the client-

Now that we have separate client HRR and server HRR sections, move some server text to the server section. Also clarify that the second ClientHelloOuterAAD is computed from the second ClientHelloOuter, not reusing the first value. (Some cleanup in advance of writing up the HPKE context reuse version.)
Can we merge this? The number of PRs is getting confusing. NAME

HelloRetryRequest processing apply. Clients bind encryption of the second ClientHelloInner to encryption  <ins> of the first ClientHelloInner via the derived ech_hrr_key by running a modified encryption process: </ins> The "info" parameter to SetupPSKS is the concatenation of "tls ech",  <ins> a zero byte, and the serialized ECHConfig.  ClientHelloOuterAAD is computed from the second ClientHelloOuter as described in authenticating-outer.  Note the encrypted value is also authenticated by ech_hrr_key.  The rationale for this is described in flow-hrr- hijack. </ins> If the client offered ECH in the first ClientHello, then it MUST offer ECH in the second.  Likewise, if the client did not offer ECH aborts the connection with a "decrypt_error" alert rather than continuing the handshake with the second ClientHelloOuter. <ins> When decrypting the second ClientECH.payload, the client-facing server performs a corresponding process to client-hrr: ClientHelloOuterAAD is computed from the second ClientHelloOuter as described in authenticating-outer.  The "info" parameter to SetupPSKR is the concatenation of "tls ech", a zero byte, and the serialized ECHConfig. </ins> [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified integration with the backend server.  Stateful HRR on the client-


"pre_shared_key" extension is forbidden in ClientHelloOuter. The client then generates the HPKE encryption context.  Finally, it <del> computes the encapsulated key, context, HRR key (see client-hrr), and payload as: </del>  Note that the HPKE functions Deserialize and SetupBaseS are those which match "ECHConfig.kem_id" and the AEAD/KDF used with "context" in HelloRetryRequest.  Otherwise, the usual rules for HelloRetryRequest processing apply. <del> Clients bind encryption of the second ClientHelloInner to encryption of the first ClientHelloInner via the derived ech_hrr_key by running a modified encryption process: </del>  <del> The "info" parameter to SetupPSKS is the concatenation of "tls ech", a zero byte, and the serialized ECHConfig.  ClientHelloOuterAAD is computed from the second ClientHelloOuter as described in authenticating-outer.  Note the encrypted value is also authenticated by ech_hrr_key.  The rationale for this is described in flow-hrr- hijack. </del>  If the client offered ECH in the first ClientHello, then it MUST offer ECH in the second.  Likewise, if the client did not offer ECH in the first ClientHello, then it MUST NOT not offer ECH in the second. <del> [[OPEN ISSUE: Should we be using the PSK input or the info input?  On the one hand, the requirements on info seem weaker, but maybe actually this needs to be secret?  Analysis needed.]] </del> 6.2. If the client attempts to connect to a server and does not have an ECHConfig structure available for the server, it SHOULD send a GREASE <del> RFC8701 "encrypted_client_hello" extension as follows: </del>  Set the "cipher_suite" field to a supported ECHCipherSuite.  The selection SHOULD vary to exercise all supported configurations, scheme and L is the size of the EncodedClientHelloInner the client would compute when offering ECH, padded according to padding.  If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with a "decode_error" alert if it is invalid.  It otherwise ignores the 7.1. <del> Upon receiving a non-empty "encrypted_client_hello" extension, the client-facing server determines if it will accept ECH, prior to negotiating any other TLS parameters.  Note that successfully decrypting the extension will result in a new ClientHello to process, so even the client's TLS version preferences may have changed. </del>  First, the server collects a set of candidate ECHConfigs.  This set is determined by one of the two following methods: forwards the ClientHelloInner to the appropriate backend server, which proceeds as in backend-server.  If the backend server responds with a HelloRetryRequest, the client-facing server forwards it, <del> decrypts the client's second ClientHelloOuter using the modified procedure in server-hrr, and forwards the resulting second ClientHelloInner.  The client-facing server forwards all other TLS messages between the client and backend server unmodified. </del>  Otherwise, if all candidate ECHConfigs fail to decrypt the extension, the client-facing server MUST ignore the extension and proceed with 7.1.1. <del> In case a HelloRetryRequest (HRR) is sent, the client-facing server MUST consistently accept or decline ECH between the two ClientHellos, using the same ECHConfig, and abort the handshake if this is not possible.  This is achieved as follows.  Let CH1 and CH2 denote, respectively, the first and second ClientHello transmitted on the wire by the client: If CH1 contains the "encrypted_client_hello" extension but CH2 does not, or if CH2 contains the "encrypted_client_hello" extension but CH1 does not, then the server MUST abort the handshake with an "illegal_parameter" alert. If the "encrypted_client_hello" extension is sent in CH2, the server follows the procedure in client-facing-server to decrypt the extension, but it uses the previously-selected ECHConfig as the set of candidate ECHConfigs.  If decryption fails, the server aborts the connection with a "decrypt_error" alert rather than continuing the handshake with the second ClientHelloOuter. When decrypting the second ClientECH.payload, the client-facing server performs a corresponding process to client-hrr: ClientHelloOuterAAD is computed from the second ClientHelloOuter as described in authenticating-outer.  The "info" parameter to SetupPSKR is the concatenation of "tls ech", a zero byte, and the serialized ECHConfig. </del>  [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified controlled) ClientHello, the Certificate produced would leak the client's chosen SNI to the attacker. <del> This attack is mitigated by binding the first and second ClientHello messages together.  In particular, since the attacker does not possess the ech_hrr_key, it cannot generate a valid encryption of the second inner ClientHello.  The server will attempt decryption using ech_hrr_key, detect failure, and fail the connection. </del>  <del> If the second ClientHello were not bound to the first, it might be </del>  possible for the server to act as an oracle if it required parameters from the first ClientHello to match that of the second ClientHello. For example, imagine the client's original SNI value in the inner

(Note: this PR sits on top of , though hopefully the combined diff is also readable enough.) This achieves the same security properties of echhrrkey, but gives us a few improvements: Client-facing servers that offload their HPKE operations to RPCs do not need to worry about race conditions with rotation between the two ClientHellos. (See .)We only perform one addition asymmetric operation with ECH, rather than sometimes two. For servers with offloaded ECH keys, this also avoids an extra RPC call.HPKE implementations for ECH only need SetupBase and not SetupPSK.The description of HRR handling in general can be greatly simplified. Rather than running the server's CH1 algorithm with various modifications, we can just write a straightforward, simpler algorithm. This PR also adds some missing text for GREASE on HRR, which (Although it's worth noting that HRR connections can be easily distinguished by an active attacker anyway. This PR doesn't change this, but makes it much clearer.)
This is much simpler, but, if I understand it correctly, I think it might be a little bit of a regression. The previous export-then-inject design forced the server to process both ECH configs and encapsulated keys in both CH1 and CH2, whereas here the server just ignores those in CH2 and uses the values from CH1. That lets clients change these fields in CH2 without any effect. Now, I don't really know of anything that could go wrong here, but it smells like the sort of "insufficient binding" problem we've continually run into before. (For example, I think an on-path attacker could modify the or fields in CH2, without touching anything else. Decryption would succeed, as the server used CH1's value, and the transcript is unaffected since the server would choose the CH inner value. Is that a way for an on-path attacker to quickly check if the connection is real or fake ECH?) Chris asked if we should require servers to verify these match, and as you pointed out, that's a mess. (We also acknowledged that servers missing these checks is something we should expect to happen in the wild, which is somewhat why we are concerned about this re-binding attack in the first place.) I wonder if we can solve this in a different way: . That would prevent the modification above, I think, and shouldn't be too bad in terms of additional complexity.
I think allowing the actual client to change ignored fields is a general property of HelloRetryRequest. Because we don't expect servers to check consistency between CH1 and CH2, a client that goes out of their way to send something inconsistent may find that their (CH1, CH2) tuple is interpreted ambiguously. But the transcript reflects the whole tuple, so, as far as the handshake itself, the client is just causing its own preferences to be misrepresented here. Ideally ambiguous things would be unrepresentable and consistently treated as errors but HRR is what it is. :-/ I think real vs. fake ECH on HRR connections is entirely hopeless, short of completely redoing HRR. If CH1 uses ECH, we need to require CH2 also use ECH. That means ECH decryption failures in CH2 are fatal, and you can distinguish the two by simply replaying CH1 and perturbing the ciphertext in CH2. (That said, note that CHOuterAAD means whether a ClientHello triggers HRR is not under attacker control. So if a client/server pair doesn't trigger HRR, the attacker can't try to do this.) That said, I think there's a variant of this on CH1 in the optional configid mode. (See below.) Yeah, this is the "I think we got HRR wrong and have to live with it" problem. :-) I don't think I believe this matters for HRR (only attack I can see is real vs fake ECH, which is hopeless with HRR anyway), but now that you mention it, I think there is a variant of this for the optional configid mode in the non-HRR case. In CH1, we don't bind configid, enc, and ciphersuite to the encryption, so a priori an attacker might be able to perturb them without breaking the decryption. Perturbing enc and ciphersuite seems likely to break decryption anyway, but configid is interesting. In the normal mode, it too will break decryption. In the optional configid mode, configid is just a placeholder. That means an attacker can distinguish optional configid ECH from {GREASE or configid ECH} by flipping the config_id, sending it to the server, and seeing if the client can decrypt the result. (Of course, all this is relative to an unclear and undefined "don't stick out" model.) Complexity-wise, I think it's fine, but note it changes the processing order somewhat interestingly, both for the spec and implementations. Right now the spec says: Make your CHInner. This should largely be running through your usual CH assembly logic. Encode it with the extensions you expect to compress. Make your CHOuter, minus the ECH extension. This should largely run through your existing CH assembly logic and computes CHOuterAAD. Take EncodedCHInner and CHOuterAAD and run through the Deserialize, SetupBaseS, context.Seal logic to assemble the final CHOuter. In order to put these fields in AAD, we'd need to say: Make your CHInner. This should largely be running through your usual CH assembly logic. Encode it with the extensions you expect to compress. Make your CHOuter, minus the ECH extension. This should largely run through your existing CH assembly logic. Run Deserialize and SetupBaseS. Compute CHOuterAAD with half an ECH extension. Take EncodedCHInner and CHOuterAAD and run through the context.Seal logic to assemble the final CHOuter.
A similar "don't stick out" distinguisher: Flip a bit of ClientECH.payload in CH2. The client-facing server will abort with "decrypt_error" if real-ECH is in use but not if grease-ECH is in use. This attack is unavoidable if we want to ensure that the server doesn't change from CHInner to CHOuter across the HRR. (Enforcing this invariant seems like a good idea to me.) This seems like a sensible idea, although I'm not super concerned about the "don't stick out" attack it mitigates. I would vote we land this change as-is and follow up with a new PR.
Right, I mentioned that HRR attack above. ( "I think real vs. fake ECH on HRR connections is entirely hopeless [...]".) Since avoiding this is hopeless in HRR, I don't think we should use it as a justification for binding bits of CH2's ClientECH or checking they match (which don't actually do the same thing). Whereas attacks on CH1 are interesting because that situation is less obviously hopeless. So maybe we want to add more things to CHOuterAAD for that, though that is orthogonal to this PR, so I think we should deal with it separately. (CHOuterAAD was originally intended to fix the ClientHello malleability attack, in a way that avoids burning 32 bytes on an inner_digest.)
Agreed. (I was in the middle of editing my comment above to say the same thing when I saw your message!)

This attack is unavoidable if we want to ensure that the server doesn't change from CHInner to CHOuter across the HRR. Yeah, this morning I came to the conclusion that mitigating this sort of distinguisher for HRR was not really possible unless we relax the inner/outer matching invariant. I'm fine pursuing more AAD as a separate change on top of this, since it's strictly an improvement (and HRR is hopeless).
NAME can you please resolve conflicts?
Rebased.
I'll try to put together a PR for this later, but filing this now so I don't forget. (NAME FYI) The draft currently says you randomize this both times, which won't work. Also "MUST check the extension syntactically" appears to have been a little ambiguous, since how deeply you parse ECHConfigs gets a little complicated. I'll adjust that too.
What fields are suggesting to not randomize?
. The draft doesn't quite say to randomize it, but it does say to vary it. (I'm not sure that's correct, actually. It depends on how much variation to expect in server ECHConfigs. If everyone does the AES-GCM / ChaCha20 pair, as they should, then the client will always pick one matching its cipher preference.) Additionally, after , also should not be re-randomized on CH2.
NAME what is the concern here?
Just that GREASE and ECH should be consistent in which fields get copied over and the text doesn't say that right now.
Oh, apparently I already thought of this in and included some text about this: So I'll just mark as closing this too but leave this open just in case we decide not to do .
ECH currently adds an ECDH operation on both the first and second ClientHello. That's extra asymmetric operations, and if folks end up offloading those keys, it's two RPCs. Given that we need to bind the two decryptions together anyway, and HPKE supports multiple encrypted messages (it has an internal sequence number), perhaps it's better to just use that. Here's a possibility: the first ClientHello makes a new HPKE context and calls context.Seal on it. The second ClientHello simply calls context.Seal on the same context, which internally increments the sequence number. Probably we copy over the first ClientHello's ciphersuite, configid, and enc, even though they don't really do anything. The server, if stateful, just calls context.Open to mirror the client behavior. If stateless, probably the simplest thing is to pickle the AEAD key inside the cookie. [Edit: I accidentally wrote "ticket" earlier.] It probably could also recompute the HPKE context and bump the sequence number up by one, maybe checking the selected ECHConfig and enc values didn't change. (The text already requires checking the former.)
EDITED: s/ticket/cookie/ I like this suggestion overall. A few comments: It's not sufficient to store only the key in the cookie: minimally we'll need the key and the initialization vector (called "nonce" in the HPKE spec, unfortunately). We may also end up needing the exporter secret as well, although this is currently only used in ECH to compute the HRR PSK for and this call would go away if we decided to adopt this change. In case of ECH acceptance, the backend server stores the key/IV in the cookie. This is a problem for "Split Mode": as you previously pointed out, there is currently no way for the client-facing server to convey cookie content to the backend server. Perhaps a solution to is a prerequisite to this? HPKE does not specify serialization of the HPKE context. This use case makes me think it should.
(I think you want to s/ticket/cookie/g since ticket is the resumption thing. That was my bad. I wrote ticket instead of cookie too.) Good catch. Yes, we need to store both derived values. Although the difference between storing one short byte string or two short byte strings is largely one of semantics. (You can just as easily concatenate them and store the one resulting short byte string.) You could also store the root secret and rederive it. Whichever. We wouldn't need the exporter secret or SetupPSK in this design. I don't think this introduces a new problem, so there's no need to block on . Though we need to deal with that too. In both this design and the status quo, the client-facing server needs to either be stateful or store something secret in the cookie. Right now it's echhrrkey. In this design it's the key/IV. (Though this design introduces the alternate strategy of storing a less secret thing, where you just remember the client's public bits and recompute the HPKE context.) The serialization doesn't need to be standard or anything. A TLS server might want to keep its cookie small by observing it doesn't need the exporter secret. But some guidance might be useful, sure. (The only thing that needs to consume the cookie is the server, who also produced it. Just as TLS doesn't (and shouldn't) specify session state serialization, yet every TLS implementation has a session state serialization. I could also implement TLS by, rather than keeping connection state in memory, write it to disk and read it back out on every packet. Even an in-memory representation is a serialization.)
This is an interesting consideration. On one hand, I agree that it's simpler to simply reuse the existing context, though on the other hand it seems to run against the general desire to use primarily CH2 for "everything." That said, ECH annoyingly requires binding between the two CHs, which we currently address by tying them together with a PSK, and this proposal is arguably simpler. (Anecdotally, I recall some server stacks verifying PSK binders on CH1 and then ignoring them on CH2, which was a situation that wasn't carefully analyzed in the formal analyses. I think that was fixed, though I don't recall how: are binders verified for both CHs, or only for the latter? And if verifying a binder involves a public key operation, why would ECH be any different?)
The "use CH2 for everything" theory mostly assumes you don't negotiate very many parameters on CH1, whereas you need to resolve ECH early to even get at CH1. And, as you note, we need to bind the two. So I think we're pretty stuck with having ECH resolved at CH1 rather than CH2 anyway. Verifying a binder doesn't require a public key operation. It's mostly an HMAC. If you resolved the PSK at CH1, you've got the PSK available for CH2 and can compute the binder easily enough. If you resolved the PSK at CH2[], then you just do it normally. [] As an aside, you only sort of are able to do this. If your stack implements pskke, which thankfully most folks didn't, you need to resolve PSK at CH1 to know if you need a key share at all. Whereas if you know you only support pskdhe_ke, then you can proceed assuming you need a key share and defer sorting out the PSK to later.
Yeah, I think that's right. What if the key is inside a ticket? Doesn't that require a public key operation? (I should have clarified that I was referring to the whole process, involving getting the key and then using it to verify the binder. Sorry!)
I guess it depends on how you do your tickets. They're typically just encrypted with a symmetric key, but I've also seen folks use asymmetric stuff. But, in any case, if you decrypt the ticket at CH1, by CH2 you've already gotten the decrypted ticket so you just compute the binder and don't redo the ticket business. If you decrypt the ticket at CH2, you only have the one ticket decryption by construction.
PR in .
I was thinking about and the discussion on server HRR checks started by URL, and I'm not sure HRR cookies even work in split mode right now. First off, suppose we don't merge something like . The current text says: This means if you get a cookied HRR only good for ClientHelloInner, there's no requirement to stick the cookie in ClientHelloOuter! But now the client-facing server can't see the cookie, so it can't be stateless. It would seem like fixes this, but now we have a more fundamental problem: whose cookie? Suppose the client-facing server wishes to handle HRR statelessly. It decrypts the first ClientHelloInner and forwards it to the backend server. If the backend server responds with HRR, the client-facing server must forward the HRR as-is, or the transcript breaks. But the client-facing server has no opportunity to bind its state (echhrrkey) into the HRR cookie. (Perhaps there isn't even one. If there is, it's encrypted with the backend server's key anyway. Either way, it's fixed now.) The simplest solution to all this is to only support stateful client-facing servers in split mode, unless the client-facing and backend servers can coordinate enough to share one cookie. I don't know if that would satisfy everyone. I could imagine doing something horrific to URL, akin to what we did to URL, but that sounds really messy.
Let's mark this as an "OPEN ISSUE" in the draft.
Done. URL
NAME mentions possibly binding the outer CH to the ECH encryption in . If we don't do that, we should at least bind the encryption to the non-encrypted parts of the ECH extension (, , and so on). cc NAME
I also like this idea :) AD is cheap.
Closing in favor of . We can revisit the ECH extension AD information later if desired.
Re-opening to consider adding all data to AAD. NAME wrote out the flow if we did this: Make your CHInner. This should largely be running through your usual CH assembly logic. Encode it with the extensions you expect to compress. Make your CHOuter, minus the ECH extension. This should largely run through your existing CH assembly logic. Run Deserialize and SetupBaseS. Compute CHOuterAAD with half an ECH extension. Take EncodedCHInner and CHOuterAAD and run through the context.Seal logic to assemble the final CHOuter. So, not great, but also not too terrible. I'd prefer we avoid falling into the "gee, I sure wish we bound more to the encryption" state, given the history of ECH, though I agree that this isn't cheap. NAME what do you think?
To be clear, the suggestion is to change AEAD encryption so that it authenticates , , and , correct? This change seems sensible to me. One might argue that this is redundant because these parameters are incorporated into the transcript (on ECH rejection). On the other hand, ECH usage is resolved before the transcript is known! It seems like a good idea to authenticate these parameters before consuming them.
They aren't incorporated on ECH accept, which is mildly interesting. ECH accept also doesn't negotiate with them, so it's probably okay, though there is a "don't stick out" failure (depending on threat model) for the optional config_id flow if we don't authenticate them.
This seems generally good. I'm not sure why we didn't do this in the first place. Are reusable contexts new?LGTM.

"pre_shared_key" extension is forbidden in ClientHelloOuter. The client then generates the HPKE encryption context.  Finally, it  <ins> computes the encapsulated key, context, and payload as: </ins> Note that the HPKE functions Deserialize and SetupBaseS are those which match "ECHConfig.kem_id" and the AEAD/KDF used with "context" in HelloRetryRequest.  Otherwise, the usual rules for HelloRetryRequest processing apply.  <ins> The client encodes the second ClientHelloInner as in encoding-inner, using the second ClientHelloOuter for any referenced extensions.  It then encrypts the new EncodedClientHelloInner value as a second message with the previous HPKE context: </ins>  <ins> ClientHelloOuterAAD is computed as described in authenticating-outer, but again using the second ClientHelloOuter.  Note an HPKE context maintains a sequence number, so this operation internally uses a fresh nonce for the AEAD operation.  Reusing the HPKE context avoids an attack described in flow-hrr-hijack. The client then modifies the "encrypted_client_hello" extension in ClientHelloOuter as follows: "cipher_suite" is unchanged and contains the client's chosen HPKE cipher suite. "config_id" is replaced with the empty string. "enc" is replaced with the empty string. "payload" is replaced with the value computed above. </ins> If the client offered ECH in the first ClientHello, then it MUST offer ECH in the second.  Likewise, if the client did not offer ECH in the first ClientHello, then it MUST NOT not offer ECH in the second.  6.2. If the client attempts to connect to a server and does not have an ECHConfig structure available for the server, it SHOULD send a GREASE  <ins> RFC8701 "encrypted_client_hello" extension in the first ClientHello as follows: </ins> Set the "cipher_suite" field to a supported ECHCipherSuite.  The selection SHOULD vary to exercise all supported configurations, scheme and L is the size of the EncodedClientHelloInner the client would compute when offering ECH, padded according to padding. <ins> When sending a second ClientHello in response to a HelloRetryRequest, the client copies the "cipher_suite" field from the first ClientHello.  It sets "config_id", and "enc" to the empty string. Finally, it generates a new "payload" field, using the length of a padded second EncodedClientHelloInner for L. </ins> If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with a "decode_error" alert if it is invalid.  It otherwise ignores the 7.1.  <ins> Upon receiving a non-empty "encrypted_client_hello" extension in an initial ClientHello, the client-facing server determines if it will accept ECH, prior to negotiating any other TLS parameters.  Note that successfully decrypting the extension will result in a new ClientHello to process, so even the client's TLS version preferences may have changed. </ins> First, the server collects a set of candidate ECHConfigs.  This set is determined by one of the two following methods: forwards the ClientHelloInner to the appropriate backend server, which proceeds as in backend-server.  If the backend server responds with a HelloRetryRequest, the client-facing server forwards it,  <ins> decrypts the client's second ClientHelloOuter using the procedure in server-hrr, and forwards the resulting second ClientHelloInner.  The client-facing server forwards all other TLS messages between the client and backend server unmodified. </ins> Otherwise, if all candidate ECHConfigs fail to decrypt the extension, the client-facing server MUST ignore the extension and proceed with 7.1.1.  <ins> After sending or forwarding a HelloRetryRequest, the client-facing server does not repeat the steps in client-facing-server with the second ClientHelloOuter.  Instead it continues with the ECHConfig selection from the first ClientHelloOuter as follows: If the client-facing server accepted ECH, it checks the second ClientHelloOuter also contains the "encrypted_client_hello" extension.  If not, it MUST abort the handshake with a "missing_extension" alert.  Otherwise, it checks that ClientECH.cipher_suite is unchanged, and that ClientECH.config_id and ClientECH.enc are empty.  If not, it MUST abort the handshake with an "illegal_parameter" alert. Finally, it decrypts the new ClientECH.payload as a second message with the previous HPKE context: ClientHelloOuterAAD is computed as described in authenticating-outer, but using the second ClientHelloOuter.  If decryption fails, the client-facing server MUST abort the handshake with a "decrypt_error" alert.  Otherwise, it reconstructs the second ClientHelloInner from the new EncodedClientHelloInner as described in encoding-inner, using the second ClientHelloOuter for any referenced extensions. The client-facing server then forwards the resulting ClientHelloInner to the backend server.  It forwards all subsequent TLS messages between the client and backend server unmodified. If the client-facing server rejected ECH, or if the first ClientHello did not include an "encrypted_client_hello" extension, the client- facing server proceeds with the connection as usual.  The server does not decrypt the second ClientHello's ClientECH.payload value, if there is one. </ins> [[OPEN ISSUE: If the client-facing server implements stateless HRR, it has no way to send a cookie, short of as-yet-unspecified controlled) ClientHello, the Certificate produced would leak the client's chosen SNI to the attacker.  <ins> This attack is mitigated by using the same HPKE context for both ClientHello messages.  The attacker does not possess the context's keys, so it cannot generate a valid encryption of the second inner ClientHello. </ins>  <ins> If the attacker could manipulate the second ClientHello, it might be </ins> possible for the server to act as an oracle if it required parameters from the first ClientHello to match that of the second ClientHello. For example, imagine the client's original SNI value in the inner


use ClientHelloInner or reject it and use ClientHelloOuter.  In handling the server's response, the client's first step is to determine which value was used.  The client presumes acceptance if <del> the last 8 bytes of ServerHello.random are equal to "accept_confirmation" as defined in backend-server.  Otherwise, it </del>  presumes rejection. 6.1.2.1. Upon receipt of an empty "encrypted_client_hello" extension, if the backend server negotiates TLS 1.3 or higher, then it MUST confirm ECH <del> acceptance by setting ServerHello.random[24:32] to where HKDF-Expand-Label and HKDF-Extract are as defined in RFC8446. The value of ServerHello.random[0:24] is generated as usual by invoking a secure random number generator (see RFC8446, Section 4.1.2). </del>  The backend server MUST NOT perform this operation if it negotiated TLS 1.2 or below.  Note that doing so would overwrite the downgrade

, this change mitigates an active "don't stick out" attack . The attack is as follows. The attacker intercepts the client's first flow () and forwards it to the server. The attacker intercepts the server's first flow and transforms and into and respectively, as follows. is just like except that the key share is replaced with key share , where is known to the attacker. is a malformed handshake message encrypted using a key derived from . Finally, it sends to the client. If the client aborts with a "badrecord_mac" alert, then the attacker guesses that real-ECH was used. Otherwise, it guesses that grease-ECH was used.
I have really mixed feelings about this. On the one hand, it's kind of annoying extra complexity. OTOH, given how hard a time we have had by reasoning about the security of ECH, I think better safe than sorry.
NAME can you please rebase this PR?
NAME this is nice -- thanks! I think the new type name for the special SH is fine, though if someone has an idea for a better name, let's consider it.
This change is definitely more invasive than the existing signal, but not all that hard to implement. (E.g., URL) cc/ NAME NAME
Thanks, NAME NAME and NAME if you can also comment on the implementation difficulty, we can try and land this soon.
This seems pretty straightforward. I don't think it would be too difficult to do in NSS.
I think on balance we should do this.
The primary "don't stick out" [RFC8744] consideration for ECH is to ensure that the new extension does not trigger middlebox misbehavior. To accomplish this, the draft specifies two protocol variants: in the "real-ECH" protocol the client offers the ECH extension, which is processed by the server; in the "grease-ECH" protocol, the client generates a dummy ECH extension, which is ignored or rejected by the server. Grease-ECH is designed to look enough like real-ECH so that middleboxes do not differentiate the latter from the former, the idea being that if grease-ECH can be deployed without triggering middlebox misbehavior, then real-ECH ought to be deployable, too. But what do we mean when we say grease-ECH should "look like" real-ECH? In ECH-08 the stated goal is "to hide, to some extent, (a) whether or not a specific server supports ECH and (b) whether or not ECH was accepted for a particular connection" ]. These goals are hard to meet. First, an attacker can violate (a) by simply running grease-ECH with the server to see if it returns retry configs. Second, there are a number of ways to distinguish real-ECH from grease-ECH, thereby violating (b): NAME . If the ECH config is known to the attacker, then it can use ClientECH.configid to distinguish real-ECH from grease-ECH. The outer SNI must match ECHConfig.publicname. Hence, for connections to a given backend server, the outer SNI will be different in real-ECH than in grease-ECH. The client and server must negotiate TLS 1.3 when ECH is accepted, but may otherwise negotiate an earlier version. In particular, it is valid for a client to run grease-ECH but not indicate support for 1.3. PSK must not be offered in the outer CH in real-ECH, but may be offered in grease-ECH. In general, the set of extensions negotiated may depend on whether ECH was accepted. The client-facing server must not request client authentication when rejecting ECH, but the backend server may request client authentication when ECH was accepted. Whether or not an HRR message is issued by the server may depend on ECH acceptance. There are a few active distinguisher attacks that exploit the HRR codepath. Some of these distinguishers can be mitigated by careful implementation, while mitigating others would require changes to the specification. (E.g., .) However, it's unlikely that all implementations will mitigate all of these distinguishers. Moreover, future extensions are likely to change the shape of TLS in a way that leads to yet more distinguishers. Given all of this, I wonder if indistinguishability (of real-ECH from grease-ECH) is the right way to think about this problem. It seems to me that it's stronger than strictly necessary, since the existence of a distinguisher does not necessarily imply that some feature of real-ECH will trigger middlebox misbehavior that would not otherwise be triggered by grease-ECH. For example, while the outer SNI might be a dead give away of likely ECH usage, this value is unlikely to impact middlebox behavior. Similarly, the absence of the PSK extension in real-ECH is unlikely to cause issues. If it were possible to enumerate the set of properties that impact middlebox behavior, then ensuring that middleboxes don't differentiate between real-ECH and grease-ECH may simply be a matter of ensuring that this set of properties does not vary. This approach would be roughly analogous to the design of "Middlebox Compatibility Mode" in TLS 1.3, which makes the handshake "look like" a particular mode of 1.2 that middleboxes are known to tolerate. Of course, a curious adversary (e.g., a censor) can easily distinguish between these, but experience suggests that middleboxes do not treat them differently. This leaves us the daunting task of deciding what features are known to impact the behavior of existing middleboxes. Here's a preliminary list of things that might fit in this category. For the sake of discussion, let's call them differentiable features: The set of extensions that appear to be negotiated; Visible alerts, e.g., "badrecordmac"; Number of round trips in a given transport context (e.g., TCP connection); Packet size, or the number of packets comprising a single message; Timing; ... I think we should set the following goals for ECH-09 (or some draft that follows it). If we can agree on a set of differentiable features, then we should ensure that this set does not vary between real-ECH and grease-ECH. ECH-08 aims only to mitigate passive "don't stick out" attacks, but there is a lot of evidence that middleboxes exhibit active behavior. (For example, middleboxes have been observed removing CH extensions they don't recognize or adding their own.) Thus, we should aim to thwart attacks that change the set of differentiable features in real-ECH vs. grease-ECH. This includes NAME attack in particular: see . To the greatest extent possible, the spec should allow for implementations to aim for the strongest possible notion of security. For example, we might consider relaxing the requirement that the outer SNI matches ECHConfig.public_name, so long as this change doesn't impact interoperability.
For the PSK extension specifically, I think we have two distinguishers to consider: GREASE ECH + PSK extension in a Client Hello identifies GREASE ECH, due to PSKs being disallowed in CHOuter. Real ECH + resumption (or any accepted PSK) identifies accepted ECH. CHOuter contains no PSK extension, but SH will contain the presharedkey extension (selected_identity).
Due to assertions stated in IETF meetings that ECH isn't an anti-censorship tool, and the literally innumerable difficulties in making real ECH indistinguishable from GREASE ECH (as mentioned above), I propose dropping the "do not stick out" goal. The primary aim of ECH is to make the SNI private, and it's possible to keep the SNI private while telegraphing that we're using ECH. We can, however, still use GREASE to make sure middleboxes are prepared to handle the code point and the larger size of the ClientHello.
I agree with cjpatton's analysis. I don't think anyone is asking that these things be "indistinguishable" to a determined active intermediary in the common case. That is clearly impossible (e.g. cjpatton's attack 2). Instead, the goal is to minimize barriers to deployment of ECH. If ECH isn't reliable in practice, it won't be widely deployed, and so will not keep the SNI private. In my view, the minimum threshold is: a passive intermediary with only per-connection state must not be able to distinguish ECH from GREASE with confidence for a typical client and server configuration. Defenses for less common protocol uses and low-grade active intermediaries (e.g. simple replay) are good to have but we shouldn't delay the protocol if they prove difficult. I think the only remaining issue to reach this level is Real ECH + resumption (kjacobs second attack). All the other attacks (a) only affect a subset of GREASE (e.g. resumption GREASE), (b) require the intermediary to be active (emitting or modifying packets), or (c) require the intermediary to have per-server state (e.g. a DNS database or a persistent server profile). We should build defenses for those cases too where we can, but some are intractable, and that's OK.
It is kind of sad, but I agree with the analysis. The sticking out starts with the definition of ECH as an extension, which already provides a differentiation from standard TLS. The theory that grease will fix that by making it hard to distinguish "real" and "fake" ECH is most probably wrong, except if the middlebox is really naive. That means the only sure way to grease the ECH function is to actually use it, even when the inner SNI is the same as the outer ECH -- only a real ECH is undistinguishable from a real ECH.
I agree with NAME The ECH goals in the document mostly align with this proposed minimum threshold. I think we should close this with no action, or tweak language to make this more clear, assuming we have consensus. NAME NAME NAME NAME what do you think?
I don't think action is needed at this point, but I think we should keep the issue open until we're closer to RFC. We're pretty close to interop, and we should expect real-world experiments to follow shortly thereafter. Those experiments will reveal if we missed anything, and it will be useful to have this issue as a reference.
This seems like a reasonable objective for avoiding operational/ossification issues.
NAME description sounds right. I would note that this seems to actually make unnecessary, right? Not saying we necessarily need to not do it, but...
Just to see about sharpening NAME description: if an attacker needs to interfere with a connection in such a way that it would cause a failure in that connection in order to determine whether ECH is in use, that's out of scope, right?
Yes, that seems like a higher threshold than the one I proposed (purely passive). I'm not sure about "out of scope". For example, I fully support . I think that when we run into problems like this, we should solve them if we have a solution available, but not delay the protocol if we have to go searching for one.
I disagree. I think that if a particular threat model is out of scope, it's out of scope, and any mitigations of them shouldn't be in the document. There's no point in half-measures that ultimately won't be sufficient for addressing certain types of attackers but will add complexity.
An interesting detail on this one: interfering with the bytes and observing either client or server's response will break that connection, but observing the server's response is a little different from observing the client. One can often make a second connection to the server. (Perhaps a "passive and a half" attack.) That is, suppose the server sent decryptfailure on bad ECH decryptions. (It no longer does.) You can determine whether ECH is in use by flipping a bit in the ciphertext and seeing if the server notices. That breaks the connection, but you could make a second connection to the server, send your perturbed ClientHello, and not interfere with the original connection. Whereas, if your attack involves probing the client's behavior, you'd need to induce the client to start a second connection and not notice the first. Systems do tend to retry on failure, so this isn't entirely_ implausible, but harder than the first attack.
ESNI seems to rely on TLS 1.3 KeyShareEntry which makes it incompatible with TLS 1.2. That's unfortunate with that "The protocol designed in this document is quite straightforward." The gist of ESNI comes down to "encryptedservername extension, which contains the true extension encrypted under the provider’s public key." That seems pretty generic and backwards compatible.
To prevent cut-and-paste attacks, you need to bind ESNI to the CH. That means we’d need 1.2 clients to send a key share in the CH to use when deriving encryption keying material, and that’s not currently done.
Since TLS 1.2 sends the server certificate in plaintext it seems like ESNI wouldn't be particularly useful there anyway, no?
Indeed. I guess someone could argue about wildcard certificates or when multiple names are present but the names are usually related so I agree there’s no significant benefit from encrypting the SNI. The ECH proposal for Encrypted SNI is almost ready, but for a very small debate. The original proposal was using trial description to distinguish between ECH aware responses to the encrypted inner Client-Hello from non ECH aware response to the "cover" outer CH. This is problematic in the QUIC use case. The latest proposal is to embed a "hint" in 8 bytes of the Server Random. The proposal was for ECH-aware servers to use eight bytes of a hash of the inner Client Random as a hint. Analysis shows that this enables two attacks: 1) If the Client Hello is replayed, the same hint will be present in the response to the original CH and to the response to the copy, providing observers with a clear indication that ECH was used. 2) If the Client Hello is intercepted by an MITM attacker, the attacker can rewrite the server's response before presenting it to the client. The attacker formats its own Server Hello that reuses the Server Random from the original server response, but use its own key share, etc. The hint will cause the ECH aware client to create an handshake key using the inner CH. In a QUIC set up, the MITM attacker can easily detect that, before even transmitting the server's certificate. Thus, the MITM attacker can detect usage of ECH. We have a simple proposal that solves the replay attack: set the hint as a hash of both the inner Client Random and the "non-hint" bytes of the Server Random. That's clearly a good idea, but it does not solve the active MITM attack. Solving that requires tying the hint with the handshake key derived by the original server, for example by hashing the inner Client Random, the "non-hint" bytes of the Server Random, and the server key share. That's harder to implement, so the question is about cost and opportunity. This all relates to how much ECH is "sticking out". The current stance is that a passive attacker cannot distinguish between a client using ECH to access a hidden SNI and a client merely greasing the ECH extension. The observation is that there may be many potential active attacks, especially if the server shares its ESNI/ECH configuration publicly. If there are many such attacks, and if defense of the hint against MITM is hard to implement, implementing the defense might make the code more fragile, with little actual gain. But I am not convinced by that argument, because it smells a lot like "the other side of the boat is leaking too, why should I plug this particular leak?" And so, at Chris Wood's request, I am sending this message to the list. -- Christian Huitema
Ok, in that case it is worth making the don’t “stick out” property more precise. 1) We can either try to prevent the attacker from learning whether ECH was actually used in a particular connection, or 2) We can try to prevent the attacker from learning whether the server supports ECH at all. I gather the consensus is that we should try to obtain (2) at least until ECH support becomes ubiquitous on the server side? Since TLS 1.3 moves all but a few SH extensions to EE, I suppose there isn’t any other extension to hide this signal in. In that case, I guess using a value derived from the handshake key as part of the ServerRandom should probably work. That is, when constructing the ServerHello: first set the first N bytes of the server random to 0 compute the hanshake secret (based on either the inner or outer CH) compute the signal of N bytes as signal = HKDF-Expand-Label(handshake secret, “s hs hmac”, Transcript-Hash(ClientHello..ServerHello), N) replace the first N bytes of the server random with signal This is a somewhat icky design, but I think we should be able to justify it for (say) N <= 16 bytes.

use ClientHelloInner or reject it and use ClientHelloOuter.  In handling the server's response, the client's first step is to determine which value was used.  The client presumes acceptance if  <ins> the last 8 bytes of ServerHello.random are equal to the first 8 bytes of "accept_confirmation" as defined in backend-server.  Otherwise, it </ins> presumes rejection. 6.1.2.1. Upon receipt of an empty "encrypted_client_hello" extension, if the backend server negotiates TLS 1.3 or higher, then it MUST confirm ECH  <ins> acceptance to the client by computing its ServerHello as described here. The backend server begins by generating a message ServerHelloECHConf, which is identical in content to a ServerHello message with the exception that ServerHelloECHConf.random is equal to 24 random bytes followed by 8 zero bytes.  It then computes a string where Derive-Secret and Handshake Secret are as specified in RFC8446, Section 7.1 and ClientHelloInner..ServerHelloECHConf refers to the sequence of handshake messages beginning with the first ClientHello and ending with ServerHelloECHConf.  Finally, the backend server constructs its ServerHello message so that it is equal to ServerHelloECHConf but with the last 8 bytes of ServerHello.random set to the first 8 bytes of accept_confirmation. </ins> The backend server MUST NOT perform this operation if it negotiated TLS 1.2 or below.  Note that doing so would overwrite the downgrade


4. <del> ECH uses draft-05 of HPKE for public key encryption I-D.irtf-cfrg- </del>  hpke.  The ECH configuration is defined by the following "ECHConfig" structure.

ESNI seems to rely on TLS 1.3 KeyShareEntry which makes it incompatible with TLS 1.2. That's unfortunate with that "The protocol designed in this document is quite straightforward." The gist of ESNI comes down to "encryptedservername extension, which contains the true extension encrypted under the provider’s public key." That seems pretty generic and backwards compatible.
To prevent cut-and-paste attacks, you need to bind ESNI to the CH. That means we’d need 1.2 clients to send a key share in the CH to use when deriving encryption keying material, and that’s not currently done.
Since TLS 1.2 sends the server certificate in plaintext it seems like ESNI wouldn't be particularly useful there anyway, no?
Indeed. I guess someone could argue about wildcard certificates or when multiple names are present but the names are usually related so I agree there’s no significant benefit from encrypting the SNI.

4.  <ins> ECH uses draft-06 of HPKE for public key encryption I-D.irtf-cfrg- </ins> hpke.  The ECH configuration is defined by the following "ECHConfig" structure.


would compute when offering ECH, padded according to padding. When sending a second ClientHello in response to a HelloRetryRequest, <del> the client copies the "cipher_suite" field from the first ClientHello.  It sets "config_id", and "enc" to the empty string. Finally, it generates a new "payload" field, using the length of a padded second EncodedClientHelloInner for L. </del>  If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with

This is a temporary mitigation for the compatibility risks from , as any clients experimenting with GREASE will impact servers that otherwise don't expect ECH. To resolve this issue overall, we may need a more involved solution.
I just noticed this as I was pondering our various HRR issues. RFC8446, section 4.1.2 says: URL The "and present in the HelloRetryRequest" portion is fun. In RFC8446 as written, we're not allowed to change the extension on the second ClientHello unless the server sent in HelloRetryRequest, which it doesn't. With the protocol as-is, it seems we'll at least need some text to deal with the contradiction. The impact is practice is thankfully limited. If the servers does not enforce consistency between the two ClientHellos, it doesn't matter. (They're not required to, but they're not explicitly forbidden from it either, and a stateless server may need to enforce some minimum consistency to avoid getting into a broken state.) If a server does, ECH's behavior on HRR will break it. Such a server is supposed to handshake with ClientHelloOuter, so it just affects the retry flow. That's maybe okay, but mildly annoying. Administrators using server software that checks need to know to first upgrade to a version that doesn't check before trying to deploy ECH. We can also add an ECH extension to HRR. Then, on ECH-reject + HRR, the server perhaps wouldn't send the extension and the client would be required to replay the extension. This is a waste in many ways, but would work. This would break don't stick out, but some threat models, don't stick out with HRR is hopeless anyway. (See URL) It would also be weirdly asymmetric between ECH and SH. That said, this constraint is also pretty obnoxious. For instance, currently adds a new padding extension codepoint, because the old one isn't defined for other messages. However, the RFC8446 allowance only works for the existing padding code point, so you don't want to send any other code points in ClientHello, lest the server force you to keep it unchanged in ClientHello2. So maybe we should confirm no servers did the fully strict check and undo that constraint? (Although, for , I personally favor the padding message strategy anyway.)
NAME points out the Go implementation does check CH1 and CH2 consistency. Fortunately, it only checks known extensions, so it's still compatible with the ECH draft. URL Checking only know extensions is basically saying we should strike "and present in the HelloRetryRequest" in rfc8446bis and make sure everyone knows not to check this.
I don't think we can just strike this text because the point of that text was that you couldn't like change your KeyShares if the HRR didn't tell you to. But I agree we can sharpen this in some sensible way.
I figured that case was covered by "defined in the future", which I'm not proposing we delete. keyshare isn't defined in the future, so it falls under existing rules. The existing rules (bullet points over that one) only let you change keyshare "[i]f a "key_share" extension was supplied in the HelloRetryRequest". But perhaps that's a little roundabout. The rules I'm thinking of are (changes emphasized): Each extension gets to define how it reacts to HRR. It typically is sensitive the corresponding HRR extension, but doesn't have to be. RFC8446(bis) defines the rules for all existing extensions. Future extensions define their own rules. (And probably if they don't say anything, we assume they don't get to change.) As a server, you can make assumptions based on the rules for extensions you know about. You have to tolerate random changes in extensions you don't know about, because you don't know their rules.* I'm running a probe on a bunch of servers to see how they react to changes in cipher suites, curves, and a random extension. It seems most don't notice. Some (I'm guessing Go) notice cipher suites and curves. None notice a random extension. That suggests the change will be compatible.
I spoke too soon. LibreSSL breaks if you change an extension they don't recognize. :-(
So draft-09 isn't tied up with this, I've uploaded URL as a bandaid for the potential compatibility issue. (I say potential because it's only a problem if the server checks unknown extensions and has different enough key share preferences from the client that it needs HRR. HRR is pretty rare, so it may not actually be an issue. For Chrome, at least, I don't think the particular servers I found actually trigger HRR. But it is still a risk for clients or servers with different configurations.)
Given the compatibility concerns NAME found, and based on discussion in IETF 109, I think this is prudent to merge for -09. Doing so now.

would compute when offering ECH, padded according to padding. When sending a second ClientHello in response to a HelloRetryRequest,  <ins> the client copies the entire "encrypted_client_hello" extension from the first ClientHello. [[OPEN ISSUE: The above doesn't match HRR handling for either ECH acceptance or rejection.  See issue #358.]] </ins> If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with


To prevent a network attacker from modifying the reconstructed ClientHelloInner (see flow-clienthello-malleability), ECH <del> 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. </del>  Note the decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the

This isn't critical but I think it might be better to allow use of a single-shot HPKE API that doesn't need to first and separately generate a public/private pair before calling Seal(). The downside of forcing two HPKE calls is perhaps mostly that someone will be tempted to re-use that key pair for something else or a 2nd attempt with maybe bad effects. If someone ever did HPKE in h/w there might also be a security difference. I'm also not entirely sure what we're protecting by including that public value ("enc") in the AAD as well so that could be better documented at least, if we keep things as-is, but perhaps including that field is really a bit of overkill?
A single-shot HPKE already isn't viable because of HRR. You need to keep the second HPKE context around in case you need to encrypt second ClientHello. That said, I'm perfectly happy with omitting it. (Or including it.) This dates to URL and URL Of the added fields, and are already incorporated into HPKE itself anyway, so we shouldn't need to redo it in ECH. That leaves , which is... almost redundant. It's part of ECHConfig which is already authenticated, except in the , where it is otherwise unauthenticated. But it's also ignored in that mode so... ¯()_/¯
I've not done anything specific yet wrt HRR, and I'm guessing those details are still in-play to some extent, so not sure. But yeah, if the 2nd CH has to use the same keying then that may be the case. I agree. If there's a good reason to include it in the AAD that'd be fine, but I'm puzzled as to what that reason may be. Cheers, S.
NAME what is "it" here? The AAD? That seems orthogonal to whether or not the HPKE context (and shared KEM secret) are reused for CH2.
Sorry, yeah, that was unclear. By "it" I mean in AAD.
The reason to authenticate these things was to be as conservative as possible. Of course, we don't have any formal analysis that tells us whether or not it's strictly necessary. But given that single-shot HPKE isn't viable in this application, I'd suggest we keep it as-is. (Unless another reason to change this comes along :)
NAME given that this is needed for HRR to avoid two KEM operations, are you comfortable closing this?
(I almost missed this in today's torrent of githubbery...) Not sure. Given that HRR is rare and only needed because no good group was chosen, (and hence involves more D-H stuff in any case), I'm not clear that saving a KEM operation (in the group for ECH) is a convincing target. Is there a pointer anywhere to the analysis that such a saving is not a premature optimisation? It may be an ok design of course, but I wonder if a simpler yet less CPU efficient design might exist and be overall better. S. PS: It also strikes me that if anyone ever designs a TLS extension that contains a value (e.g. a time value) that needs to be within an RTT of something external to the h/s, then we're maybe breaking that before it's been invented.
Reusing the context is not just about performance. It also avoids a host of complexity and weird corner cases when ECH decryptions are RPC calls. It also reduces server complexity by not needing to check for the client picking a different ECHConfig the second time, etc. I would oppose regressing those complexity improvements.
Fair enough. Well, HRR+ECH seems to be it's own weirdly complex corner case as a result though. But, that may be unavoidable. I'll re-read the HRR proposal before the interim but if that requires re-tx'ing the same set of inner extension values (possibly other than URL) then that itself may be the cause of corner cases. (And break some putative yet to be defined extensions maybe.) Anyway, I think it makes sense to leave this issue open until we're a bit more settled on HRR handling. Cheers, S.
is the PR that switched it, by the way. Also some discussion in .
NAME now that is merged, which reuses the HPKE context for HRR, which accepts that HRR must be dealt with in the protocol, along with , I'd like to propose closing this. Any objections?
Reluctant acceptance here:-) S
Sounds good -- thanks!
NAME mentions possibly binding the outer CH to the ECH encryption in . If we don't do that, we should at least bind the encryption to the non-encrypted parts of the ECH extension (, , and so on). cc NAME
I also like this idea :) AD is cheap.
Closing in favor of . We can revisit the ECH extension AD information later if desired.
Re-opening to consider adding all data to AAD. NAME wrote out the flow if we did this: Make your CHInner. This should largely be running through your usual CH assembly logic. Encode it with the extensions you expect to compress. Make your CHOuter, minus the ECH extension. This should largely run through your existing CH assembly logic. Run Deserialize and SetupBaseS. Compute CHOuterAAD with half an ECH extension. Take EncodedCHInner and CHOuterAAD and run through the context.Seal logic to assemble the final CHOuter. So, not great, but also not too terrible. I'd prefer we avoid falling into the "gee, I sure wish we bound more to the encryption" state, given the history of ECH, though I agree that this isn't cheap. NAME what do you think?
To be clear, the suggestion is to change AEAD encryption so that it authenticates , , and , correct? This change seems sensible to me. One might argue that this is redundant because these parameters are incorporated into the transcript (on ECH rejection). On the other hand, ECH usage is resolved before the transcript is known! It seems like a good idea to authenticate these parameters before consuming them.
They aren't incorporated on ECH accept, which is mildly interesting. ECH accept also doesn't negotiate with them, so it's probably okay, though there is a "don't stick out" failure (depending on threat model) for the optional config_id flow if we don't authenticate them.
ESNI seems to rely on TLS 1.3 KeyShareEntry which makes it incompatible with TLS 1.2. That's unfortunate with that "The protocol designed in this document is quite straightforward." The gist of ESNI comes down to "encryptedservername extension, which contains the true extension encrypted under the provider’s public key." That seems pretty generic and backwards compatible.
To prevent cut-and-paste attacks, you need to bind ESNI to the CH. That means we’d need 1.2 clients to send a key share in the CH to use when deriving encryption keying material, and that’s not currently done.
Since TLS 1.2 sends the server certificate in plaintext it seems like ESNI wouldn't be particularly useful there anyway, no?
Indeed. I guess someone could argue about wildcard certificates or when multiple names are present but the names are usually related so I agree there’s no significant benefit from encrypting the SNI.

To prevent a network attacker from modifying the reconstructed ClientHelloInner (see flow-clienthello-malleability), ECH  <ins> authenticates ClientHelloOuter by computing ClientHelloOuterAAD as described below and passing it in as the associated data for HPKE sealing and opening operations.  ClientHelloOuterAAD has the following structure: The first three parameters are equal to, respectively, the "ClientECH.cipher_suite", "ClientECH.config_id", and "ClientECH.enc" fields of the payload of the "encrypted_client_hello" extension.  The last parameter, "outer_hello", is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension removed. </ins> Note the decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the


7. <del> Servers that support ECH play one of two roles, depending on the extension in the ClientHello.  If the "encrypted_client_hello" extension is present, the server acts as a client-facing server and proceeds as described in client-facing-server to extract a ClientHelloInner, if available.  If the "ech_is_inner" extension is present and the "encrypted_client_hello" extension is not present, the server acts as a backend server and proceeds as described in backend-server. </del>  7.1. Upon receipt of an "ech_is_inner" extension in a ClientHello, if the backend server negotiates TLS 1.3 or higher, then it MUST confirm ECH acceptance to the client by computing its ServerHello as described <del> here.  If both the "ech_is_inner" and "encrypted_client_hello" extensions are present in the ClientHello, the backend server MUST abort with an "illegal_parameter" alert. </del>  The backend server begins by generating a message ServerHelloECHConf, which is identical in content to a ServerHello message with the

The text for servers rejecting both extensions present is under Backend Server, but the Server Behavior text gates that section under just the one extension. Likewise, the client-facing server text allows for both extensions to be present. Move the illegal_parameter text around and write out the four cases.

7.  <ins> Servers that support ECH play one of two roles, depending on which of the "ech_is_inner" and "encrypted_client_hello" extensions are present in the ClientHello: If both the "ech_is_inner" and "encrypted_client_hello" extensions are present in the ClientHello, the backend server MUST abort with an "illegal_parameter" alert. If only the "encrypted_client_hello" extension is present, the server acts as a client-facing server and proceeds as described in client-facing-server to extract a ClientHelloInner, if available. If only the "ech_is_inner" extension is present and the "encrypted_client_hello" extension is not present, the server acts as a backend server and proceeds as described in backend-server. If neither extension is present, the server completes the handshake normally, as described in RFC8446. </ins> 7.1. Upon receipt of an "ech_is_inner" extension in a ClientHello, if the backend server negotiates TLS 1.3 or higher, then it MUST confirm ECH acceptance to the client by computing its ServerHello as described  <ins> here. </ins> The backend server begins by generating a message ServerHelloECHConf, which is identical in content to a ServerHello message with the


config), "tls ech config id", 8)", unless it is optional for an application; see optional-configs. "config" is the "ECHConfig" structure.  "Extract" and "Expand" are as specified by the cipher <del> suite KDF.  (Passing the literal """" as the salt is interpreted by "Extract" as no salt being provided.) </del>  The HPKE encapsulated key, used by servers to decrypt the corresponding "payload" field.

The backtick-quoted empty string translates to """" in the text output.
Not sure if this is the best solution, but the quadruple quote in the txt is goofy.
This seems fine, yeah. Thanks for catching it!

config), "tls ech config id", 8)", unless it is optional for an application; see optional-configs. "config" is the "ECHConfig" structure.  "Extract" and "Expand" are as specified by the cipher  <ins> 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 "payload" field.


TLS 1.2 or below.  Note that doing so would overwrite the downgrade signal for TLS 1.3 (see RFC8446, Section 4.1.3).  8. Unlike most TLS extensions, placing the SNI value in an ECH extension

~It's sufficient to ignore it, I think.~ The backend server enforces the extension's emptiness.

TLS 1.2 or below.  Note that doing so would overwrite the downgrade signal for TLS 1.3 (see RFC8446, Section 4.1.3). <ins> The "ech_is_inner" is expected to have an empty payload.  If the payload is non-empty (i.e., the length of the "extension_data" field is non-zero) then the backend server MUST abort the handshake with an "illegal_parameter" alert. </ins> 8. Unlike most TLS extensions, placing the SNI value in an ECH extension


fields of the payload of the "encrypted_client_hello" extension.  The last parameter, "outer_hello", is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension removed.  Note the decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the

Clarifies that serialization of this structure doesn't include the message type and length. We do the same thing for the serialization of EncodedClientHelloInner, but this makes it explicit in case it's not clear from context.

fields of the payload of the "encrypted_client_hello" extension.  The last parameter, "outer_hello", is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension removed. <ins> Note this does not include the four-byte header included in the Handshake structure. </ins> Note the decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the


SHOULD take steps to minimize or protect such requests during certificate validation.  10.7. To signal acceptance, the backend server overwrites 8 bytes of its

Addresses
Done. Thanks!
(Filing this as a separate ticket so I don't forget to propose new text.) added this text: I think this statement is a bit strong and overstates the issue. It reads as if resuming connections across ECH and non-ECH links the two connections. If we're talking about someone other than the server, this isn't true. Tickets are issued encrypted and, in ECH, even sent encrypted. Even if they were sent in the clear, TLS 1.3's protects against such correlations. Rather, the correlation in is a much weaker one: based on either the size of the ClientHello or whether the server accepted the ticket, you learn a single bit for whether it was some previous connection made by that client. You don't know which previous connection it was, and you don't know if it was an ECH connection or a non-ECH connection. In fact what you learn... more-or-less aligns with the anonymity set. See URL (If we're talking about the server, yes, resuming allows the server to link the two connections. That's fundamental to resumption and is in the purview of URL I don't think most applications would consider ECH vs non-ECH a correlation boundary w.r.t. the server itself.)
The original text doesn't necessarily imply that on-the-wire tickets are the linking factor. Rather that the shape of the handshake, time it takes to complete, etc, can be a proxy for whether or not resumption occurred. I don't think we need to change anything here. (But I also would not object to someone else sending a PR!)
(This came up in a discussion between me and NAME So, the current text says "For example, a client that [does X] may [do Y], thereby linking the resulting ECH connection to the original non-ECH connection". That implies that doing X and Y allows linking the two connections together. You're right that tickets may not be the only linking factor, but the other channels don't let you link connections, only, as you say, whether or not resumption occurred. That single boolean doesn't tell you which non-ECH connection corresponded to the ECH connection, or what the name was. It only tells you that it's the same name as some previous connection to the anonymity set. It might be a non-ECH one (but you don't know which) or it might be an ECH one.
Feel free to suggest text to reword this. I think the example was meant to be general enough to suggest that side channels exist. I definitely do not feel strongly about the exact wording here.
I agree that the text as-is overstates the issue.
Martin pointed out a potential SNI leak via resumption under the following conditions: A client connects to server "A" without ECH, and receives a session ticket. The client later resumes the connection to server "A" using ECH. An observer can see that a PSK was accepted (via SH containing presharedkey). If any of the hosts previously visited by the client are known, an attacker can do a lookup of each for a config_id matching the resumed session. This effectively reduces the size of the anonymity set for a particular client/victim. While it's true that the hostname is already leaked by the first connection without ECH, this seems to violate the security goals as they are written. We used to require separation between non-ECH and ECH PSKs, but it was removed in .
Ah fun. I agree that observing a resumption does increase the probability the connection was one of the previous non-ECH connections the client made, when server just deployed ECH. (Though the client could also be visiting someone else in the anonymity set for a second time.) I'm not sure I follow what's going on in (3) though. Is that just checking if one of the previously-visited hosts shares a client-facing server with the connection under attack? That seems like something the attacker can regardless. (With or without resumption, one would expect a client's visited sites pre-ECH-deployment to somewhat predict with the ones post-ECH-deployment.) As an aside, even if SH.presharedkey was hidden, the observer can infer resumption based on whether the server responded with a lot of data (it sent a certificate in there), or very little data (it sent no certificate in there). Unless the server adds some truly wasteful padding on resumption connections.
(3) just provides additional evidence that the SNI guess is correct (by further constraining set of possible hostnames).
To mitigate this, it ought to be sufficient to have the client track whether a given PSK was established ruing a ECH handshake or a non-ECH handshake. It seems like this would be a lot more involved for a server to manage.
Is it actually additional evidence? It doesn't seem to narrow the usual ECH anonymity set, just identifies it. If I understand you correctly, (3) is: The attacker observes a connection from the client with some configid and some secret SNI A. The attacker queries DNS or the recovery flow[] for the cleartext SNIs that the client previously visited and sees if one of them, B, has an ECHConfig with matching configid. If there is a match, the attacker learns that B was in the same anonymity set as A. The attacker learns this whether or not the connection resumes. The attacker also hasn't expanded the anonymity sent. In fact, they didn't even need to query sites the client previously visited. They could just as easily query the top N sites or a list of popular hosting providers known to support ECH. I think this information is part of the baseline for our security goals. It is true that a client which, before ECH, leaked that it visited A many times is more likely to visit A again in the future. That seems fundamental. I think the interesting thing is that (2) tells the observer that it is some repeat visit, which increases the chance it's A. (But doesn't guarantee it. Maybe I've found a new site in the anonymity set that I like.) I don't think PSK separation even mitigates this. Repeat post-ECH visits still reveal whether resumption happened in ServerHello. And even if we fixed that, depending on how much padding the client is willing to add, the ClientHello size will reveal whether the client had a ticket to offer. I think the only thing PSK separation mitigates is that immediately after the client/server pair deploys ECH, you get to reset the repeat visit counter. But once you count to 2 again, you've got the same signal as before. [*] Although there's no actual requirement that the recovery flow use the same ECHConfig as the long-lived DNS config...
Well no, but since they have that information, they can further reduce the anonymity set. (The attacker doesn't need to consider the sites the user never visited.) As soon as you involve non-ECH in a session, the security guarantees just don't hold. I'm fine with just being explicit about that in the draft.
That's probably the best way to resolve this, given the impracticality and constraints (to time of deployment or enablement). I'll have a PR up by the end of the week.
That works for me. Thanks, NAME
This LGTM. I have a slight rephrasing, but feel free to reject!

SHOULD take steps to minimize or protect such requests during certificate validation. <ins> Attacks that rely on non-ECH traffic to infer server identity in an ECH connection are out of scope for this document.  For example, a client that connects to a particular host prior to ECH deployment may later resume a connection to that same host after ECH deployment, thereby linking the resulting ECH connection to the original non-ECH connection. </ins> 10.7. To signal acceptance, the backend server overwrites 8 bytes of its