The "ECHConfigContents" structure contains the following fields: <del> The non-empty name of the client-facing server, i.e., the entity trusted to update the ECH configuration.  This is used to correct misconfigured clients, as described in handle-server-response. The HPKE public key used by the client to encrypt ClientHelloInner. The HPKE KEM identifier corresponding to "public_key".  Clients MUST ignore any "ECHConfig" structure with a key using a KEM they do not support. The list of HPKE KDF and AEAD identifier pairs clients can use for encrypting ClientHelloInner. </del>  The longest name of a backend server, if known.  If this value is not known it can be set to zero, in which case clients SHOULD use removed from the anonymity set during the lifetime of a particular ECH configuration.  A list of extensions that the client must take into consideration when generating a ClientHello message.  These are described below (config-extensions).  The client-facing server advertises a sequence of ECH configurations to clients, serialized as follows. When offered by the client, the extension appears only in the ClientHelloOuter.  The payload MUST have the following structure:  The cipher suite used to encrypt ClientHelloInner.  This MUST match a value provided in the corresponding "ECHConfigContents.cipher_suites" list. <del> The configuration identifier, equal to "Expand(Extract("", 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.) </del> The HPKE encapsulated key, used by servers to decrypt the corresponding "payload" field. The value of the "encrypted_client_hello" extension in the ClientHelloOuter is a "ClientECH" with the following values: <del> "cipher_suite", the client's chosen cipher suite; </del>  <del> "config_id", the identifier of the chosen ECHConfig structure; </del>  "enc", as computed above; and "payload", as computed above. If optional configuration identifiers (see optional-configs)) are <del> used, the "config_id" field MAY be empty or randomly generated. </del>  Unless specified by the application using (D)TLS or externally configured on both sides, implementations MUST compute the field as specified in encrypted-client-hello. 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. <del> "config_id" is replaced with the empty string. </del> "enc" is replaced with the empty string. "payload" is replaced with the value computed above. RFC8701 "encrypted_client_hello" extension in the first ClientHello as follows: <del> Set the "cipher_suite" field to a supported ECHCipherSuite.  The selection SHOULD vary to exercise all supported configurations, but MAY be held constant for successive connections to the same server in the same session. </del>  <del> Set the "config_id" field to a randomly-generated 8-byte string. </del>  Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. Collect all known ECHConfigs as candidates, with trial decryption below determining the final selection. <del> Some uses of ECH, such as local discovery mode, may omit the </del>  ClientECH.config_id since it can be used as a tracking vector.  In such cases, the second method should be used for matching ClientECH to known ECHConfig.  See optional-configs.  Unless specified by the Optional configuration identifiers may be useful in scenarios where clients and client-facing servers do not want to reveal information about the client-facing server in the "encrypted_client_hello" <del> extension.  In such settings, clients send either an empty config_id or a randomly generated config_id in the ClientECH.  (The precise implementation choice for this mechanism is out of scope for this document.)  Servers in these settings must perform trial decryption since they cannot identify the client's chosen ECH key using the config_id value.  As a result, support for optional configuration identifiers may exacerbate DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any known ECH key, in order to force wasteful decryption.  Servers that support this feature should, for example, implement some form of rate limiting mechanism to limit the damage caused by such attacks. </del>  10.5.

We may want to change how we deal with GREASE (does a randomly selected byte stick out?), but this should address the issue for now. I think we can (should) treat these issues separately, and focus just on the simplification here. cc NAME NAME NAME NAME
My intuition is that one byte is too short; I think 32 bits would be wiser. However, this is a slightly less serious problem than usual because we can rev the ECHConfig version number and the ECH extension if we find something longer is needed.
Yep, that sounds reasonable to me.
This is somewhat of a bike shed, but 8 bytes seems quite large for something that will only have a handful of possible values at any given point in time. Is there a reason to make this longer beyond the performance cost of decryption failing as a result of collisions?
I guess the only reason I can see is that we've settled on q^2 / 2^64 as our threshold for operational risks. In particular, the "acceptance" signal in the URL is 8 bytes (URL). I suppose we could get away with a smaller identifier, since the q is much smaller in this case.
This seems different from acceptance signal collision, which actually causes breakage. In contrast, a key_id collision here just means that the server wastes cycles attempting decryption, right?
A config collision is bad because the server has to figure out which config the client used. It can only do so by trial decryption.
Eh? If there's a collision, the server knows which key to try (the one it collided with!), and it will do so, fail to decrypt, and move on as per usual. (There's no trial decryption here, but maybe I'm missing something.)
Just to put a fine point on this: the spec SHOULD NOT require that implementations support trial decryption. In particular, we ought to ensure that, with high probability, configuration identifiers don't collide. A one-byte, randomly-generated identifier would make collisions inevitable, meaning that implementations will have to support trial decryption.
Yeah, that's reasonable. If we're going to abide by this, then the ID needs to be at least 8 bytes. I'd be fine with that that for now assuming others are too. (NAME suggested it be longer at the top anyway, so many we should just do it.)
NAME we're now at 8 bytes :-)
I don't think one-byte necessary collides. First, note that the number of configs a server needs to have at a time is relatively small. Past the config's DNS TTL, you can retire it. That means you only need O(dnsttl / timebetweenrotation) configs at once. (From , I think the exact value is 1 + ceil(dnsttl / timebetweenrotation). Maybe add a little bit of leeway for people using stale DNS caches, though note we have a recovery flow so you only need to cover most, not all, of stale caches.) This means we don't need to worry about that many values. Additionally, everything here is server-allocated. Whether you're worried about a birthday bound depends on how you allocate IDs. If it's random, you have a birthday bound, yes. If you cycle through a counter, you get the full range. (Compare how QUIC and DTLS 1.3 only send the low-order bits of the epoch.) If it's random, but you check for duplicates at key issuance, you also get the full range. The counters or duplicate checks are trickier to issue, of course, though you can implement the trial decryption as a safety net. As of , the trial decryption and config_id modes are already unified. Specifically, decryption failure falls back to the outer ClientHello.
I agree with everything you said, but want to drill in on this: This seems to be key. I agree that the fallback case is clear and well understood, though one consideration is the case wherein a client attempts GREASE to a server that actually supports ECH. The probability of collision in that scenario is high, and if that scenario happens often, then decryption failure and fallback to the outer CH happens quite regularly. Bumping the length back to eight bytes removes the collision probability for that scenario, I think. I'd like to propose we treat the two problems here separately. That is, let's first see if we can land the key ID change (from a client-computed hash to a server-selected ID). And then let's see if we can safely make that ID smaller in size. I don't think we need to address both here. Would that work?
I think that the only thing that pushes toward a larger identifier space for me is the risk of grease colliding with an active key. I agree with David, that your likely number of keys is ~2 (I might go as high as 4 if the server is in an extended period of offering two types of KEM). With a one-byte identifier, that's only 1/64 greased sessions hitting the expensive decryption code. Flexibility here seems appealing, but I'm not sure that this is a place that needs it. Operationally, this seems very much dependent on centralization, so I'm not buying any story based on loose coordination. I also note that very small spaces reduce the options servers have for targeted tracking through the use of keys (though it is impossible to eliminate tracking, small spaces make the cost higher to operate tracking at any reasonable scale). That is why I was motivated to go as low as 1 byte. If you prefer a much lower probability of having to deal with failed decryption in response to greasing, then 2 bytes gets that down to lower than your likely failed connection rate at 1/16k.
Indeed, this is why I propose we separate these issues. Specifically, let's use this PR to land the server-chosen key ID, and then make it smaller (as needed) in a followup change. (Personally, I'm fine with one, two, or four bytes here, as I think the decryption failure case is unavoidable.) I'll file an issue for the size of the ID in a bit. NAME NAME NAME NAME what say you? :-)
Ditto.
After very useful discussion with many folks, it seems now is the time to remove config_id. There are a couple reasons to do this: Servers need to know the correct hash function to use when computing the ID, and possibly need to compute multiple IDs for a given public key (since a config can have multiple hash-based KDFs), which is somewhat annoying and redundant. The entire ECHConfig is already authenticated, so using a hash to prove that the client made a determination based on the correct config contents is unnecessary. Servers will likely only ever have a small handful of keys in motion at any point in time, so anything more than a couple of bits to identify one of said keys is excessive. ... there are probably more. The original design . (Oops!) It seems we're now in a better position to bring this back, but with some simplifications.
Hiya, Can you send a pointer to those discussions? (Sorry, I seem to have missed 'em;-) Ta, S.
Sorry -- these were informal conversations off-list.
Ah. That's a recurring problem. And even this exchange is actually off-list. Can we please fix that? I think I've whined about that a few times now, and would hope that continual whining is not a requirement;-) S.
few times now, and would hope that continual whining is not a requirement;-) Fix what? A few of us had a private conversation about some ideas and came to the conclusion that it would be better to replace the hash with a key_id. Then Chris and Martin brought it to a Github issue. This is precisely in line with the WG consensus to use Github issue discussion mode, as documented in 8874 S 5.3.
I'm guessing that was a private discussion amongst what could well be described as the usual suspects or an old-boys club (or are a few of you "many folks?"). Correct me if I'm wrong but that had zero visibility and zero traceability and zero accountability for anyone who isn't one of those old boys. And yet we're zapping a field in a PDU from what was declared as an "implementation draft" a day or so after one major deployment declared they had an interop target. To me, that smells of disrespect for outsiders tbh. You can probably guess what I think about that state of affairs:-) I don't buy that tbh. Chris' mail was presented as a conclusion, from a wg-chair following a private discussion that for sure excluded some implementers. (Me being one, albeit a not very good one:-) And you yourself are an ex WG-chair so I'd guess you ought also care about exclusivity here. What gain is there from that kind of behaviour? I don't see it myself. IMO, there is no need nor benefit from being so exclusive wrt this spec, yet that is what is happening and has been for a few years. I'd encourage those of you who may be "old-boys" to consider whether or not that is really the best way to proceed. IMO - it is not, you lose way more than you might think you gain, almost all the time. Cheers, S. PS: I don't disagree with the outcome, though have opinions on what -10 might contain. I do very much dislike the awful process, which is my point here.
NAME I would completely agree with you had the proposed PR been merged and the associated issue been closed. That has not happened. Further, it appears that you might be reading a bit more into Chris' issue summary than is really there. He is not acting in his WG chair capacity here because he is an author; I know he knows that and I really hope you can give him the benefit of the doubt here so that we do not have to suffer the "no WG chair hat on" in every message/post. The process, as I see it, is pretty darn near normal. Somebody files an issue and optionally (but preferably) files a PR to address said issue. Discussion ensues amd if substantive it's taken to the list. The PR may or may not be modified based on that discussion. If consensus is reached, the PR is merged and the issue is closed. We are at the discussion point. Can we do that now?
Perhaps it'd be better to not start a mail with "After very useful discussion with many folks, it seems now is the time to remove config_id." in cases where that discussion was purely private? I can understand it being hard to keep track with the various tools, but for me, that still reads as a decision being announced even if that was not the intent. Sorry for being over-sensitive to this but IMO there is a history and ongoing tendency with this spec for a subset of people to act as if they and not the IETF/WG have change control. I fully accept that they are not deliberately aiming to avoid having to garner WG consensus but consider that ongoing instances of carelessness such as the above demonstrate that there is an insider/outsider thing going on here. S.
Happy to have the identifier discussion separately. It is unfortunately not important or clear enough to lead to easy agreement. I can live with any outcome.One little nit then -> LGTM.

The "ECHConfigContents" structure contains the following fields:  <ins> A "HpkeKeyConfig" structure carrying the configuration information associated with the HPKE public key.  Note that this structure contains the "config_id" field, which applies to the entire ECHConfigContents.  Sites MUST NOT publish two differet"ECHConfigContents" values with the same "HpkeKeyConfig" value. </ins> The longest name of a backend server, if known.  If this value is not known it can be set to zero, in which case clients SHOULD use removed from the anonymity set during the lifetime of a particular ECH configuration. <ins> The non-empty name of the client-facing server, i.e., the entity trusted to update the ECH configuration.  This is used to correct misconfigured clients, as described in handle-server-response. </ins> A list of extensions that the client must take into consideration when generating a ClientHello message.  These are described below (config-extensions). <ins> The "HpkeKeyConfig" structure contains the following fields: An opaque identifier for the given HPKE key configuration.  This is used by clients to indicate the key used for ClientHello encryption. The HPKE KEM identifier corresponding to "public_key".  Clients MUST ignore any "ECHConfig" structure with a key using a KEM they do not support. The HPKE public key used by the client to encrypt ClientHelloInner. The list of HPKE KDF and AEAD identifier pairs clients can use for encrypting ClientHelloInner. </ins> The client-facing server advertises a sequence of ECH configurations to clients, serialized as follows. When offered by the client, the extension appears only in the ClientHelloOuter.  The payload MUST have the following structure: <ins> The ECHConfigContents.key_config.config_id for the chosen ECHConfig. </ins> The cipher suite used to encrypt ClientHelloInner.  This MUST match a value provided in the corresponding "ECHConfigContents.cipher_suites" list.  The HPKE encapsulated key, used by servers to decrypt the corresponding "payload" field. The value of the "encrypted_client_hello" extension in the ClientHelloOuter is a "ClientECH" with the following values:  <ins> "config_id", the identifier corresponding to the chosen ECHConfig structure; </ins>  <ins> "cipher_suite", the client's chosen cipher suite; </ins> "enc", as computed above; and "payload", as computed above. If optional configuration identifiers (see optional-configs)) are  <ins> used, "config_id" SHOULD be set to a randomly generated eight bytes. </ins> Unless specified by the application using (D)TLS or externally configured on both sides, implementations MUST compute the field as specified in encrypted-client-hello. The client then modifies the "encrypted_client_hello" extension in ClientHelloOuter as follows: <ins> "config_id" is replaced with the empty string. </ins> "cipher_suite" is unchanged and contains the client's chosen HPKE cipher suite.  "enc" is replaced with the empty string. "payload" is replaced with the value computed above. RFC8701 "encrypted_client_hello" extension in the first ClientHello as follows:  <ins> Set the "config_id" field to a random eight byte string. </ins>  <ins> Set the "cipher_suite" field to a supported HpkeSymmetricCipherSuite.  The selection SHOULD vary to exercise all supported configurations, but MAY be held constant for successive connections to the same server in the same session. </ins> Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. Collect all known ECHConfigs as candidates, with trial decryption below determining the final selection.  <ins> Some uses of ECH, such as local discovery mode, may randomize the </ins> ClientECH.config_id since it can be used as a tracking vector.  In such cases, the second method should be used for matching ClientECH to known ECHConfig.  See optional-configs.  Unless specified by the Optional configuration identifiers may be useful in scenarios where clients and client-facing servers do not want to reveal information about the client-facing server in the "encrypted_client_hello"  <ins> extension.  In such settings, clients send a randomly generated config_id in the ClientECH.  Servers in these settings must perform trial decryption since they cannot identify the client's chosen ECH key using the config_id value.  As a result, support for optional configuration identifiers may exacerbate DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any known ECH key, in order to force wasteful decryption.  Servers that support this feature should, for example, implement some form of rate limiting mechanism to limit the damage caused by such attacks. </ins> 10.5.


An ECHConfigs structure containing one or more ECHConfig structures, in decreasing order of preference, to be used by the <del> client in subsequent connection attempts. </del>  This document also defines the "ech_required" alert, which the client MUST send when it offered an "encrypted_client_hello" extension that handshake, authenticating for ECHConfig.contents.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application. <del> It MUST NOT use the retry keys. </del>  <del> Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an </del>  "ech_required" alert.  It then processes the "retry_configs" field from the server's "encrypted_client_hello" extension. The client MUST continue to use the previously-advertised configurations for subsequent connections.  This avoids introducing pinning concerns or a tracking vector, should a malicious server <del> present client-specific retry keys in order to identify the client in a subsequent ECH handshake. </del>  If none of the values provided in "retry_configs" contains a supported version, the client can regard ECH as securely disabled by

The reference to "retry keys" comes from when the field was called retrykeys and the type ESNIKeys. We've since renamed them to retryconfig and ECHConfig, so the prose should match.

An ECHConfigs structure containing one or more ECHConfig structures, in decreasing order of preference, to be used by the  <ins> client in subsequent connection attempts.  These are known as the server's "retry configurations". </ins> This document also defines the "ech_required" alert, which the client MUST send when it offered an "encrypted_client_hello" extension that handshake, authenticating for ECHConfig.contents.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  <ins> It MUST NOT use the retry configurations. </ins>  <ins> Otherwise, if both authentication and the handshake complete successfully, the client MUST abort the connection with an </ins> "ech_required" alert.  It then processes the "retry_configs" field from the server's "encrypted_client_hello" extension. The client MUST continue to use the previously-advertised configurations for subsequent connections.  This avoids introducing pinning concerns or a tracking vector, should a malicious server  <ins> present client-specific retry configurations in order to identify the client in a subsequent ECH handshake. </ins> If none of the values provided in "retry_configs" contains a supported version, the client can regard ECH as securely disabled by


[[OPEN ISSUE: if the client sends a GREASE "encrypted_client_hello" extension, should it also send a GREASE "pre_shared_key" extension? <del> If not, GREASE+ticket is a trivial distinguisher.]] </del>  Offering a GREASE extension is not considered offering an encrypted ClientHello for purposes of requirements in client-behavior.  In 10.1. ECH considers two types of attackers: passive and active.  Passive <del> attackers can read packets from the network.  They cannot perform any sort of active behavior such as probing servers or querying DNS.  A middlebox that filters based on plaintext packet contents is one </del>  example of a passive attacker.  In contrast, active attackers can <del> write packets into the network for malicious purposes, such as </del>  interfering with existing connections, probing servers, and querying DNS.  In short, an active attacker corresponds to the conventional threat model for TLS 1.3 RFC8446. contents of unencrypted handshake messages. Beyond these primary security and privacy goals, ECH also aims to <del> 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.  ECH aims to achieve both properties, assuming the attacker is passive and does not know the set of ECH configurations offered by the client-facing server.  It does not achieve these properties for active attackers.  More specifically: Passive attackers with a known ECH configuration can distinguish between a connection that negotiates ECH with that configuration and one which does not, because the latter used a GREASE "encrypted_client_hello" extension (as specified in grease-ech) or a different ECH configuration. Passive attackers without the ECH configuration cannot distinguish between a connection that negotiates ECH and one which uses a GREASE "encrypted_client_hello" extension. Active attackers can distinguish between a connection that negotiates ECH and one which uses a GREASE "encrypted_client_hello" extension. See dont-stick-out for more discussion about the "do not stick out" criteria from RFC8744. </del>  10.2. 10.8.4. <del> The only explicit signal indicating possible use of ECH is the ClientHello "encrypted_client_hello" extension.  Server handshake messages do not contain any signal indicating use or negotiation of ECH.  Clients MAY GREASE the "encrypted_client_hello" extension, as described in grease-ech, which helps ensure the ecosystem handles ECH correctly.  Moreover, as more clients enable ECH support, e.g., as normal part of Web browser functionality, with keys supplied by shared hosting providers, the presence of ECH extensions becomes less unusual and part of typical client behavior.  In other words, if all Web browsers start using ECH, the presence of this value will not signal unusual behavior to passive eavesdroppers. </del>  10.8.5.

This PR aims to resolve the discussion in issue by: clarifying goals/non-goals for "don't stick out"; and creating a TODO for a future experiment that assesses the deployability of the GREASE extension. Dealing with PSK sticking out has been assigned to .
Rebased on top of ECH-10 changes -- addressing NAME comments next.
NAME this is ready for your second pass. I took your suggestion of substantially reducing the amount of text. It should be a bit more concise and to the point, if less precise. I didn't mean to frame GREASE this way. Can you be more specific about how the text frames it this way?
As discussed in , there are two real-ECH/grease-ECH differentiators related to the PSK extension we may wish to address. Real ECH disallows the PSK extension in ClientHelloOuter, whereas grease ECH permits PSK in ClientHello. Real ECH allows PSK in ClientHelloInner. In case of acceptance, the backend server may respond with PSK in ServerHello. The response would appear to be unsolicited. something that would not happen in a grease-ECH or vanilla handshake. The question we will ask ourselves here is if/how to mitigate these differentiators. We might simply say that clients MAY (or SHOULD?) send a dummy PSK in the ClientHello(Outer). This ought to provide sufficient cover.
I don't mind a MAY on sending a dummy PSK in case (2), though I don't think it's a good idea for case (1). The case that really matters here is when the outer and inner ClientHello both lack PSKs: Suppose the buggy network is sensitive to (1) and only allows ECH extensions when the outer ClientHello has a PSK because it knows those can't be ECH. That network has effectively broken all connections because the only way for the client to get a PSK is to have connected to the server without a PSK at some point. Suppose instead the buggy network is sensitive to (2) and breaks if the inner ClientHello has a PSK but the outer does not. The connect will fail, but between single-use tickets and bounded client session caches, the client will quickly exhaust its tickets and get out of case (2). This is a performance issue, but the connections still work. (Though, yeah, adding the option to send a dummy one might help there. shrug)
I'm not following the following point. Could you elaborate? In any case, the case to mitigate (2) seems more compelling than the case to mitigate (1), since (2) would look like non-compliance to an ECH-ignorant network. Would be curious to see what others think.
So I think the benefit of GREASE-like strategies is that broader breakages now are more noticeable than focused breakage later. More noticeable breakage is more likely to be fixed, and less likely to make it to the ecosystem in the first place. This applies to ECH GREASE by saying we want all ClientHellos to look like ECH was deployed, so the network is less likely to develop intolerances. Differences between GREASE ECH and actual ECH thus matter because the network be intolerant to something that we failed to simulate. (1) says that, in actual ECH, all outer ClientHellos have no PSK. In GREASE ECH, some outer ClientHellos have a PSK while some do not. If we were to translate that to actual intolerance, it would mean the network rejects outer ClientHellos without a PSK. But that would break everything, ECH or non-ECH, anyway because clients don't send PSKs the first time[] they connect to a server. Clients only have PSKs to send the second time, and you can't connect a second time if you didn't connect a first time. Thus, even though (1) is a difference, it doesn't seem that difference prevents us from noticing intolerance. Agreed that mitigating (2) is more compelling than mitigating (1). [] First time overall, first time after a restart (if sessions aren't persisted), or first time after a long break (sessions expired).
I agree that (2) is more important. And I'm fine with a "MAY/SHOULD send an outer dummy PSK if you are using ECH and have an inner PSK." NAME if we were to append on a dummy PSK, what do you think would be a suitable length? (Bikeshedding over that should be fun.) As an aside, I wanted to drill in on this point: This seems true, if we're assuming the the intolerance is "break the connection," which I think is the right interpretation. But it occurs to me that there's a slightly weaker form of misbehavior thats's possible with (1): a network may learn information about when a given client uses ECH. This seems out of scope, but maybe something to keep in mind?
Re length, I dunno leave it to the client to choose? "Probably not very long." Making that in scope sounds exceedingly difficult. For instance, for a given server, you might know that tickets issued today start with 12345 (maybe the ID of today's key), but synthesized dummy tickets won't match that.
Yep, no argument there.
Discussed in 110: Will submit a PR that says "clients MAY send a dummy PSK if the inner CH uses a PSK."
LGTM

[[OPEN ISSUE: if the client sends a GREASE "encrypted_client_hello" extension, should it also send a GREASE "pre_shared_key" extension?  <ins> If not, GREASE+ticket is a trivial distinguisher.  See issue #384.]] </ins> Offering a GREASE extension is not considered offering an encrypted ClientHello for purposes of requirements in client-behavior.  In 10.1. ECH considers two types of attackers: passive and active.  Passive  <ins> attackers can read packets from the network, but they cannot perform any sort of active behavior such as probing servers or querying DNS. A middlebox that filters based on plaintext packet contents is one </ins> example of a passive attacker.  In contrast, active attackers can  <ins> also write packets into the network for malicious purposes, such as </ins> interfering with existing connections, probing servers, and querying DNS.  In short, an active attacker corresponds to the conventional threat model for TLS 1.3 RFC8446. contents of unencrypted handshake messages. Beyond these primary security and privacy goals, ECH also aims to  <ins> hide, to some extent, the fact that it is being used at all. Specifically, the GREASE ECH extension described in grease-ech does not change the security properties of the TLS handshake at all.  Its goal is to provide "cover" for the real ECH protocol (real-ech), as a means of addressing the "do not stick out" requirements of RFC8744. See dont-stick-out for details. </ins> 10.2. 10.8.4.  <ins> As a means of reducing the impact of network ossification, RFC8744 recommends SNI-protection mechanisms be designed in such a way that network operators do not differentiate connections using the mechanism from connections not using the mechanism.  To that end, ECH is designed to resemble a standard TLS handshake as much as possible. The most obvious difference is the extension itself: as long as middleboxes ignore it, as required by RFC8446, the rest of the handshake is designed to look very much as usual. The GREASE ECH protocol described in grease-ech provides a low-risk way to evaluate the deployability of ECH.  It is designed to mimic the real ECH protocol (real-ech) without changing the security properties of the handshake.  The underlying theory is that if GREASE ECH is deployable without triggering middlebox misbehavior, and real ECH looks enough like GREASE ECH, then ECH should be deployable as well.  Thus, our strategy for mitigating network ossification is to deploy GREASE ECH widely enough to disincentivize differential treatment of the real ECH protocol by the network. Ensuring that networks do not differentiate between real ECH and GREASE ECH may not be feasible for all implementations.  While most middleboxes will not treat them differently, some operators may wish to block real ECH usage but allow GREASE ECH.  This specification aims to provide a baseline security level that most deployments can achieve easily, while providing implementations enough flexibility to achieve stronger security where possible.  Minimally, real ECH is designed to be indifferentiable from GREASE ECH for passive adversaries with following capabilities: 1.  The attacker does not know the ECHConfigList used by the server.  1.  The attacker keeps per-connection state only.  In particular, it does not track endpoints across connections.  1.  ECH and GREASE ECH are designed so that the following features do not vary: the code points of extensions negotiated in the clear; the length of messages; and the values of plaintext alert messages. This leaves a variety of practical differentiators out-of-scope. including, though not limited to, the following: 1. the value of the configuration identifier; 1. the value of the outer SNI; 1. use of the "pre_shared_key" extension in the ClientHelloOuter, which is permitted in GREASE ECH but not real ECH; [[TODO: Remove this differentiator if issue #384 is resolved by a spec change.]] 1. the TLS version negotiated, which may depend on ECH acceptance; 1. client authentication, which may depend on ECH acceptance; and 1.  HRR issuance, which may depend on ECH acceptance. These can be addressed with more sophisticated implementations, but some mitigations require coordination between the client and server. These mitigations are out-of-scope for this specification. </ins> 10.8.5.


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 <del> later resume a connection to that same host after ECH deployment, thereby linking the resulting ECH connection to the original non-ECH connection. </del>  10.7.

cc NAME
(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.
Thanks!LGTM -- thanks!

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  <ins> later resume a connection to that same host after ECH deployment.  An adversary that observes this can deduce that the ECH-enabled connection was made to a host that the client previously connected to and which is within the same anonymity set. </ins> 10.7.


ClientHelloInner.  It MUST NOT be sent in either ClientHelloOuter or ClientHelloInner.  5.2. To prevent a network attacker from modifying the reconstructed

A naive implementation of EncodedClientHelloInner decoding would require a quadratic amount of work, which clients might be able to exploit to DoS the server. This adds an implementation note intended to guide servers to mitigate this problem.
cc/ NAME :)
NAME let's do that change in a separate PR for now.
A naive implementation of decoding the inner ClientHello can easily go quadratic when processing outerextensions. Thus far, nothing in TLS has required searching for O(N) extensions, only O(implemented extensions). It's likely a lot of TLS implementations handle the extensions list linearly most of the time. outerextensions is now the first time you have to search for extensions you may not have implemented. A naive implementation would then go quadratic, which is a server DoS risk. It doesn't need to go quadratic (for instance, you can sort ClientHelloOuter's extensions and then binary search per lookup), but it's probably worth at least calling this out, if not switching to a simpler decoding scheme. (For a simpler decoder, we already require CHInner group the compressed extensions consecutively. We could do the same with CHOuter, at which point we only need to reference one byte slice, be it by offset/length or some goofier scheme.)
Switching to a simpler scheme: yes please. Just dropping the compression idea entirely: even better:-) Given this optimisation is really mostly for PQ schemes, I'd argue it'd be better to just drop the current idea and have a more constrained one, e.g. using a bit mask as the value of outer_extensions, so implementation know exactly which extensions can ever be copied from the outer CH. And I'd further argue we can leave defining all of that until later too. The current scheme also imposes quite a testing burden. At least with OpenSSL most extensions have some side-effect and testing that all combinations of those are handled correctly for the inner CH isn't trivial. (That's partly down to old code being a bit awful but I guess won't be simple for any code base.) Cheers, S.
Not sure I follow the concern with side effects. Are you processing the extensions as part of decompression? In order to make the handshake work, you'd have to construct the ClientHello by bytes anyway. Thus it seems the natural implementation would be to resolve ECH early, before ClientHello processing, then compute the inner ClientHello as bytes, then run the handshake with the resulting ClientHello. This aligns nicely with the model in split mode, etc. The issue I have in mind is simply that the decoding process, a pure function from a pair of byte strings to a byte string, is easy to accidentally make inefficient.
It occurs to me this might cause some conflicts between wanting ECH-ful and ECH-less ClientHellos to look similar (don't stick out) and pressures on ClientHello extension order. The order should be irrelevant, but compat concerns have cropped up before. (If only we required extensions be sorted...) Another possibility for a simpler decode would be if outer_extensions listed indexes into the extension list, rather than extension IDs. Though we would need to answer silly questions like whether it's an index into ClientHelloOuter or ClientHelloOuterAAD.
Since this is more of an implementation detail, let's just add a warning to handle this responsibly (i.e. the sort and binary search).
Yet another way to address this is with a simple hash map, e.g.: URL My vote is to leave the encoding scheme as-is and simply leave a warning.
NAME NAME could one of you please propose text for that warning?
Another DoS risk to be careful of: implementations typically enforce size limits on TLS messages so they aren't forced to buffer 2^24 bytes. Whatever size limit is enforced on ClientHello needs to also be enforced on the inner ClientHello before allocating memory for it. In particular, if ClientHelloOuter has a single giant extension, and it is referenced multiple times in OuterExtensions, you can blow up the server's resource usage. (Though I can't think of any case without repeating an extension where the resulting size grows dramatically.)
Is there any valid reason why an extension would appear multiple times in a ClientHello? If not, maybe we should specify that the server must abort if OuterExtensions references an extension more than once.
Seems reasonable. I think the combination of "no duplicate extensions" and "no referencing the ECH extension" imply that sizeof(ClientHelloInner) < sizeof(ClientHelloOuter).
Keeping this opening to track NAME and NAME comments on CH sizes. NAME could you please send a PR if you have cycles?
Discussed in 110: Will propose text to address the second vector and then close this issue.
LGTM, though I guess we should either update it to resolve this other vector, or just make the PR not quite close the issue yet: URL (This other vector is even more fun because you can make a quadratically-sized ClientHello!)LGTM pending nits and suggestions from NAME

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


4. <del> ECH uses draft-07 of HPKE for public key encryption I-D.irtf-cfrg- </del>  hpke.  The ECH configuration is defined by the following "ECHConfig" structure. IANA is requested to create the following three entries in the existing registry for ExtensionType (defined in RFC8446): <del> encrypted_client_hello(0xfe09), with "TLS 1.3" column values set </del>  to "CH, EE", and "Recommended" column set to "Yes". ech_is_inner (0xda09), with "TLS 1.3" column values set to "CH",

and also updates the ECH version. I didn't bump the extension codepoint for "echisinner," since that seems harmless. cc NAME NAME
This new version of the draft removes the version-vector dependency, so this should be the last time we update the pinned version.
Works for me!

4.  <ins> ECH uses draft-08 of HPKE for public key encryption I-D.irtf-cfrg- </ins> hpke.  The ECH configuration is defined by the following "ECHConfig" structure. IANA is requested to create the following three entries in the existing registry for ExtensionType (defined in RFC8446):  <ins> encrypted_client_hello(0xfe0a), with "TLS 1.3" column values set </ins> to "CH, EE", and "Recommended" column set to "Yes". ech_is_inner (0xda09), with "TLS 1.3" column values set to "CH",


The client-facing server advertises a sequence of ECH configurations to clients, serialized as follows. <del> The "ECHConfigs" structure contains one or more "ECHConfig" </del>  structures in decreasing order of preference.  This allows a server to support multiple versions of ECH and multiple sets of ECH parameters. server MAY include an "encrypted_client_hello" extension in its EncryptedExtensions message with the following payload: <del> An ECHConfigs structure containing one or more ECHConfig </del>  structures, in decreasing order of preference, to be used by the client in subsequent connection attempts.  These are known as the server's "retry configurations". 6.1. <del> To offer ECH, the client first chooses a suitable ECH configuration. To determine if a given "ECHConfig" is suitable, it checks that it supports the KEM algorithm identified by "ECHConfig.contents.kem_id", at least one KDF/AEAD algorithm identified by "ECHConfig.contents.cipher_suites", and the version of ECH indicated by "ECHConfig.contents.version".  Once a suitable configuration is found, the client selects the cipher suite it will use for encryption.  It MUST NOT choose a cipher suite or version not </del>  advertised by the configuration.  If no compatible configuration is found, then the client SHOULD proceed as described in grease-ech. addition to the "encrypted_client_hello" extension, the server MUST abort with an "illegal_parameter" alert. <del> First, the server collects a set of candidate ECHConfigs.  This set is determined by one of the two following methods: </del>  <del> Compare ClientECH.config_id against identifiers of known ECHConfigs and select the ones that match, if any, as candidates. </del>  <del> Collect all known ECHConfigs as candidates, with trial decryption below determining the final selection. </del>  Some uses of ECH, such as local discovery mode, may randomize the ClientECH.config_id since it can be used as a tracking vector.  In application using (D)TLS or externally configured on both sides, implementations MUST use the first method. <del> The server then iterates over all candidate ECHConfigs, attempting to decrypt the "encrypted_client_hello" extension: </del>  The server verifies that the ECHConfig supports the cipher suite indicated by the ClientECH.cipher_suite and that the version of ECH If decryption fails, the server continues to the next candidate ECHConfig.  Otherwise, the server reconstructs ClientHelloInner from EncodedClientHelloInner, as described in encoding-inner.  It then <del> stops considering candidate ECHConfigs. </del>  Upon determining the ClientHelloInner, the client-facing server then forwards the ClientHelloInner to the appropriate backend server, 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 the connection using ClientHelloOuter.  This connection proceeds as usual, except the server MUST include the "encrypted_client_hello" extension in its EncryptedExtensions with the "retry_configs" field set to one or more ECHConfig structures with up-to-date keys. Servers MAY supply multiple ECHConfig values of different versions. This allows a server to support multiple versions at once. </del>  Note that decryption failure could indicate a GREASE ECH extension (see grease-ech), so it is necessary for servers to proceed with the not possible.  See plaintext-dns for more details regarding plaintext DNS. <del> Authenticating the ECHConfigs structure naturally authenticates the </del>  included public name.  This also authenticates any retry signals from the client-facing server because the client validates the server certificate against the public name before retrying.

See .
I left this out because I figure there may be cases where there is no "published" ECHConfigList. (Maybe the config is baked in the app?)
Ah fair, that might be too precise. I think it still makes sense for that case to have the baked-in thing be an ECHConfigList than an ECHConfig, just for consistency. (I'm envisioning that ECHConfigList is the "public API" for client ECH and your TLS library might just that one byte string as input.) How about s/published/server's/?
That'd work :)
NAME done!
LGTMThanks, Chris!

The client-facing server advertises a sequence of ECH configurations to clients, serialized as follows.  <ins> The "ECHConfigList" structure contains one or more "ECHConfig" </ins> structures in decreasing order of preference.  This allows a server to support multiple versions of ECH and multiple sets of ECH parameters. server MAY include an "encrypted_client_hello" extension in its EncryptedExtensions message with the following payload:  <ins> An ECHConfigList structure containing one or more ECHConfig </ins> structures, in decreasing order of preference, to be used by the client in subsequent connection attempts.  These are known as the server's "retry configurations". 6.1.  <ins> To offer ECH, the client first chooses a suitable ECHConfig from the server's ECHConfigList.  To determine if a given "ECHConfig" is suitable, it checks that it supports the KEM algorithm identified by "ECHConfig.contents.kem_id", at least one KDF/AEAD algorithm identified by "ECHConfig.contents.cipher_suites", and the version of ECH indicated by "ECHConfig.contents.version".  Once a suitable configuration is found, the client selects the cipher suite it will use for encryption.  It MUST NOT choose a cipher suite or version not </ins> advertised by the configuration.  If no compatible configuration is found, then the client SHOULD proceed as described in grease-ech. addition to the "encrypted_client_hello" extension, the server MUST abort with an "illegal_parameter" alert.  <ins> First, the server collects a set of candidate ECHConfig values.  This list is determined by one of the two following methods: </ins>  <ins> Compare ClientECH.config_id against identifiers of each known ECHConfig and select the ones that match, if any, as candidates. </ins>  <ins> Collect all known ECHConfig values as candidates, with trial decryption below determining the final selection. </ins> Some uses of ECH, such as local discovery mode, may randomize the ClientECH.config_id since it can be used as a tracking vector.  In application using (D)TLS or externally configured on both sides, implementations MUST use the first method.  <ins> The server then iterates over the candidate ECHConfig values, attempting to decrypt the "encrypted_client_hello" extension: </ins> The server verifies that the ECHConfig supports the cipher suite indicated by the ClientECH.cipher_suite and that the version of ECH If decryption fails, the server continues to the next candidate ECHConfig.  Otherwise, the server reconstructs ClientHelloInner from EncodedClientHelloInner, as described in encoding-inner.  It then  <ins> stops iterating over the candidate ECHConfig values. </ins> Upon determining the ClientHelloInner, the client-facing server then forwards the ClientHelloInner to the appropriate backend server, client-facing server forwards all other TLS messages between the client and backend server unmodified.  <ins> Otherwise, if all candidate ECHConfig values fail to decrypt the extension, the client-facing server MUST ignore the extension and proceed with the connection using ClientHelloOuter.  This connection proceeds as usual, except the server MUST include the "encrypted_client_hello" extension in its EncryptedExtensions with the "retry_configs" field set to one or more ECHConfig structures with up-to-date keys.  Servers MAY supply multiple ECHConfig values of different versions.  This allows a server to support multiple versions at once. </ins> Note that decryption failure could indicate a GREASE ECH extension (see grease-ech), so it is necessary for servers to proceed with the not possible.  See plaintext-dns for more details regarding plaintext DNS.  <ins> Authenticating the ECHConfig structure naturally authenticates the </ins> included public name.  This also authenticates any retry signals from the client-facing server because the client validates the server certificate against the public name before retrying.


A "HpkeKeyConfig" structure carrying the configuration information associated with the HPKE public key.  Note that this structure contains the "config_id" field, which applies to the entire <del> ECHConfigContents.  Sites MUST NOT publish two differet"ECHConfigContents" values with the same "HpkeKeyConfig" value. </del>  The longest name of a backend server, if known.  If this value is not known it can be set to zero, in which case clients SHOULD use The "HpkeKeyConfig" structure contains the following fields: <del> An opaque identifier for the given HPKE key configuration.  This </del>  is used by clients to indicate the key used for ClientHello encryption. "payload", as computed above. If optional configuration identifiers (see optional-configs)) are <del> used, "config_id" SHOULD be set to a randomly generated eight bytes. Unless specified by the application using (D)TLS or externally configured on both sides, implementations MUST compute the field as specified in encrypted-client-hello. </del>  6.1.1. The client then modifies the "encrypted_client_hello" extension in ClientHelloOuter as follows: <del> "config_id" is replaced with the empty string. </del>  "cipher_suite" is unchanged and contains the client's chosen HPKE cipher suite. RFC8701 "encrypted_client_hello" extension in the first ClientHello as follows: <del> Set the "config_id" field to a random eight byte string. </del>  Set the "cipher_suite" field to a supported HpkeSymmetricCipherSuite.  The selection SHOULD vary to exercise ClientHelloOuter also contains the "encrypted_client_hello" extension.  If not, it MUST abort the handshake with a "missing_extension" alert.  Otherwise, it checks that <del> 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. </del>  Finally, it decrypts the new ClientECH.payload as a second message with the previous HPKE context:

accidentally targeted an older branch that was since merged into master. This just takes the same change and redirects it to the main branch.

A "HpkeKeyConfig" structure carrying the configuration information associated with the HPKE public key.  Note that this structure contains the "config_id" field, which applies to the entire  <ins> ECHConfigContents.  Sites MUST NOT publish two different "ECHConfigContents" values with the same "HpkeKeyConfig" value. The RECOMMENDED technique for choosing "config_id" is to choose a random byte.  This process is repeated if this config_id matches that of any valid ECHConfig, which could include any ECHConfig that has been recently removed from active use. </ins> The longest name of a backend server, if known.  If this value is not known it can be set to zero, in which case clients SHOULD use The "HpkeKeyConfig" structure contains the following fields:  <ins> A one-byte identifier for the given HPKE key configuration.  This </ins> is used by clients to indicate the key used for ClientHello encryption. "payload", as computed above. If optional configuration identifiers (see optional-configs)) are  <ins> used, "config_id" SHOULD be set to a randomly generated byte.  Unless specified by the application using (D)TLS or externally configured on both sides, implementations MUST set the field as specified in encrypted-client-hello. </ins> 6.1.1. The client then modifies the "encrypted_client_hello" extension in ClientHelloOuter as follows:  <ins> "config_id" is unchanged and contains the "config_id" corresponding to the client's chosen ECHConfig. </ins> "cipher_suite" is unchanged and contains the client's chosen HPKE cipher suite. RFC8701 "encrypted_client_hello" extension in the first ClientHello as follows:  <ins> Set the "config_id" field to a random byte. </ins> Set the "cipher_suite" field to a supported HpkeSymmetricCipherSuite.  The selection SHOULD vary to exercise ClientHelloOuter also contains the "encrypted_client_hello" extension.  If not, it MUST abort the handshake with a "missing_extension" alert.  Otherwise, it checks that  <ins> ClientECH.cipher_suite and ClientECH.config_id are unchanged, and that ClientECH.enc is empty.  If not, it MUST abort the handshake with an "illegal_parameter" alert. </ins> Finally, it decrypts the new ClientECH.payload as a second message with the previous HPKE context:


"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 <del> ClientHelloOuter with the "encrypted_client_hello" extension removed. Note this does not include the four-byte header included in the Handshake structure. </del>  <del> Note the decompression process in encoding-inner forbids </del>  "encrypted_client_hello" in OuterExtensions.  This ensures the unauthenticated portion of ClientHelloOuter is not incorporated into ClientHelloInner.

Having looked at this, I have a preference for a construction where the outer ClientHello has a placeholder with empty extension data, rather than a placeholder that has an empty payload field. Zeroing out the payload field is not easier in our implementation. It is somewhat more difficult to implement. This is a preference only; anything can be implemented. But this version is preferable.
The position of the ECH extension is not authenticated as part of ClientHelloOuterAAD. This means that it is possible to construct multiple different ClientHello messages that will all be accepted. This probably isn't that interesting, but it might be worth noting.
If we want to fix this (I don't care either way), we can remove the fields prepended to ClientHelloOuterAAD, leave the ECH extension in, and say the AAD is computed by replacing ClientECH.payload with the empty string.
That seems like a reasonable proposal. NAME what do you think? (NAME might also have thoughts on this.)
I can live with either a placeholder or text in the draft that explains the limitation. Extension order doesn't carry any semantics, so I don't see malleability as a real risk. I'd lean toward the placeholder, but only slightly. More so if we were planning to change the AAD anyway.
This seems like a good idea to me.
If we don't really care about this issue, why specify it? Does this add complexity or reduce it?
We don't know of any way this can be a problem, but that doesn't mean there is none. Plus, malleability is generally not great. I don't think this moves the complexity needle in a meaningful way.
I guess it exposes an active distinguishing attack, though I suspect this style of attack has plenty of vectors: Attacker rewrites CH to move the ECH extension to another spot and forwards to server Server responds with SH..Finished. Attacker forwards that to client unmodified If this was negotiating CHOuter, this would break the transcript and the client would reject. If this was negotiating CHInner, the client won't notice and proceed
Yep, nice! Let's definitely make this change, then.

"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  <ins> ClientHelloOuter with the "encrypted_client_hello" extension set to the empty string, i.e., the "extension_data" list has zero length. Note this serialization does not include the four-byte header included in the Handshake structure. </ins>  <ins> The decompression process in encoding-inner forbids </ins> "encrypted_client_hello" in OuterExtensions.  This ensures the unauthenticated portion of ClientHelloOuter is not incorporated into ClientHelloInner.


stops iterating over the candidate ECHConfig values. Upon determining the ClientHelloInner, the client-facing server then <del> 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, 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. </del>  Otherwise, if all candidate ECHConfig values fail to decrypt the extension, the client-facing server MUST ignore the extension and

We currently have a number of invariants on the ClientHelloInner that the client must follow: ClientHelloInner must include echisinner ClientHelloInner may not offer TLS 1.2 and below ClientHelloInner may not include encryptedclienthello (somewhat implicit from the first rule and section 7) But, syntactically, the client could violate this. How is the server expected to respond? In particular, a shared mode server directly observes all invariants, and it seems a little weird to ask it to send something it knows is inconsistent. But if we say to enforce, we either have a shared/split difference, or we put all the enforcement at the client-facing server (not wrong...) because the backend server doesn't have enough information to check these. If ClientHelloInner lacks echisinner, should the server just accept ECH without reporting the confirmation signal, or should it report an error? If we end up negotiating TLS 1.2 with ClientHelloInner, is that an error or do we just proceed, without reporting the confirmation signal and sending a cleartext certificate? If ClientHelloInner contains another encryptedclienthello extension, one possible implementation strategy would report retry keys, since backend server halves that see encryptedclienthello typically assume the extension wasn't decrypted. Is that correct, or do you omit retry keys? NAME
These are just bugs in the client, so I have no qualms with requiring the server to reject the connection when it can. A split mode server might not see all of this. If the fronting server fails to validate the inner CH, all of this might slip through. Especially when the backend isn't able to require ECH on every connection, which seems plausible. So I think we're OK with hard rejection. Failure here in shared mode requires client bugs and the shared mode server can report those. Failure in split mode might not always be detected, but it requires the same client bugs and a lack of diligence on the part of the fronting server (for this to be detectable, echisinner is all they need to check).

stops iterating over the candidate ECHConfig values. Upon determining the ClientHelloInner, the client-facing server then  <ins> checks that the message includes the "ech_is_inner" extension, omits the "encrypted_client_hello" extension, and does not offer TLS 1.2 or below versions.  If any of these checks fails, the client-facing server MUST abort with an "illegal_parameter" alert. If these checks succeed, the client-facing server then 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, 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 ECHConfig values fail to decrypt the extension, the client-facing server MUST ignore the extension and


The value of "ECHConfig.contents.public_name" MUST be placed in the "server_name" extension. <del> It MUST NOT include the "pre_shared_key" extension.  (See flow- clienthello-malleability.) </del>  [[OPEN ISSUE: We currently require HRR-sensitive parameters to match in ClientHelloInner and ClientHelloOuter in order to simplify client- 6.1.2.  This section describes a deterministic padding mechanism based on the following observation: individual extensions can reveal sensitive information through their length.  Thus, each extension in the inner EncryptedExtension, so that handshake message also needs to be padded using TLS record layer padding. <del> 6.1.3. </del>  As described in server-behavior, the server MAY either accept ECH and use ClientHelloInner or reject it and use ClientHelloOuter.  In of "accept_confirmation" as defined in backend-server.  Otherwise, it presumes rejection. <del> 6.1.3.1. </del>  If the server used ClientHelloInner, the client proceeds with the connection as usual, authenticating the connection for the true server name. <del> 6.1.3.2. </del>  If the server used ClientHelloOuter, the client proceeds with the handshake, authenticating for ECHConfig.contents.public_name as "encrypted_client_hello" extension.  If the client does not retry in either scenario, it MUST report an error to the calling application. <del> 6.1.3.3. </del>  When the server rejects ECH or otherwise ignores "encrypted_client_hello" extension, it continues with the handshake implemented, for instance, by reporting a failed connection with a dedicated error code. <del> 6.1.4. </del>  As required in real-ech, clients offering ECH MUST ensure that all extensions or parameters that might change in response to receiving a

This may also be useful for . NAME do let me know if this PR overloads the term "GREASE" in a way that you find to be inaccurate or not in the GREASE spirit :) I'm perfectly happy to call it something else.
I don't know the answer to this question, but, from the TLS 1.3 RFC: I noticed this while testing with a pre-baked ClientHello that had both extensions. The ECH draft clearly required that the extension be removed, but I wondered about .
Given URL, URL, and URL, I'd say quite the opposite.
Ah, I see text has been added saying it must appear in the ClientHelloOuter. Thanks for the references. I was working from draft-10.
Here's a fun case. Suppose the client talks to a 0-RTT-capable, ECH-capable server. It'll send earlydata + presharedkey in ClientHelloInner, wrap that in some ClientHelloOuter, and follow it up with early data records. Now the server rejects ECH and handshakes with the ClientHelloOuter. But the client has already sent early data, so the server needs to know to skip past it. I believe this means we need to require the client mirror ClientHelloInner.earlydata in ClientHelloOuter, even though it's not actually offering to resume anything. Sending earlydata without presharedkey is slightly weird (if tolerated at all), so we might even want to recommend a fake outer presharedkey. We almost_ don't care about this: the client may as well stop the handshake at server Finished and skip the client Finished flight anyway, for purposes of the recovery flow. However: What's not what the draft currently says If the server rejects ECH, handshakes with ClientHelloOuter, and sends HelloRetryRequest, we do care about the client's second flight getting through. That second case is extra fun because the client-facing server must look for the second ClientHello too.
Wasn't this already the outcome from ? (That is, if the inner CH has a PSK, then the outer one ought have a dummy PSK, too.) As for mirroring earlydata, why can the server not just ignore the application data (as if it received earlydata and decided to ignore it)? Taking a quick look back at RFC8446, the server behavior where clients send application data without early_data is not defined, so are you concerned about that, or something else?
Well, I think we only got as far as MAY in , but yeah I think this is another reason. Regarding ignoring application data, our server implementation only skips early data if there was an early_data extension that we rejected. I believe that matches RFC8446 since it doesn't say to skip application data in general, just that case. And, yeah, that's the concern.
Since this is behavior for the client-facing server, which supports ECH, can't we specify the desired behavior in this spec? That is, we might say that a client-facing server which rejects ECH MUST ignore any early data sent by the server, or something. Would that work?
We could, but then it'd break the nice property that a plain RFC8446 server will handshake ClientHelloOuter without any fuss. This property is great for rollback safety. (Otherwise any ECH server deployment needs to be two-stage, and introduces a lower bound that you cannot rollback beyond.) And elsewhere we've tried to avoid contradicting RFC8446. Yet another option would be to tell the client to retry on certain kinds of errors if it offered 0-RTT in ClientHelloInner, but that's a bit more fuss since retries are often outside the TLS stack. (Besides, the fact that you offer 0-RTT in ClientHelloInner is all but public anyway. While we don't have a cleartext 0-RTT marker[], it's pretty obvious from timing that some post-ClientHello record came before ServerHello.) [] In TCP... I want to say QUIC does, but I could be misremembering.
True! This is fun. :-)
I'm not sure why ignoring stuff is fuss. What's the alternative? To terminate the handshake if the server receives early data? Wouldn't that be more fuss? If RFC8446 doesn't specify what to do in this situation, I'm not sure why specifying it is in contradiction to RFC8446 (as opposed to expanding on it).
I think RFC8446 does say what to do in this situation. Section 5.2 says at the bottom: URL The default for the record layer is that decryption errors are fatal (as it should be). Early data skipping is an exception to this described in section 4.2.10, and the exception only applies when the server declines an earlydata extension. Oh no, the goal is to ignore stuff. That's what makes the recovery flow work. The issue is that, in RFC8446, servers only ignore the early data if there was an earlydata extension in ClientHello which they declined. Otherwise, it's just a fatal decryption error. In order to meet that condition, ClientHelloOuter needs the early_data extension whenever ClientHelloInner does.
ACK -- thanks for closing the loop!
As discussed in , there are two real-ECH/grease-ECH differentiators related to the PSK extension we may wish to address. Real ECH disallows the PSK extension in ClientHelloOuter, whereas grease ECH permits PSK in ClientHello. Real ECH allows PSK in ClientHelloInner. In case of acceptance, the backend server may respond with PSK in ServerHello. The response would appear to be unsolicited. something that would not happen in a grease-ECH or vanilla handshake. The question we will ask ourselves here is if/how to mitigate these differentiators. We might simply say that clients MAY (or SHOULD?) send a dummy PSK in the ClientHello(Outer). This ought to provide sufficient cover.
I don't mind a MAY on sending a dummy PSK in case (2), though I don't think it's a good idea for case (1). The case that really matters here is when the outer and inner ClientHello both lack PSKs: Suppose the buggy network is sensitive to (1) and only allows ECH extensions when the outer ClientHello has a PSK because it knows those can't be ECH. That network has effectively broken all connections because the only way for the client to get a PSK is to have connected to the server without a PSK at some point. Suppose instead the buggy network is sensitive to (2) and breaks if the inner ClientHello has a PSK but the outer does not. The connect will fail, but between single-use tickets and bounded client session caches, the client will quickly exhaust its tickets and get out of case (2). This is a performance issue, but the connections still work. (Though, yeah, adding the option to send a dummy one might help there. shrug)
I'm not following the following point. Could you elaborate? In any case, the case to mitigate (2) seems more compelling than the case to mitigate (1), since (2) would look like non-compliance to an ECH-ignorant network. Would be curious to see what others think.
So I think the benefit of GREASE-like strategies is that broader breakages now are more noticeable than focused breakage later. More noticeable breakage is more likely to be fixed, and less likely to make it to the ecosystem in the first place. This applies to ECH GREASE by saying we want all ClientHellos to look like ECH was deployed, so the network is less likely to develop intolerances. Differences between GREASE ECH and actual ECH thus matter because the network be intolerant to something that we failed to simulate. (1) says that, in actual ECH, all outer ClientHellos have no PSK. In GREASE ECH, some outer ClientHellos have a PSK while some do not. If we were to translate that to actual intolerance, it would mean the network rejects outer ClientHellos without a PSK. But that would break everything, ECH or non-ECH, anyway because clients don't send PSKs the first time[] they connect to a server. Clients only have PSKs to send the second time, and you can't connect a second time if you didn't connect a first time. Thus, even though (1) is a difference, it doesn't seem that difference prevents us from noticing intolerance. Agreed that mitigating (2) is more compelling than mitigating (1). [] First time overall, first time after a restart (if sessions aren't persisted), or first time after a long break (sessions expired).
I agree that (2) is more important. And I'm fine with a "MAY/SHOULD send an outer dummy PSK if you are using ECH and have an inner PSK." NAME if we were to append on a dummy PSK, what do you think would be a suitable length? (Bikeshedding over that should be fun.) As an aside, I wanted to drill in on this point: This seems true, if we're assuming the the intolerance is "break the connection," which I think is the right interpretation. But it occurs to me that there's a slightly weaker form of misbehavior thats's possible with (1): a network may learn information about when a given client uses ECH. This seems out of scope, but maybe something to keep in mind?
Re length, I dunno leave it to the client to choose? "Probably not very long." Making that in scope sounds exceedingly difficult. For instance, for a given server, you might know that tickets issued today start with 12345 (maybe the ID of today's key), but synthesized dummy tickets won't match that.
Yep, no argument there.
Discussed in 110: Will submit a PR that says "clients MAY send a dummy PSK if the inner CH uses a PSK."

The value of "ECHConfig.contents.public_name" MUST be placed in the "server_name" extension.  <ins> When the client offers the "pre_shared_key" extension in ClientHelloInner, it SHOULD also include a GREASE "pre_shared_key" extension in ClientHelloOuter, generated in the manner described in grease-psk.  The client MUST NOT use this extension to advertise a PSK to the client-facing server.  (See flow- clienthello-malleability.)  When the client includes a GREASE "pre_shared_key" extension, it MUST also copy the "psk_key_exchange_modes" from the ClientHelloInner into the ClientHelloOuter. </ins> [[OPEN ISSUE: We currently require HRR-sensitive parameters to match in ClientHelloInner and ClientHelloOuter in order to simplify client- 6.1.2. <ins> When offering ECH, the client is not permitted to advertise PSK identities in the ClientHelloOuter.  However, the client can send a "pre_shared_key" extension in the ClientHelloInner.  In this case, when resuming a session with the client, the backend server sends a "pre_shared_key" extension in its ServerHello.  This would appear to a network observer as if the the server were sending this extension without solicitation, which would violate the extension rules described in RFC8446.  Sending a GREASE "pre_shared_key" extension in the ClientHelloOuter makes it appear to the network as if the extension were negotiated properly. The client generates the extension payload by constructing an "OfferedPsks" structure (see RFC8446, Section 4.2.11) as follows. For each PSK identity advertised in the ClientHelloInner, the client generates a random PSK identity with the same length.  It also generates a random, 32-bit, unsigned integer to use as the "obfuscated_ticket_age".  Likewise, for each inner PSK binder, the client generates random string of the same length. If the server replies with a "pre_shared_key" extension in its SeverHello, then the client MUST abort the handshake with an "illegal_parameter" alert. 6.1.3. </ins> This section describes a deterministic padding mechanism based on the following observation: individual extensions can reveal sensitive information through their length.  Thus, each extension in the inner EncryptedExtension, so that handshake message also needs to be padded using TLS record layer padding.  <ins> 6.1.4. </ins> As described in server-behavior, the server MAY either accept ECH and use ClientHelloInner or reject it and use ClientHelloOuter.  In of "accept_confirmation" as defined in backend-server.  Otherwise, it presumes rejection.  <ins> 6.1.4.1. </ins> If the server used ClientHelloInner, the client proceeds with the connection as usual, authenticating the connection for the true server name.  <ins> 6.1.4.2. </ins> If the server used ClientHelloOuter, the client proceeds with the handshake, authenticating for ECHConfig.contents.public_name as "encrypted_client_hello" extension.  If the client does not retry in either scenario, it MUST report an error to the calling application.  <ins> 6.1.4.3. </ins> When the server rejects ECH or otherwise ignores "encrypted_client_hello" extension, it continues with the handshake implemented, for instance, by reporting a failed connection with a dedicated error code.  <ins> 6.1.5. </ins> As required in real-ech, clients offering ECH MUST ensure that all extensions or parameters that might change in response to receiving a


not decrypt the second ClientHello's ClientECH.payload value, if there is one. <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.  Stateful HRR on the client- facing server works fine, however.  See issue https://github.com/tlswg/draft-ietf-tls-esni/issues/333.]] </del>  7.2.

URL don't give both servers a cookie. That is, if a client-facing server forwards the ClientHello, it can't do so statelessly without coordinating with the backend. This is a consequence of moving to a single HelloRetryRequest, but it's a minor one. In most cases, there will be state established anyway just to get the connection forwarded. DTLS might not work that way, but it's still likely reasonable to assume state. No longer builds on .
I guess the dust is still settling on the rest of the HRR mess, but a6b6b67ca710d023962b8b950a2d26f2806d7aac LGTM.
+1 -- NAME can we cherry-pick a6b6b67 into a separate PR?
I would be happy for you to take that and merge it.
Or, I will just rebase -i here.
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

not decrypt the second ClientHello's ClientECH.payload value, if there is one.  <ins> Note that a client-facing server that forwards the first ClientHello cannot include its own "cookie" extension if the backend server sends a HelloRetryRequest.  This means that the client-facing server either needs to maintain state for such a connection or it needs to coordinate with the backend server to include any information it requires to process the second ClientHello. </ins> 7.2.


SNI Encryption keys can be published in the DNS using the ESNIKeys structure, defined below. <del> An opaque label used to identify the list of keys. </del> First four (4) octets of the SHA-256 message digest RFC6234 of the ESNIKeys structure starting from the first octet of "keys" to the end of the stucture. The encrypted SNI is carried in an "encrypted_server_name" extension, which contains an EncryptedSNI structure: <del> The label associated with the SNI encryption key. A cryptographic hash of the ESNIKeys structure from which the label and ESNI key was obtained, i.e., from "checksum" to the end of the structure.  This hash is computed using the hash function </del>  associated with "suite". The cipher suite used to encrypt the SNI. If it is unable to negotiate TLS 1.3 or greater, it MUST abort the connection with a "handshake_failure" alert. <del> If the EncryptedSNI.label value does not correspond to any known SNI encryption key, it MUST abort the connection with an "illegal_parameter" alert.  [[OPEN ISSUE: We looked at ignoring the extension but concluded this was better.]] </del> If the EncryptedSNI.record_digest value does not match the <del> cryptographic hash of the associated ENSIKeys structure, it MUST abort the connection with an "illegal_parameter" alert.  This is necessary to prevent downgrade attacks. </del>  If more than one KeyShareEntry has been provided, or if that share's group does not match that for the SNI encryption key, it key is provided, sharing is optimally bound by the number of hosts that share an IP address.  Server operators may further limit sharing by sending different Resource Records containing ESNIKeys with <del> different keys covered by different labels using a short TTL. </del>  7.2.3. This design requires servers to decrypt ClientHello messages with <del> EncryptedSNI extensions carrying valid labels.  Thus, it is possible </del>  for an attacker to force decryption operations on the server.  This attack is bound by the number of valid TCP connections an attacker can open. 7.2.4. <del> By sending SNI and ESNI values (with illegitimate labels), or by </del>  sending legitimate ESNI values for and "fake" SNI values, clients do not display clear signals of ESNI intent to passive eavesdroppers. As more clients enable ESNI support, e.g., as normal part of Web

Now that has been merged, label seems to be an unnecessary complication. label has no actual use on the client because labels cannot collide, the key generator needs to either keep some state across all invocations (you need a trustable store), or generate a UUID (it's just a random junk) to be used as a label a TLS server is required to at first compare the label and then check the digest. These three issues go away if we eliminate label. Key generators do not need to care about uniqueness. TLS servers can just look for the matching digest and that's it. Note that a server operator can still associate an identifier to the ESNIKeys structure for internal use. The private key will be kept in relation to the structure anyways. So having a private identifier does not complicate things at all.
After very useful discussion with many folks, it seems now is the time to remove config_id. There are a couple reasons to do this: Servers need to know the correct hash function to use when computing the ID, and possibly need to compute multiple IDs for a given public key (since a config can have multiple hash-based KDFs), which is somewhat annoying and redundant. The entire ECHConfig is already authenticated, so using a hash to prove that the client made a determination based on the correct config contents is unnecessary. Servers will likely only ever have a small handful of keys in motion at any point in time, so anything more than a couple of bits to identify one of said keys is excessive. ... there are probably more. The original design . (Oops!) It seems we're now in a better position to bring this back, but with some simplifications.
Hiya, Can you send a pointer to those discussions? (Sorry, I seem to have missed 'em;-) Ta, S.
Sorry -- these were informal conversations off-list.
Ah. That's a recurring problem. And even this exchange is actually off-list. Can we please fix that? I think I've whined about that a few times now, and would hope that continual whining is not a requirement;-) S.
few times now, and would hope that continual whining is not a requirement;-) Fix what? A few of us had a private conversation about some ideas and came to the conclusion that it would be better to replace the hash with a key_id. Then Chris and Martin brought it to a Github issue. This is precisely in line with the WG consensus to use Github issue discussion mode, as documented in 8874 S 5.3.
I'm guessing that was a private discussion amongst what could well be described as the usual suspects or an old-boys club (or are a few of you "many folks?"). Correct me if I'm wrong but that had zero visibility and zero traceability and zero accountability for anyone who isn't one of those old boys. And yet we're zapping a field in a PDU from what was declared as an "implementation draft" a day or so after one major deployment declared they had an interop target. To me, that smells of disrespect for outsiders tbh. You can probably guess what I think about that state of affairs:-) I don't buy that tbh. Chris' mail was presented as a conclusion, from a wg-chair following a private discussion that for sure excluded some implementers. (Me being one, albeit a not very good one:-) And you yourself are an ex WG-chair so I'd guess you ought also care about exclusivity here. What gain is there from that kind of behaviour? I don't see it myself. IMO, there is no need nor benefit from being so exclusive wrt this spec, yet that is what is happening and has been for a few years. I'd encourage those of you who may be "old-boys" to consider whether or not that is really the best way to proceed. IMO - it is not, you lose way more than you might think you gain, almost all the time. Cheers, S. PS: I don't disagree with the outcome, though have opinions on what -10 might contain. I do very much dislike the awful process, which is my point here.
NAME I would completely agree with you had the proposed PR been merged and the associated issue been closed. That has not happened. Further, it appears that you might be reading a bit more into Chris' issue summary than is really there. He is not acting in his WG chair capacity here because he is an author; I know he knows that and I really hope you can give him the benefit of the doubt here so that we do not have to suffer the "no WG chair hat on" in every message/post. The process, as I see it, is pretty darn near normal. Somebody files an issue and optionally (but preferably) files a PR to address said issue. Discussion ensues amd if substantive it's taken to the list. The PR may or may not be modified based on that discussion. If consensus is reached, the PR is merged and the issue is closed. We are at the discussion point. Can we do that now?
Perhaps it'd be better to not start a mail with "After very useful discussion with many folks, it seems now is the time to remove config_id." in cases where that discussion was purely private? I can understand it being hard to keep track with the various tools, but for me, that still reads as a decision being announced even if that was not the intent. Sorry for being over-sensitive to this but IMO there is a history and ongoing tendency with this spec for a subset of people to act as if they and not the IETF/WG have change control. I fully accept that they are not deliberately aiming to avoid having to garner WG consensus but consider that ongoing instances of carelessness such as the above demonstrate that there is an insider/outsider thing going on here. S.
The text in section 4, Publishing the SNI Encryption Key, specifies that if a client queries URL, the ESNI TXT name is URL That's fine, but suppose that the client queries "URL". The client needs to learn that "URL" is fronted by "URL" -- per PR , the client will do that by obtaining an ALT-SVC record. Should the client get the ESNI key from "URL", or from "URL" ?
NAME I think this is you.
covers this. Mostly. It might pay to observe that the client does not look up and DNS records for the fronting service, it is just the case that all servers in the anonymity set point to the same IP address. I made similar comments on .
I agree with NAME here. I didn't see the client querying URL
That means all servers that share the same IP need to publish the same key. Are we confident that there is no synch issue during key rollover?
Servers can publish multiple ESNI keys with different labels in a record. That way they can retire one key and start using another as needed. Though it might be good (necessary?) to require clients to use one of those keys.
While that is possible, I am not sure if it would be the approach that would generally be used. The issue is identical to the encryption key of the session tickets. In case of session tickets, we encrypt the ticket using one key, and send the identifier of the key along with the encrypted ticket. When we rollover the keys, we first deploy a new key to the server as well as keeping the old one alive for decryption only. And we retire the old key after certain amount of period that we become sure that it is no longer used. I'd assume that we can and will use the same approach for ESNIKeys.
PS. therefore, it is possible to have just one label for the set of keys rather than having labels for each of the keys (assuming that we can remove the "choose one at random" rule). In that approach, a server will identify the key being used by looking at the tuple. The benefit will be that you will save the space of N-1 labels assuming that you are sending N key shares. Identification of the key on the server-side might seem complicated, but I actually think that it simplifies things, because a server operator is anyways expected to deploy the entire set of keys found in ESNIKeys at the same moment.
To me it seems that this is necessary to prevent downgrade attack. Hence .
How do we prevent labels from becoming identifiers?
Labels as identifiers seem problematic in both approaches -- current and .
Would you mind elaborating why label becomes an identifier? We state in the document that the probability of identifying the hidden server is 1/N where N is the number of the hosts belonging to the same set of hidden servers. A label represents that entire set. So I do not think it is an issue.
Now that is merged, which removes the label altogether, and we've added text describing the anonymity set for hidden servers, I think this issue is covered. NAME do you agree?
The label issue is definitely covered.
LGMT -- I much prefer content-based identifiers to labels.

SNI Encryption keys can be published in the DNS using the ESNIKeys structure, defined below.  First four (4) octets of the SHA-256 message digest RFC6234 of the ESNIKeys structure starting from the first octet of "keys" to the end of the stucture. The encrypted SNI is carried in an "encrypted_server_name" extension, which contains an EncryptedSNI structure:  <ins> A cryptographic hash of the ESNIKeys structure from which the ESNI key was obtained, i.e., from "checksum" to the end of the structure.  This hash is computed using the hash function </ins> associated with "suite". The cipher suite used to encrypt the SNI. If it is unable to negotiate TLS 1.3 or greater, it MUST abort the connection with a "handshake_failure" alert.  If the EncryptedSNI.record_digest value does not match the  <ins> cryptographic hash of any known ENSIKeys structure, it MUST abort the connection with an "illegal_parameter" alert.  This is necessary to prevent downgrade attacks.  [[OPEN ISSUE: We looked at ignoring the extension but concluded this was better.]] </ins> If more than one KeyShareEntry has been provided, or if that share's group does not match that for the SNI encryption key, it key is provided, sharing is optimally bound by the number of hosts that share an IP address.  Server operators may further limit sharing by sending different Resource Records containing ESNIKeys with  <ins> different keys using a short TTL. </ins> 7.2.3. This design requires servers to decrypt ClientHello messages with  <ins> EncryptedSNI extensions carrying valid digests.  Thus, it is possible </ins> for an attacker to force decryption operations on the server.  This attack is bound by the number of valid TCP connections an attacker can open. 7.2.4.  <ins> By sending SNI and ESNI values (with illegitimate digests), or by </ins> sending legitimate ESNI values for and "fake" SNI values, clients do not display clear signals of ESNI intent to passive eavesdroppers. As more clients enable ESNI support, e.g., as normal part of Web


last parameter, "outer_hello", is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension set to the empty string, i.e., the "extension_data" list has zero length. <del> Note this serialization does not include the four-byte header included in the Handshake structure. </del>  The decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the

ClientHello is already a defined structure. No need to wrap it in an opaque byte array. This avoids needing to fill in an extra length prefix in front.
Isn't the length prefix there in order to prevent ossification?
Could you elaborate? I don't see how this relates to ossification. Note AAD values aren't parsed or even sent over the wire. Rather, the requirement on them is injectivity.
If the thinking is one might want to change what goes in the AAD later, the length prefix doesn't do anything. You'd need an actual distinguishing field like a type or version. We get that via labels and such when setting up the ECH context itself. By the time we've settled on speaking a particular ECH-like protocol, the AAD structure is fixed.
Thanks for the clarification. I was confused about the specifics of how this prefix was used.
Can probably just say instead of leading with , which is almost always unnecessary.

last parameter, "outer_hello", is computed by serializing ClientHelloOuter with the "encrypted_client_hello" extension set to the empty string, i.e., the "extension_data" list has zero length.  <ins> This serialization uses the ClientHello structure from Section 4.1.2 of RFC8446, which does not include the four-byte header included in the Handshake structure. </ins> The decompression process in encoding-inner forbids "encrypted_client_hello" in OuterExtensions.  This ensures the