with a version they do not understand.  [[NOTE: This means that the RFC will presumably have a nonzero value.]]  The non-empty name of the entity trusted to update these encryption keys.  This is used to repair misconfigurations, as described in handle-server-response. Zero padding whose length makes the serialized PaddedServerNameList struct have a length equal to <del> ESNIConfig.padded_length. </del>  This value consists of the serialized ServerNameList from the "server_name" extension, padded with enough zeroes to make the total <del> structure ESNIConfig.padded_length bytes long.  The purpose of the padding is to prevent attackers from using the length of the </del>  "encrypted_server_name" extension to determine the true SNI.  If the <del> serialized ServerNameList is longer than ESNIConfig.padded_length, the client MUST NOT use the "encrypted_server_name" extension. </del>  The ClientEncryptedSNI.encrypted_sni value is then computed using AEAD-Encrypt (RFC5116; Section 2.1) with the AEAD corresponding to This value is placed in an "encrypted_server_name" extension. <del> The client MUST place the value of ESNIConfig.public_name in the "server_name" extension.  (This is required for technical conformance with RFC7540; Section 9.2.)  The client MUST NOT send a "cached_info" extension RFC7924 with a CachedObject entry whose </del>  CachedInformationType is "cert", since this indication would divulge the true server name. authenticating the connection for the origin server. If the value is "esni_retry_request", the client proceeds with the <del> handshake, authenticating for ESNIConfig.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT use the retry keys. </del>  Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection If the server negotiates an earlier version of TLS, or if it does not provide an "encrypted_server_name" extension in EncryptedExtensions, the client proceeds with the handshake, authenticating for <del> ESNIConfig.public_name as described in auth-public-name.  If an earlier version was negotiated, the client MUST NOT enable the False Start optimization RFC7918 for this handshake.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT treat this as a secure signal to disable ESNI. </del>  Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an decline ESNI-established sessions if it did not accept ESNI. The client MUST verify that the certificate is valid for <del> ESNIConfig.public_name.  If invalid, it MUST abort the connection with the appropriate alert. </del>  If the server requests a client certificate, the client MUST respond with an empty Certificate message, denoting no client server performs the following checks: If the ClientEncryptedSNI.key_share group does not match one in <del> the ESNIConfig.keys, it MUST abort the connection with an </del>  "illegal_parameter" alert. If the length of the "encrypted_server_name" extension is abort the connection with a "decrypt_error" alert. If the decrypted value's length is different from the advertised <del> ESNIConfig.padded_length or the padding consists of any value other than 0, then the server MUST abort the connection with an </del>  "illegal_parameter" alert.  Otherwise, the server uses the PaddedServerNameList.sni value as if it were the "server_name" extension.  Any actual "server_name" extension is ignored, which also

ESNIConfig has a version number in front for extensibility, but it is unclear what the version invariants are. Presumably we would like versions to change the contents of an ESNIConfig (indeed different drafts have done so), but it is not obvious this is possible and it is impossible to skip over an ESNIConfig with a version you do not understand. Fix this by moving the body of an ESNIConfig into an ESNIConfigContents and adding a length prefix in front.
As of draft-ietf-tls-esni-04, when the server rejects ESNI with esniretryrequest, it includes one or more ESNIKeys for retry: The client behavior described in section 5.1.2 is to pick one of the ESNIKeys values from this vector: Encoding of contains the total size of the vector field but not the size of the individual ESNIKeys elements. This makes it difficult (impossible?) to parse a vector with unknown future versions, so this part is not really extensible.
Thanks for the comment! URL should resolve this. It's a little goofy-looking but ah well.
Thank you for confirming the issue. Not the nicest but will be effective. It's the only real blocking point I see when implementing draft -04.

with a version they do not understand.  [[NOTE: This means that the RFC will presumably have a nonzero value.]] <ins> An opaque byte string whose contents depend on the version of the structure.  For this specification, the contents are an ESNIConfigContents structure. The ESNIConfigContents structure contains the following fields: </ins> The non-empty name of the entity trusted to update these encryption keys.  This is used to repair misconfigurations, as described in handle-server-response. Zero padding whose length makes the serialized PaddedServerNameList struct have a length equal to  <ins> ESNIConfigContents.padded_length. </ins> This value consists of the serialized ServerNameList from the "server_name" extension, padded with enough zeroes to make the total  <ins> structure ESNIConfigContents.padded_length bytes long.  The purpose of the padding is to prevent attackers from using the length of the </ins> "encrypted_server_name" extension to determine the true SNI.  If the  <ins> serialized ServerNameList is longer than ESNIConfigContents.padded_length, the client MUST NOT use the "encrypted_server_name" extension. </ins> The ClientEncryptedSNI.encrypted_sni value is then computed using AEAD-Encrypt (RFC5116; Section 2.1) with the AEAD corresponding to This value is placed in an "encrypted_server_name" extension.  <ins> The client MUST place the value of ESNIConfigContents.public_name in the "server_name" extension.  (This is required for technical conformance with RFC7540; Section 9.2.)  The client MUST NOT send a "cached_info" extension RFC7924 with a CachedObject entry whose </ins> CachedInformationType is "cert", since this indication would divulge the true server name. authenticating the connection for the origin server. If the value is "esni_retry_request", the client proceeds with the  <ins> handshake, authenticating for ESNIConfigContents.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT use the retry keys. </ins> Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection If the server negotiates an earlier version of TLS, or if it does not provide an "encrypted_server_name" extension in EncryptedExtensions, the client proceeds with the handshake, authenticating for  <ins> ESNIConfigContents.public_name as described in auth-public-name.  If an earlier version was negotiated, the client MUST NOT enable the False Start optimization RFC7918 for this handshake.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT treat this as a secure signal to disable ESNI. </ins> Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an decline ESNI-established sessions if it did not accept ESNI. The client MUST verify that the certificate is valid for  <ins> ESNIConfigContents.public_name.  If invalid, it MUST abort the connection with the appropriate alert. </ins> If the server requests a client certificate, the client MUST respond with an empty Certificate message, denoting no client server performs the following checks: If the ClientEncryptedSNI.key_share group does not match one in  <ins> the ESNIConfigContents.keys, it MUST abort the connection with an </ins> "illegal_parameter" alert. If the length of the "encrypted_server_name" extension is abort the connection with a "decrypt_error" alert. If the decrypted value's length is different from the advertised  <ins> ESNIConfigContents.padded_length or the padding consists of any value other than 0, then the server MUST abort the connection with an </ins> "illegal_parameter" alert.  Otherwise, the server uses the PaddedServerNameList.sni value as if it were the "server_name" extension.  Any actual "server_name" extension is ignored, which also


The ESNIConfigs structure contains one or more ESNIConfig structures in decreasing order of preference.  This allows a server to support <del> multiple versions of ESNI and multiple sets of ESNI extensions. </del>  The ESNIConfig structure contains the following fields: encryption keys.  This is used to repair misconfigurations, as described in handle-server-response. <del> The list of keys which can be used by the client to encrypt the SNI.  Every key being listed MUST belong to a different group. </del>  The length to pad the ServerNameList value to prior to encryption. This value SHOULD be set to the largest ServerNameList the server order bit set to 1.  A client which receives a mandatory extension they do not understand must reject the ESNIConfig content. <del> Any of the listed keys in the ESNIConfig value may be used to encrypt the SNI for the associated domain name.  The cipher suite list is orthogonal to the list of keys, so each key may be used with any cipher suite.  Clients MUST parse the extension list and check for unsupported mandatory extensions.  If an unsupported mandatory extension is present, clients MUST reject the ESNIConfig value. </del>  5. In order to send an encrypted SNI, the client MUST first select one of the ESNIConfig values in the ESNIConfigs structure which it is <del> able to process (see esni-configuration).  It MUST then select one of the KeyShareEntry values in the ESNIConfig and generate an (EC)DHE share in the matching group.  This share will then be sent to the server in the "encrypted_server_name" extension and used to derive the SNI encryption key.  It does not affect the (EC)DHE shared secret used in the TLS key schedule.  The client MUST also select an </del>  appropriate cipher suite from the list of suites offered by the server.  If the client is unable to select an appropriate group or suite it SHOULD ignore that ESNIConfig value and MAY attempt to use If the ClientEncryptedSNI value does match a known ESNIConfig, the server performs the following checks: <del> If the ClientEncryptedSNI.key_share group does not match one in the ESNIConfigContents.keys, it MUST abort the connection with an </del>  "illegal_parameter" alert. If the length of the "encrypted_server_name" extension is

[This was split out of as the possibly more controversial idea. It's a simplification but does make configs larger. Also it sits on top of and . Ignore the first two comments to look at it in isolation.] We have two layers of parameter negotiation. Multiple KeyShareEntry values in an ESNIConfig, and multiple ESNIConfig values in an ESNIConfigs. We only need one mechanism, so store only one KeyShareEntry in an ESNIConfig. This simplifies representing client and server configuration, which simplies implementation internals as well as interchange formats. Right now, on the server, configuration has two layers of lists, something like list(tuple(ESNIConfig, list(PrivateKey))). Now the server maintains list(tuple(ESNIConfig, PrivateKey)). Note also that draft-farrell-tls-pemesni-00 has this oddity around what to do if the ESNIKeys contains multiple keys. This is particularly a nuisance because a single standalone PRIVATE KEY / ESNIKEY pair is not usable because the server has committed to answering to all keys in the ESNIKeys. The server must consume multiple PEM files, match up the ESNIKeyses, and then assert that it has a complete set. The downside is that, in the likely case that the server only supports one version and one set of extensions, the non-key ESNI config bits (notably the public name; the rest is small) are repeated for every named group. However, the public name should not be that large and servers should only need one or two groups (see URL).
A server can publish multuple HTTPSSVC RRs. Each of those RRs can contain multiple ECHOConfigContent values (i.e. multiple public keys and public_names). I don't see a real need for two different ways to publish more than one public key for a given server. There is a minor efficiency benefit in terms of the number of octets published in the DNS, but there is a lot of additional complexity in terms of code, and if nobody really publishes multiple public keys in one RR value, then that code is likely where we'll find CVEs. Reducing down to one public key per RR value also reduces the complexity when writing code to create/publish RRs. I think we have to support multiple RR values, because that's how the DNS works, and so the one to drop is the multiple public keys in one ECHOConfigs. IOW, I'd suggest one ECHOConfig has exactly one public key.
A single ECHOConfig already has only one public key from URL and the HPKE bits. I'm guessing you meant ECHOConfigs? There are multiple ECHOConfig values in a single ECHOConfigs, which came from URL (Gosh, that was a terrible name. My bad. Maybe we should rename it?) I think the rationale there still holds? The problem is that an HTTPSSVC RR implies transport-level decisions, while not every ECHOConfig can be consumed by your TLS logic. Folding them together means transport connect needs to be aware of the exact ECHO capabilities. For instance, the origin could advertise: URL echo=ECHOv1 URL echo=ECHOv2 The logic picking HTTPSSVC routes no longer gets a free choice. It must be aware of the TLS layer's capabilities (down to mandatory extensions and HPKE code points). ECHOConfigs means the expectation is: URL echo={ECHOv1,ECHOv2} URL echo={ECHOv2,ECHOv2} Now you pick a route freely and pass the entire opaque ECHO blob to TLS to interpret. Of course, one could still advertise: URL echo={ECHOv1} URL echo={ECHOv2} But you're no longer forced to explode it out like that, so we can say an ECHOv1-only client that happens to pick route2 can act as if route1 didn't exist. I.e. we set an expectation that all your routes must be equally capable. (Just as we have that expectation elsewhere. If route1 only speaks TLS 1.0, you're not getting TLS's downgrade protection because the attacker can always redirect you to the less secure config.)
Gotta admit I'm not at all getting what you're saying, sorry. As-is, clients need to write the code to handle multiple keys (and all other variations possible) at both levels, or if not, then we're missing text in the spec saying what doesn't need to be supported. I don't see a real benefit in that complexity.
As per today's call, we're closing this due to the simplifications it brings for client implementations, in particular around collating all logic about ECHOConfig compatibility inside the TLS stack without the need for additional interfaces.
This LGTM -- thanks, David!

The ESNIConfigs structure contains one or more ESNIConfig structures in decreasing order of preference.  This allows a server to support  <ins> multiple versions of ESNI and multiple sets of ESNI parameters. </ins> The ESNIConfig structure contains the following fields: encryption keys.  This is used to repair misconfigurations, as described in handle-server-response.  <ins> The key which can be used by the client to encrypt the SNI. Clients MUST ignore any ESNIConfig structure with a key using a NamedGroup they do not support or are unwilling to use. </ins> The length to pad the ServerNameList value to prior to encryption. This value SHOULD be set to the largest ServerNameList the server order bit set to 1.  A client which receives a mandatory extension they do not understand must reject the ESNIConfig content.  <ins> Clients MUST parse the extension list and check for unsupported mandatory extensions.  If an unsupported mandatory extension is present, clients MUST reject the ESNIConfig value. </ins> 5. In order to send an encrypted SNI, the client MUST first select one of the ESNIConfig values in the ESNIConfigs structure which it is  <ins> able to process (see esni-configuration).  It then generates an (EC)DHE share in the matching group.  This share will then be sent to the server in the "encrypted_server_name" extension and used to derive the SNI encryption key.  It does not affect the (EC)DHE shared secret used in the TLS key schedule.  The client MUST also select an </ins> appropriate cipher suite from the list of suites offered by the server.  If the client is unable to select an appropriate group or suite it SHOULD ignore that ESNIConfig value and MAY attempt to use If the ClientEncryptedSNI value does match a known ESNIConfig, the server performs the following checks:  <ins> If the ClientEncryptedSNI.key_share group does not match ESNIConfigContents.key, it MUST abort the connection with an </ins> "illegal_parameter" alert. If the length of the "encrypted_server_name" extension is


The design in this document takes a different approach: it assumes that private origins will co-locate with or hide behind a provider <del> (CDN, app server, etc.) which is able to activate encrypted SNI (ESNI) for all of the domains it hosts.  Thus, the use of encrypted SNI does not indicate that the client is attempting to reach a private origin, but only that it is going to a particular service provider, which the observer could already tell from the IP address. </del>  2. 3.2. <del> SNI encryption requires that each provider publish a public key and metadata which is used for SNI encryption for all the domains for which it serves directly or indirectly (via Split Mode).  This document defines the format of the SNI encryption public key and metadata, referred to as an ESNI configuration, and delegates DNS publication details to HTTPSSVC, though other delivery mechanisms are possible.  In particular, if some of the clients of a private server are applications rather than Web browsers, those applications might have the public key and metadata preconfigured. </del>  When a client wants to form a TLS connection to any of the domains <del> served by an ESNI-supporting provider, it sends an "encrypted_server_name" extension, which contains the true extension encrypted under the provider's public key.  The provider can then decrypt the extension and either terminate the connection (in Shared Mode) or forward it to the backend server (in Split Mode). </del>  4. <del> SNI Encryption configuration information is conveyed with the following ESNIConfig structure. </del>  <del> The ESNIConfig structure contains the following fields: </del>  The version of the structure.  For this specification, that value <del> SHALL be 0xff03.  Clients MUST ignore any ESNIConfig structure with a version they do not understand.  [[NOTE: This means that the RFC will presumably have a nonzero value.]] </del>  The non-empty name of the entity trusted to update these encryption keys.  This is used to repair misconfigurations, as described in handle-server-response. <del> The list of keys which can be used by the client to encrypt the SNI.  Every key being listed MUST belong to a different group. </del>  <del> padded_length The length to pad the ServerNameList value to prior to encryption.  This value SHOULD be set to the largest ServerNameList the server expects to support rounded up the nearest multiple of 16. If the server supports arbitrary wildcard names, it SHOULD set this value to 260.  Clients SHOULD reject ESNIConfig as invalid if padded_length is greater than 260. </del>  A list of extensions that the client can take into consideration <del> when generating a Client Hello message.  The format is defined in RFC8446; Section 4.2.  The purpose of the field is to provide room for additional features in the future.  An extension may be tagged as mandatory by using an extension type codepoint with the high order bit set to 1.  A client which receives a mandatory extension they do not understand must reject the ESNIConfig content. Any of the listed keys in the ESNIConfig value may be used to encrypt the SNI for the associated domain name.  The cipher suite list is orthogonal to the list of keys, so each key may be used with any cipher suite.  Clients MUST parse the extension list and check for unsupported mandatory extensions.  If an unsupported mandatory extension is present, clients MUST reject the ESNIConfig value. </del>  5. <del> The encrypted SNI is carried in an "encrypted_server_name" extension, defined as follows: </del>  For clients (in ClientHello), this extension contains the following <del> ClientEncryptedSNI structure: </del>  <del> The cipher suite used to encrypt the SNI. </del>  <del> The KeyShareEntry carrying the client's public ephemeral key share used to derive the ESNI key. A cryptographic hash of the ESNIConfig structure from which the ESNI key was obtained, i.e., from the first byte of "version" to </del>  the end of the structure.  This hash is computed using the hash function associated with "suite". <del> The ClientESNIInner structure, AEAD-encrypted using cipher suite "suite" and the key generated as described below. For servers (in EncryptedExtensions), this extension contains the following structure: </del>  <del> Indicates whether the server processed the client ESNI extension. (See handle-server-response and server-behavior.} </del>  <del> The contents of ClientESNIInner.nonce.  (See client-behavior.) </del>  <del> One or more ESNIConfig structures containing the keys that the client should use on subsequent connections to encrypt the ClientESNIInner structure. </del>  <del> This protocol also defines the "esni_required" alert, which is sent </del>  by the client when it offered an "encrypted_server_name" extension which was not accepted by the server.  Finally, requirements in client-behavior and server-behavior require implementations to track, alongside each PSK established by a previous connection, whether the connection negotiated this extension <del> with the "esni_accept" response type.  If so, this is referred to as an "ESNI PSK".  Otherwise, it is a "non-ESNI PSK".  This may be </del>  implemented by adding a new field to client and server session states. <del> 5.1. 5.1.1. In order to send an encrypted SNI, the client MUST first select one of the server ESNIKeyShareEntry values and generate an (EC)DHE share in the matching group.  This share will then be sent to the server in the "encrypted_server_name" extension and used to derive the SNI encryption key.  It does not affect the (EC)DHE shared secret used in the TLS key schedule.  The client MUST also select an appropriate cipher suite from the list of suites offered by the server.  If the client is unable to select an appropriate group or suite it SHOULD ignore that ESNIConfig value and MAY attempt to use another value provided by the server.  The client MUST NOT send encrypted SNI using groups or cipher suites not advertised by the server. When offering an encrypted SNI, the client MUST NOT offer to resume any non-ESNI PSKs.  It additionally MUST NOT offer to resume any sessions for TLS 1.2 or below. Let Z be the DH shared secret derived from a key share in ESNIConfig and the corresponding client share in ClientEncryptedSNI.key_share. The SNI encryption key is computed from Z as follows: where ESNIContents is as specified below and Hash is the hash function associated with the HKDF instantiation.  The salt argument for HKDF-Extract is a string consisting of Hash.length bytes set to zeros.  For a client's first ClientHello, KeyLabel = "esni key" and IVLabel = "esni iv", whereas for a client's second ClientHello, sent in response to a HelloRetryRequest, KeyLabel = "hrr esni key" and IVLabel = "hrr esni iv".  (This label variance is done to prevent nonce re-use since the client's ESNI key share, and thus the value of Zx, does not change across ClientHello retries.) Note that ESNIContents will not be directly transmitted to the server in the ClientHello.  The server will instead reconstruct the same object by obtaining its values from ClientEncryptedSNI and ClientHello. [[TODO: label swapping fixes a bug in the spec, though this may not be the best way to deal with HRR.  See https://github.com/tlswg/ draft-ietf-tls-esni/issues/121 and https://github.com/tlswg/draft- ietf-tls-esni/pull/170 for more details.]] Same value as ClientEncryptedSNI.record_digest. Same value as ClientEncryptedSNI.key_share. Same nonce as ClientHello.random. The client then creates a ClientESNIInner structure: A random 16-octet value to be echoed by the server in the "encrypted_server_name" extension. The true SNI DNS name, that is, the HostName value that would have been sent in the plaintext "server_name" extension.  (NameType values other than "host_name" are unsupported since SNI extensibility failed SNIExtensibilityFailed). Zero padding whose length makes the serialized PaddedServerNameList struct have a length equal to ESNIConfig.padded_length. This value consists of the serialized ServerNameList from the "server_name" extension, padded with enough zeroes to make the total structure ESNIConfig.padded_length bytes long.  The purpose of the padding is to prevent attackers from using the length of the "encrypted_server_name" extension to determine the true SNI.  If the serialized ServerNameList is longer than ESNIConfig.padded_length, the client MUST NOT use the "encrypted_server_name" extension. The ClientEncryptedSNI.encrypted_sni value is then computed using the usual TLS 1.3 AEAD: Where KeyShareClientHello is the "extension_data" field of the "key_share" extension in a Client Hello (Section 4.2.8 of RFC8446)). Including KeyShareClientHello in the AAD of AEAD-Encrypt binds the ClientEncryptedSNI value to the ClientHello and prevents cut-and- paste attacks. Note: future extensions may end up reusing the server's ESNIKeyShareEntry for other purposes within the same message (e.g., encrypting other values).  Those usages MUST have their own HKDF labels to avoid reuse. [[OPEN ISSUE: If in the future you were to reuse these keys for 0-RTT priming, then you would have to worry about potentially expanding twice of Z_extracted.  We should think about how to harmonize these to make sure that we maintain key separation.]] This value is placed in an "encrypted_server_name" extension. The client MUST place the value of ESNIConfig.public_name in the "server_name" extension.  (This is required for technical conformance with RFC7540; Section 9.2.)  The client MUST NOT send a "cached_info" extension RFC7924 with a CachedObject entry whose </del>  CachedInformationType is "cert", since this indication would divulge the true server name. <del> 5.1.2. If the server negotiates TLS 1.3 or above and provides an "encrypted_server_name" extension in EncryptedExtensions, the client then processes the extension's "response_type" field: If the value is "esni_accept", the client MUST check that the extension's "nonce" field matches ClientESNIInner.nonce and otherwise abort the connection with an "illegal_parameter" alert. The client then proceeds with the connection as usual, authenticating the connection for the origin server. If the value is "esni_retry_request", the client proceeds with the handshake, authenticating for ESNIConfig.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT use the retry keys. Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an "esni_required" alert.  It then processes the "retry_keys" field from the server's "encrypted_server_name" extension. If one of the values contains a version supported by the client, it can regard the ESNI keys as securely replaced by the server. It SHOULD retry the handshake with a new transport connection, using that value to encrypt the SNI.  The value may only be applied to the retry connection.  The client MUST continue to use the previously-advertised keys for subsequent connections.  This avoids introducing pinning concerns or a tracking vector, should a malicious server present client-specific retry keys to identify clients. If none of the values provided in "retry_keys" contains a supported version, the client can regard ESNI as securely disabled by the server.  As below, it SHOULD then retry the handshake with a new transport connection and ESNI disabled. If the field contains any other value, the client MUST abort the connection with an "illegal_parameter" alert. </del>  If the server negotiates an earlier version of TLS, or if it does not provide an "encrypted_server_name" extension in EncryptedExtensions, the client proceeds with the handshake, authenticating for <del> ESNIConfig.public_name as described in auth-public-name.  If an earlier version was negotiated, the client MUST NOT enable the False Start optimization RFC7918 for this handshake.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT treat this as a secure signal to disable ESNI. </del>  Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an <del> "esni_required" alert.  The client can then regard ESNI as securely </del>  disabled by the server.  It SHOULD retry the handshake with a new <del> transport connection and ESNI disabled. </del>  [[TODO: Key replacement is significantly less scary than saying that <del> ESNI-naive servers bounce ESNI off.  Is it worth defining a strict mode toggle in the ESNI keys, for a deployment to indicate it is </del>  ready for that? ]] Clients SHOULD implement a limit on retries caused by <del> "esni_retry_request" or servers which do not acknowledge the </del>  "encrypted_server_name" extension.  If the client does not retry in either scenario, it MUST report an error to the calling application. <del> If the server sends a HelloRetryRequest in response to the ClientHello and the client can send a second updated ClientHello per the rules in RFC8446, the "encrypted_server_name" extension values which do not depend on the (possibly updated) KeyShareClientHello, i.e,, ClientEncryptedSNI.suite, ClientEncryptedSNI.key_share, and ClientEncryptedSNI.record_digest, MUST NOT change across ClientHello messages.  Moreover, ClientESNIInner MUST not change across ClientHello messages.  Informally, the values of all unencrypted extension information, as well as the inner extension plaintext, must be consistent between the first and second ClientHello messages. 5.1.3. </del>  When the server cannot decrypt or does not process the "encrypted_server_name" extension, it continues with the handshake using the cleartext "server_name" extension instead (see server- <del> behavior).  Clients that offer ESNI then authenticate the connection </del>  with the public name, as follows: If the server resumed a session or negotiated a session that did not use a certificate for authentication, the client MUST abort the connection with an "illegal_parameter" alert.  This case is <del> invalid because send-esni requires the client to only offer ESNI- </del>  established sessions, and server-behavior requires the server to <del> decline ESNI-established sessions if it did not accept ESNI. </del>  The client MUST verify that the certificate is valid for <del> ESNIConfig.public_name.  If invalid, it MUST abort the connection with the appropriate alert. </del>  If the server requests a client certificate, the client MUST respond with an empty Certificate message, denoting no client implemented, for instance, by reporting a failed connection with a dedicated error code. <del> 5.1.4. </del>  If the client attempts to connect to a server and does not have an <del> ESNIConfig structure available for the server, it SHOULD send a GREASE I-D.ietf-tls-grease "encrypted_server_name" extension as </del>  follows: Select a supported cipher suite, named group, and padded_length <del> value.  The padded_length value SHOULD be 260 or a multiple of 16 less than Set the "suite" field to the selected cipher suite.  These selections 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 "key_share" field to a randomly-generated valid public key for the named group. </del>  Set the "record_digest" field to a randomly-generated string of hash_length bytes, where hash_length is the length of the hash function associated with the chosen cipher suite. <del> Set the "encrypted_sni" field to a randomly-generated string of 16 + padded_length + tag_length bytes, where tag_length is the tag length of the chosen cipher suite's associated AEAD. </del>  <del> If the server sends an "encrypted_server_name" extension, the client </del>  MUST check the extension syntactically and abort the connection with <del> a "decode_error" alert if it is invalid.  If the "response_type" field contains "esni_retry_requested", the client MUST ignore the extension and proceed with the handshake.  If it contains "esni_accept" or any other value, the client MUST abort the connection with an "illegal_parameter" alert. </del>  Offering a GREASE extension is not considered offering an encrypted <del> SNI for purposes of requirements in client-behavior.  In particular, the client MAY offer to resume sessions established without ESNI. </del>  <del> 5.2. </del>  <del> Upon receiving an "encrypted_server_name" extension, the client- </del>  facing server MUST check that it is able to negotiate TLS 1.3 or greater.  If not, it MUST abort the connection with a "handshake_failure" alert. <del> The ClientEncryptedSNI value is said to match a known ESNIConfig if there exists an ESNIConfig that can be used to successfully decrypt ClientEncryptedSNI.encrypted_sni.  This matching procedure should be </del>  done using one of the following two checks: <del> Compare ClientEncryptedSNI.record_digest against cryptographic hashes of known ESNIConfig and choose the one that matches. </del>  <del> Use trial decryption of ClientEncryptedSNI.encrypted_sni with known ESNIConfig and choose the one that succeeds. </del>  <del> Some uses of ESNI, such as local discovery mode, may omit the ClientEncryptedSNI.record_digest since it can be used as a tracking </del>  vector.  In such cases, trial decryption should be used for matching <del> ClientEncryptedSNI to known ESNIConfig.  Unless specified by the </del>  application using (D)TLS or externally configured on both sides, implementations MUST use the first method. <del> If the ClientEncryptedSNI value does not match any known ESNIConfig </del>  structure, it MUST ignore the extension and proceed with the connection, with the following added behavior: <del> It MUST include the "encrypted_server_name" extension in EncryptedExtensions message with the "response_type" field set to "esni_retry_requested" and the "retry_keys" field set to one or more ESNIConfig structures with up-to-date keys.  Servers MAY supply multiple ESNIConfig values of different versions.  This allows a server to support multiple versions at once. </del>  The server MUST ignore all PSK identities in the ClientHello which <del> correspond to ESNI PSKs.  ESNI PSKs offered by the client are associated with the ESNI name.  The server was unable to decrypt then ESNI name, so it should not resume them when using the </del>  cleartext SNI name.  This restriction allows a client to reject resumptions in auth-public-name. <del> Note that an unrecognized ClientEncryptedSNI.record_digest value may be a GREASE ESNI extension (see grease-extensions), so it is </del>  necessary for servers to proceed with the connection and rely on the <del> client to abort if ESNI was required.  In particular, the unrecognized value alone does not indicate a misconfigured ESNI </del>  advertisement (misconfiguration).  Instead, servers can measure <del> occurrences of the "esni_required" alert to detect this case. If the ClientEncryptedSNI value does match a known ESNIConfig, the server performs the following checks: If the ClientEncryptedSNI.key_share group does not match one in the ESNIConfig.keys, it MUST abort the connection with an "illegal_parameter" alert. If the length of the "encrypted_server_name" extension is inconsistent with the advertised padding length (plus AEAD expansion) the server MAY abort the connection with an "illegal_parameter" alert without attempting to decrypt. Assuming these checks succeed, the server then computes K_sni and decrypts the ServerName value.  If decryption fails, the server MUST abort the connection with a "decrypt_error" alert. If the decrypted value's length is different from the advertised ESNIConfig.padded_length or the padding consists of any value other than 0, then the server MUST abort the connection with an "illegal_parameter" alert.  Otherwise, the server uses the PaddedServerNameList.sni value as if it were the "server_name" extension.  Any actual "server_name" extension is ignored, which also means the server MUST NOT send the "server_name" extension to the client. </del>  Upon determining the true SNI, the client-facing server then either serves the connection directly (if in Shared Mode), in which case it case, it does not make any changes to the TLS messages, but just blindly forwards them. <del> If the ClientHello is the result of a HelloRetryRequest, servers MUST abort the connection with an "illegal_parameter" alert if any of the ClientEncryptedSNI.suite, ClientEncryptedSNI.key_share, ClientEncryptedSNI.record_digest, or decrypted ClientESNIInner values from the second ClientHello do not match that of the first ClientHello. 5.3. A server operating in Shared Mode uses PaddedServerNameList.sni as if it were the "server_name" extension to finish the handshake.  It SHOULD pad the Certificate message, via padding at the record layer, such that its length equals the size of the largest possible Certificate (message) covered by the same ESNI key.  Moreover, the server MUST include the "encrypted_server_name" extension in EncryptedExtensions with the "response_type" field set to "esni_accept" and the "nonce" field set to the decrypted PaddedServerNameList.nonce value from the client "encrypted_server_name" extension. </del> If the server sends a NewSessionTicket message, the corresponding <del> ESNI PSK MUST be ignored by all other servers in the deployment when not negotiating ESNI, including servers which do not implement this </del>  specification. <del> This restriction provides robustness for rollbacks (see misconfiguration). 5.4. In Split Mode, the backend server must know PaddedServerNameList.nonce to echo it back in EncryptedExtensions and complete the handshake.  communicating-sni describes one mechanism for sending both PaddedServerNameList.sni and ClientESNIInner.nonce to the backend server.  Thus, backend servers function the same as servers operating in Shared Mode. As in Shared Mode, if the backend server sends a NewSessionTicket message, the corresponding ESNI PSK MUST be ignored by other servers in the deployment when not negotiating ESNI, including servers which do not implement this specification. 6. </del>  <del> Unlike most TLS extensions, placing the SNI value in an ESNI </del>  extension is not interoperable with existing servers, which expect the value in the existing cleartext extension.  Thus server operators <del> SHOULD ensure servers understand a given set of ESNI keys before </del>  advertising them.  Additionally, servers SHOULD retain support for <del> any previously-advertised keys for the duration of their validity. </del>  However, in more complex deployment scenarios, this may be difficult to fully guarantee.  Thus this protocol was designed to be robust in <del> case of inconsistencies between systems that advertise ESNI keys and </del>  servers, at the cost of extra round-trips due to a retry.  Two specific scenarios are detailed below. <del> 6.1. </del>  <del> It is possible for ESNI advertisements and servers to become </del>  inconsistent.  This may occur, for instance, from DNS misconfiguration, caching issues, or an incomplete rollout in a multi-server deployment.  This may also occur if a server loses its <del> ESNI keys, or if a deployment of ESNI must be rolled back on the </del>  server. The retry mechanism repairs inconsistencies, provided the server is authoritative for the public name.  If server and advertised keys <del> mismatch, the server will respond with esni_retry_requested.  If the </del>  server does not understand the "encrypted_server_name" extension at all, it will ignore it as required by RFC8446; Section 4.1.2. Provided the server can present a certificate valid for the public name, the client can safely retry with updated settings, as described in handle-server-response. <del> Unless ESNI is disabled as a result of successfully establishing a connection to the public name, the client MUST NOT fall back to cleartext SNI, as this allows a network attacker to disclose the SNI. It MAY attempt to use another server from the DNS results, if one is </del>  provided. <del> 6.2. </del>  A more serious problem is MITM proxies which do not support this extension.  RFC8446; Section 9.3 requires that such proxies remove certificate as authoritative for the public name, this may trigger the retry logic described in handle-server-response or result in a connection failure.  A proxy which is not authoritative for the <del> public name cannot forge a signal to disable ESNI. </del>  <del> A non-conformant MITM proxy which instead forwards the ESNI </del>  extension, substituting its own KeyShare value, will result in the client-facing server recognizing the key, but failing to decrypt the SNI.  This causes a hard failure.  Clients SHOULD NOT attempt to repair the connection in this case. <del> 7. </del>  <del> 7.1. </del>  In comparison to I-D.kazuho-protected-sni, wherein DNS Resource <del> Records are signed via a server private key, ESNI records have no </del>  authenticity or provenance information.  This means that any attacker which can inject DNS responses or poison DNS caches, which is a common scenario in client access networks, can supply clients with <del> fake ESNI records (so that the client encrypts SNI to them) or strip the ESNI record from the response.  However, in the face of an attacker that controls DNS, no SNI encryption scheme can work because the attacker can replace the IP address, thus blocking client </del>  connections, or substituting a unique IP address which is 1:1 with the DNS name that was looked up (modulo DNS wildcards).  Thus, <del> allowing the ESNI records in the clear does not make the situation </del>  significantly worse. Clearly, DNSSEC (if the client validates and hard fails) is a defense against this form of attack, but DoH/DPRIVE are also defenses against DNS attacks by attackers on the local network, which is a common case <del> where SNI is desired.  Moreover, as noted in the introduction, SNI encryption is less useful without encryption of DNS queries in transit via DoH or DPRIVE mechanisms. </del>  <del> 7.2. </del>  Supporting optional record digests and trial decryption opens oneself up to DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any <del> known ESNI key, in order to force decryption.  Servers that support </del>  this feature should, for example, implement some form of rate limiting mechanism to limit the damage caused by such attacks. <del> 7.3. ESNI protects only the SNI in transit.  Other ClientHello extensions, such as ALPN, might also reveal privacy-sensitive information to the network.  As such, it might be desirable to encrypt other extensions alongside the SNI.  However, the SNI extension is unique in that non- TLS-terminating servers or load balancers may act on its contents. Thus, using keys specifically for SNI encryption promotes key separation between client-facing servers and endpoints party to TLS connections.  Moreover, the ESNI design described herein does not preclude a mechanism for generic ClientHello extension encryption. </del>  <del> 7.4. ESNI requires encrypted DNS to be an effective privacy protection </del>  mechanism.  However, verifying the server's identity from the Certificate message, particularly when using the X509 CertificateType, may result in additional network traffic that may SHOULD take steps to minimize or protect such requests during certificate validation. <del> 7.5. </del>  I-D.ietf-tls-sni-encryption lists several requirements for SNI encryption.  In this section, we re-iterate these requirements and <del> assess the ESNI design against them. </del>  <del> 7.5.1. </del>  <del> Since the SNI encryption key is derived from a (EC)DH operation between the client's ephemeral and server's semi-static ESNI key, the ESNI encryption is bound to the Client Hello.  It is not possible for an attacker to "cut and paste" the ESNI value in a different Client Hello, with a different ephemeral key share, as the terminating server will fail to decrypt and verify the ESNI value. </del>  <del> 7.5.2. </del>  This design depends upon DNS as a vehicle for semi-static public key distribution.  Server operators may partition their private keys however they see fit provided each server behind an IP address has <del> the corresponding private key to decrypt a key.  Thus, when one ESNI </del>  key is provided, sharing is optimally bound by the number of hosts that share an IP address.  Server operators may further limit sharing <del> by publishing different DNS records containing ESNIConfig values with </del>  different keys using a short TTL. <del> 7.5.3. </del>  This design requires servers to decrypt ClientHello messages with <del> ClientEncryptedSNI extensions carrying valid digests.  Thus, it is </del>  possible 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. <del> 7.5.4. </del>  <del> As more clients enable ESNI support, e.g., as normal part of Web </del>  browser functionality, with keys supplied by shared hosting <del> providers, the presence of ESNI extensions becomes less suspicious </del>  and part of common or predictable client behavior.  In other words, <del> if all Web browsers start using ESNI, the presence of this value does </del>  not signal suspicious behavior to passive eavesdroppers. Additionally, this specification allows for clients to send GREASE <del> ESNI extensions (see grease-extensions), which helps ensure the </del>  ecosystem handles the values correctly. <del> 7.5.5. </del>  <del> This design is not forward secret because the server's ESNI key is </del>  static.  However, the window of exposure is bound by the key lifetime.  It is RECOMMENDED that servers rotate keys frequently. <del> 7.5.6. </del>  This design permits servers operating in Split Mode to forward connections directly to backend origin servers, thereby avoiding unnecessary MiTM attacks. <del> 7.5.7. </del>  <del> Assuming ESNI records retrieved from DNS are authenticated, e.g., via </del>  DNSSEC or fetched from a trusted Recursive Resolver, spoofing a server operating in Split Mode is not possible.  See cleartext-dns for more details regarding cleartext DNS. <del> Authenticating the ESNIConfig structure naturally authenticates the </del>  included public name.  This also authenticates any retry signals from the server because the client validates the server certificate against the public name before retrying. <del> 7.5.8. </del>  This design has no impact on application layer protocol negotiation. It may affect connection routing, server certificate selection, and client certificate verification.  Thus, it is compatible with multiple protocols. <del> 7.6. </del>  Note that the backend server has no way of knowing what the SNI was, but that does not lead to additional privacy exposure because the responsible for verifying the server's identity in its certificate. [[TODO: Some more analysis needed in this case, as it is a little <del> odd, and probably some precise rules about handling ESNI and no SNI </del>  uniformly?]] <del> 8. </del>  <del> 8.1. </del>  IANA is requested to create an entry, encrypted_server_name(0xffce), in the existing registry for ExtensionType (defined in RFC8446), with "TLS 1.3" column values being set to "CH, EE", and "Recommended" column being set to "Yes". <del> 8.2. </del>  <del> IANA is requested to create an entry, esni_required(121) in the </del>  existing registry for Alerts (defined in RFC8446), with the "DTLS-OK" column being set to "Y". <del> 8.3. IANA is requested to create an entry, ESNI(0xff9f), in the existing registry for Resource Record (RR) TYPEs (defined in RFC6895) with "Meaning" column value being set to "Encrypted SNI". </del>

This change implements the ESNI -> ECHO design discussed in Singapore. There a couple pending TODOs, particularly around cross-CH HRR binding, but the meat is what's currently being analyzed. cc NAME NAME NAME NAME
This still seems a bit rough, but I think better in the spec than out.

The design in this document takes a different approach: it assumes that private origins will co-locate with or hide behind a provider  <ins> (CDN, app server, etc.) which is able to activate encrypted SNI, by encrypting the entire ClientHello (ECHO), for all of the domains it hosts.  As a result, the use of ECHO to protect the SNI does not indicate that the client is attempting to reach a private origin, but only that it is going to a particular service provider, which the observer could already tell from the IP address. </ins> 2. 3.2.  <ins> ECHO works by encrypting the entire ClientHello, including the SNI and any additional extensions such as ALPN.  This requires that each provider publish a public key and metadata which is used for ClientHello encryption for all the domains for which it serves directly or indirectly (via Split Mode).  This document defines the format of the SNI encryption public key and metadata, referred to as an ECHO configuration, and delegates DNS publication details to HTTPSSVC, though other delivery mechanisms are possible.  In particular, if some of the clients of a private server are applications rather than Web browsers, those applications might have the public key and metadata preconfigured. </ins> When a client wants to form a TLS connection to any of the domains  <ins> served by an ECHO-supporting provider, it constructs a ClientHello in the regular fashion containing the true SNI value (ClientHelloInner) and then encrypts it using the public key for the provider.  It then constructs a new ClientHello (ClientHelloOuter) with an innocuous SNI (and potentially innocuous versions of other extensions such as ALPN RFC7301) and containing the encrypted ClientHelloInner as an extension.  It sends ClientHelloOuter to the server. Upon receiving ClientHelloOuter, the server can then decrypt ClientHelloInner and either terminate the connection (in Shared Mode) or forward it to the backend server (in Split Mode). Note that both ClientHelloInner and ClientHelloOuter are both valid, complete ClientHello messages.  ClientHelloOuter carries an encrypted representation of ClientHelloInner in a "encrypted_client_hello" extension, defined in encrypted-client-hello. </ins> 4.  <ins> ClientHello encryption configuration information is conveyed with the following ECHOConfigs structure. </ins>  <ins> The ECHOConfigs structure contains one or more ECHOConfig structures in decreasing order of preference.  This allows a server to support multiple versions of ECHO and multiple sets of ECHO parameters. The ECHOConfig structure contains the following fields: </ins> The version of the structure.  For this specification, that value  <ins> SHALL be 0xff03.  Clients MUST ignore any ECHOConfig structure with a version they do not understand. An opaque byte string whose contents depend on the version of the structure.  For this specification, the contents are an ECHOConfigContents structure. The ECHOConfigContents structure contains the following fields: </ins> The non-empty name of the entity trusted to update these encryption keys.  This is used to repair misconfigurations, as described in handle-server-response.  <ins> The HPKE I-D.irtf-cfrg-hpke public key which can be used by the client to encrypt the ClientHello. The HPKE I-D.irtf-cfrg-hpke KEM identifier corresponding to public_key.  Clients MUST ignore any ECHOConfig structure with a key using a KEM they do not support. The list of TLS cipher suites which can be used by the client to encrypt the ClientHello.  See hpke-map for information on how a cipher suite maps to corresponding HPKE algorithm identifiers. </ins>  <ins> the largest name the server expects to support.  If the server supports arbitrary wildcard names, it SHOULD set this value to 256.  Clients SHOULD reject ESNIConfig as invalid if maximum_name_length is greater than 256. </ins> A list of extensions that the client can take into consideration  <ins> when generating a Client Hello message.  The purpose of the field is to provide room for additional features in the future.  The format is defined in RFC8446; Section 4.2.  The same interpretation rules apply: extensions MAY appear in any order, but there MUST NOT be more than one extension of the same type in the extensions block.  An extension may be tagged as mandatory by using an extension type codepoint with the high order bit set to 1.  A client which receives a mandatory extension they do not understand must reject the ECHOConfig content. Clients MUST parse the extension list and check for unsupported mandatory extensions.  If an unsupported mandatory extension is present, clients MUST reject the ECHOConfig value. </ins> 5.  <ins> The encrypted ClientHelloInner is carried in an "encrypted_client_hello" extension, defined as follows: </ins> For clients (in ClientHello), this extension contains the following  <ins> ClientEncryptedCH structure: </ins>  <ins> The cipher suite used to encrypt ClientHelloInner. </ins>  <ins> A cryptographic hash of the ECHOConfig structure from which the ECHO key was obtained, i.e., from the first byte of "version" to </ins> the end of the structure.  This hash is computed using the hash function associated with "suite".  <ins> The HPKE encapsulated key, used by servers to decrypt the corresponding encrypted_ch field. </ins>  <ins> The serialized and encrypted ClientHelloInner structure, AEAD- encrypted using cipher suite "suite" and the key generated as described below. </ins>  <ins> If the server accepts ECHO, it does not send this extension.  If it rejects ECHO, then it sends the following structure in EncryptedExtensions: </ins>  <ins> An ESNIConfigs structure containing one or more ECHOConfig structures in decreasing order of preference that the client should use on subsequent connections to encrypt the ClientHelloInner structure. </ins>  <ins> This protocol also defines the "echo_required" alert, which is sent </ins> by the client when it offered an "encrypted_server_name" extension which was not accepted by the server. <ins> 6. When using ECHO, the client MUST also add an extension of type "echo_nonce" to the ClientHelloInner (but not to the outer ClientHello).  This nonce ensures that the server's encrypted Certificate can only be read by the entity which sent this ClientHello.  [[TODO: Describe HRR cut-and-paste 1 in Security Considerations.]] This extension is defined as follows: A 16-byte nonce exported from the HPKE encryption context.  See send-echo for details about its computation. </ins> Finally, requirements in client-behavior and server-behavior require implementations to track, alongside each PSK established by a previous connection, whether the connection negotiated this extension  <ins> with the "echo_accept" response type.  If so, this is referred to as an "ECHO PSK".  Otherwise, it is a "non-ECHO PSK".  This may be </ins> implemented by adding a new field to client and server session states.  <ins> 6.1. Some TLS 1.3 extensions can be quite large and having them both in the inner and outer ClientHello will lead to a very large overall size.  One particularly pathological example is "key_share" with post-quantum algorithms.  In order to reduce the impact of duplicated extensions, the client may use the "outer_extensions" extension. OuterExtensions MUST only be used in ClientHelloInner.  It consists of one or more ExtensionType values, each of which reference an extension in ClientHelloOuter, and a digest of the complete ClientHelloInner. When sending ClientHello, the client first computes ClientHelloInner, including any PSK binders, and then MAY substitute extensions which it knows will be duplicated in ClientHelloOuter.  To do so, the client computes a hash H of the entire ClientHelloInner message with the same hash as for the KDF used to encrypt ClienHelloInner.  Then, the client removes and and replaces extensions from ClientHelloInner with a single "outer_extensions" extension.  The list of outer_extensions include those which were removed from ClientHelloInner, in the order in which they were removed.  The hash contains the full ClientHelloInner hash H computed above. This process is reversed by client-facing servers upon receipt. Specifically, the server replaces the "outer_extensions" with extensions contained in ClientHelloOuter.  The server then computes a hash H' of the reconstructed ClientHelloInner.  If H' does not equal OuterExtensions.hash, the server aborts the connection with an "illegal_parameter" alert. Clients SHOULD only use this mechanism for extensions which are large.  All other extensions SHOULD appear in both ClientHelloInner and ClientHelloOuter even if they have identical values. 7. 7.1. In order to send an encrypted ClientHello, the client first determines if it supports the server's chosen KEM, as identified by ECHOConfig.kem_id.  If one is supported, the client MUST select an appropriate cipher suite from the list of suites offered by the server.  If the client does not support the corresponding KEM or is unable to select an appropriate group or suite, it SHOULD ignore that ECHOConfig value and MAY attempt to use another value provided by the server.  The client MUST NOT send ECHO using HPKE algorithms not advertised by the server. Given a compatible ECHOConfig with fields public_key and kem_id, carrying the HpkePublicKey and KEM identifier corresponding to the server, clients compute an HPKE encryption context as follows: Note that the HPKE algorithm identifiers are those which match the client's chosen CipherSuite, according to hpke-map.  The client MAY replace any large, duplicated extensions in ClientHelloInner with the corresponding "outer_extensions" extension, as described in outer- extensions. The client then generates a ClientHelloInner value.  In addition to the normal values, ClientHelloInner MUST also contain: an "echo_nonce" extension TLS padding Padding SHOULD be P = L - D bytes, where L = ECHOConfig.maximum_name_length, rounded up to the nearest multiple of 16 D = len(dns_name), where dns_name is the DNS name in the ClientHelloInner "server_name" extension When offering an encrypted ClientHello, the client MUST NOT offer to resume any non-ECHO PSKs.  It additionally MUST NOT offer to resume any sessions for TLS 1.2 or below. The encrypted ClientHello value is then computed as: Finally, the client MUST generate a ClientHelloOuter message containing the "encrypted_client_hello" extension with the values as indicated above.  In particular, suite contains the client's chosen ciphersuite; record_digest contains the digest of the corresponding ECHOConfig structure; enc contains the encapsulated key as output by SetupBaseS; and encrypted_ch contains the HPKE encapsulated key (enc) and the ClientHelloInner ciphertext (encrypted_ch_inner). The client MUST place the value of ECHOConfig.public_name in the ClientHelloOuter "server_name" extension.  The remaining contents of the ClientHelloOuter MAY be identical to those in ClientHelloInner but MAY also differ.  The ClientHelloOuter MUST NOT contain a "cached_info" extension RFC7924 with a CachedObject entry whose </ins> CachedInformationType is "cert", since this indication would divulge the true server name.  <ins> 7.2. As described in server-behavior, the server MAY either accept ECHO and use ClientHelloInner or reject it and use ClientHelloOuter. However, there is no indication in ServerHello of which one the server has done and the client must therefore use trial decryption in order to determine this. 7.2.1. If the server used ClientHelloInner, the client proceeds with the connection as usual, authenticating the connection for the origin server. 7.2.2. If the server used ClientHelloOuter, the client proceeds with the handshake, authenticating for ECHOConfig.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT use the retry keys. Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an "echo_required" alert.  It then processes the "retry_keys" field from the server's "encrypted_server_name" extension. If one of the values contains a version supported by the client, it can regard the ECHO keys as securely replaced by the server.  It SHOULD retry the handshake with a new transport connection, using that value to encrypt the ClientHello.  The value may only be applied to the retry connection.  The client MUST continue to use the previously-advertised keys for subsequent connections.  This avoids introducing pinning concerns or a tracking vector, should a malicious server present client-specific retry keys to identify clients. If none of the values provided in "retry_keys" contains a supported version, the client can regard ECHO as securely disabled by the server.  As below, it SHOULD then retry the handshake with a new transport connection and ECHO disabled. If the field contains any other value, the client MUST abort the connection with an "illegal_parameter" alert. </ins> If the server negotiates an earlier version of TLS, or if it does not provide an "encrypted_server_name" extension in EncryptedExtensions, the client proceeds with the handshake, authenticating for  <ins> ECHOConfigContents.public_name as described in auth-public-name.  If an earlier version was negotiated, the client MUST NOT enable the False Start optimization RFC7918 for this handshake.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT treat this as a secure signal to disable ECHO. </ins> Otherwise, when the handshake completes successfully with the public name authenticated, the client MUST abort the connection with an  <ins> "echo_required" alert.  The client can then regard ECHO as securely </ins> disabled by the server.  It SHOULD retry the handshake with a new  <ins> transport connection and ECHO disabled. </ins> [[TODO: Key replacement is significantly less scary than saying that  <ins> ECHO-naive servers bounce ECHO off.  Is it worth defining a strict mode toggle in the ECHO keys, for a deployment to indicate it is </ins> ready for that? ]] Clients SHOULD implement a limit on retries caused by  <ins> "echo_retry_request" or servers which do not acknowledge the </ins> "encrypted_server_name" extension.  If the client does not retry in either scenario, it MUST report an error to the calling application.  <ins> 7.2.2.1. </ins> When the server cannot decrypt or does not process the "encrypted_server_name" extension, it continues with the handshake using the cleartext "server_name" extension instead (see server-  <ins> behavior).  Clients that offer ECHO then authenticate the connection </ins> with the public name, as follows: If the server resumed a session or negotiated a session that did not use a certificate for authentication, the client MUST abort the connection with an "illegal_parameter" alert.  This case is  <ins> invalid because send-echo requires the client to only offer ECHO- </ins> established sessions, and server-behavior requires the server to  <ins> decline ECHO-established sessions if it did not accept ECHO. </ins> The client MUST verify that the certificate is valid for  <ins> ECHOConfigContents.public_name.  If invalid, it MUST abort the connection with the appropriate alert. </ins> If the server requests a client certificate, the client MUST respond with an empty Certificate message, denoting no client implemented, for instance, by reporting a failed connection with a dedicated error code.  <ins> 7.2.3. If the server sends a HelloRetryRequest in response to the ClientHello, the client sends a second updated ClientHello per the rules in RFC8446.  However, at this point, the client does not know whether the server processed ClientHelloOuter or ClientHelloInner, and MUST regenerate both values to be acceptable.  Note: if the inner and outer ClientHellos use different groups for their key shares or differ in some other way, then the HelloRetryRequest may actually be invalid for one or the other ClientHello, in which case a fresh ClientHello MUST be generated, ignoring the instructions in HelloRetryRequest.  Otherwise, the usual rules for HelloRetryRequest processing apply. Clients bind encryption of the second ClientHelloInner to encryption of the first ClientHelloInner via the derived echo_hrr_key by modifying HPKE setup as follows: Clients then encrypt the second ClientHelloInner using this new HPKE context.  In doing so, the encrypted value is also authenticated by echo_hrr_key. Client-facing servers perform the corresponding process when decrypting second ClientHelloInner messages.  In particular, upon receipt of a second ClientHello message with a ClientEncryptedCH value, servers setup their HPKE context and decrypt ClientEncryptedCH as follows: [[OPEN ISSUE: Should we be using the PSK input or the info input?  On the one hand, the requirements on info seem weaker, but maybe actually this needs to be secret?  Analysis needed.]] [[OPEN ISSUE: This, along with trial decryption is pretty gross.  It would just be a lot easier if we were willing to have the server indicate whether ECHO had been accepted or not.  Given that the server is supposed to only reject ECHO when it doesn't know the key, and this is easy to probe for, can we just instead have an extension to indicate what has happened.]] 7.3. </ins> If the client attempts to connect to a server and does not have an  <ins> ECHOConfig structure available for the server, it SHOULD send a GREASE I-D.ietf-tls-grease "encrypted_client_hello" extension as </ins> follows: Select a supported cipher suite, named group, and padded_length  <ins> value.  The padded_length value SHOULD be 260 (sum of the maximum DNS name length and TLS encoding overhead) or a multiple of 16 less than 260.  Set the "suite" field to the selected cipher suite.  These selections SHOULD vary to exercise all supported configurations, but MAY be held constant for successive connections to the same server in the same session. </ins>  <ins> Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. </ins> Set the "record_digest" field to a randomly-generated string of hash_length bytes, where hash_length is the length of the hash function associated with the chosen cipher suite.  <ins> Set the "encrypted_client_hello" field to a randomly-generated string of [TODO] bytes. </ins>  <ins> If the server sends an "encrypted_client_hello" extension, the client </ins> MUST check the extension syntactically and abort the connection with  <ins> a "decode_error" alert if it is invalid. </ins> Offering a GREASE extension is not considered offering an encrypted  <ins> ClientHello for purposes of requirements in client-behavior.  In particular, the client MAY offer to resume sessions established without ECHO. </ins>  <ins> 8. </ins>  <ins> Upon receiving an "encrypted_client_hello" extension, the client- </ins> facing server MUST check that it is able to negotiate TLS 1.3 or greater.  If not, it MUST abort the connection with a "handshake_failure" alert.  <ins> The ClientEncryptedCH value is said to match a known ECHOConfig if there exists an ECHOConfig that can be used to successfully decrypt ClientEncryptedCH.encrypted_ch.  This matching procedure should be </ins> done using one of the following two checks:  <ins> Compare ClientEncryptedCH.record_digest against cryptographic hashes of known ECHOConfig and choose the one that matches. </ins>  <ins> Use trial decryption of ClientEncryptedCH.encrypted_ch with known ECHOConfig and choose the one that succeeds. </ins>  <ins> Some uses of ECHO, such as local discovery mode, may omit the ClientEncryptedCH.record_digest since it can be used as a tracking </ins> vector.  In such cases, trial decryption should be used for matching  <ins> ClientEncryptedCH to known ECHOConfig.  Unless specified by the </ins> application using (D)TLS or externally configured on both sides, implementations MUST use the first method.  <ins> If the ClientEncryptedCH value does not match any known ECHOConfig </ins> structure, it MUST ignore the extension and proceed with the connection, with the following added behavior:  <ins> It MUST include the "encrypted_client_hello" extension with the "retry_keys" field set to one or more ECHOConfig structures with up-to-date keys.  Servers MAY supply multiple ECHOConfig values of different versions.  This allows a server to support multiple versions at once. </ins> The server MUST ignore all PSK identities in the ClientHello which  <ins> correspond to ECHO PSKs.  ECHO PSKs offered by the client are associated with the ECHO name.  The server was unable to decrypt then ECHO name, so it should not resume them when using the </ins> cleartext SNI name.  This restriction allows a client to reject resumptions in auth-public-name.  <ins> Note that an unrecognized ClientEncryptedCH.record_digest value may be a GREASE ECHO extension (see grease-extensions), so it is </ins> necessary for servers to proceed with the connection and rely on the  <ins> client to abort if ECHO was required.  In particular, the unrecognized value alone does not indicate a misconfigured ECHO </ins> advertisement (misconfiguration).  Instead, servers can measure  <ins> occurrences of the "echo_required" alert to detect this case. If the ClientEncryptedCH value does match a known ECHOConfig, the server then decrypts ClientEncryptedCH.encrypted_ch, using the private key skR corresponding to ESNIConfig, as follows: If decryption fails, the server MUST abort the connection with a "decrypt_error" alert.  Moreover, if there is no "echo_nonce" extension, or if its value does not match the derived echo_nonce, the server MUST abort the connection with a "decrypt_error" alert.  Next, the server MUST scan ClientHelloInner for any "outer_extension" extensions and substitute their values with the values in ClientHelloOuter.  It MUST first verify that the hash found in the extension matches the hash of the extension to be interpolated in and if it does not, abort the connections with a "decrypt_error" alert. </ins> Upon determining the true SNI, the client-facing server then either serves the connection directly (if in Shared Mode), in which case it case, it does not make any changes to the TLS messages, but just blindly forwards them.  If the server sends a NewSessionTicket message, the corresponding  <ins> ECHO PSK MUST be ignored by all other servers in the deployment when not negotiating ECHO, including servers which do not implement this </ins> specification.  <ins> 9. </ins>  <ins> Unlike most TLS extensions, placing the SNI value in an ECHO </ins> extension is not interoperable with existing servers, which expect the value in the existing cleartext extension.  Thus server operators  <ins> SHOULD ensure servers understand a given set of ECHO keys before </ins> advertising them.  Additionally, servers SHOULD retain support for  <ins> any previously-advertised keys for the duration of their validity </ins> However, in more complex deployment scenarios, this may be difficult to fully guarantee.  Thus this protocol was designed to be robust in  <ins> case of inconsistencies between systems that advertise ECHO keys and </ins> servers, at the cost of extra round-trips due to a retry.  Two specific scenarios are detailed below.  <ins> 9.1. </ins>  <ins> It is possible for ECHO advertisements and servers to become </ins> inconsistent.  This may occur, for instance, from DNS misconfiguration, caching issues, or an incomplete rollout in a multi-server deployment.  This may also occur if a server loses its  <ins> ECHO keys, or if a deployment of ECHO must be rolled back on the </ins> server. The retry mechanism repairs inconsistencies, provided the server is authoritative for the public name.  If server and advertised keys  <ins> mismatch, the server will respond with echo_retry_requested.  If the </ins> server does not understand the "encrypted_server_name" extension at all, it will ignore it as required by RFC8446; Section 4.1.2. Provided the server can present a certificate valid for the public name, the client can safely retry with updated settings, as described in handle-server-response.  <ins> Unless ECHO is disabled as a result of successfully establishing a connection to the public name, the client MUST NOT fall back to using unencrypted ClientHellos, as this allows a network attacker to disclose the contents of this ClientHello, including the SNI.  It MAY attempt to use another server from the DNS results, if one is </ins> provided.  <ins> Client-facing servers with non-uniform cryptographic configurations across backend origin servers segment the ECHO anonymity set based on these configurations.  For example, if a client-facing server hosts k backend origin servers, and exactly one of those backend origin servers supports a different set of cryptographic algorithms than the other (k - 1) servers, it may be possible to identify this single server based on the contents of the ServerHello as this message is not encrypted. 9.2. </ins> A more serious problem is MITM proxies which do not support this extension.  RFC8446; Section 9.3 requires that such proxies remove certificate as authoritative for the public name, this may trigger the retry logic described in handle-server-response or result in a connection failure.  A proxy which is not authoritative for the  <ins> public name cannot forge a signal to disable ECHO. </ins>  <ins> A non-conformant MITM proxy which instead forwards the ECHO </ins> extension, substituting its own KeyShare value, will result in the client-facing server recognizing the key, but failing to decrypt the SNI.  This causes a hard failure.  Clients SHOULD NOT attempt to repair the connection in this case.  <ins> 10. </ins>  <ins> Per RFC8446, TLS ciphersuites define an AEAD and hash algorithm.  In contrast, HPKE defines separate AEAD algorithms and key derivation functions.  The table below lists the mapping between ciphersuites and HPKE identifiers.  TLS_AES_128_CCM_SHA256 and TLS_AES_128_CCM_8_SHA256 are not supported ECHO ciphersuites as they have no HPKE equivalent. 11. 11.1. </ins> In comparison to I-D.kazuho-protected-sni, wherein DNS Resource  <ins> Records are signed via a server private key, ECHO records have no </ins> authenticity or provenance information.  This means that any attacker which can inject DNS responses or poison DNS caches, which is a common scenario in client access networks, can supply clients with  <ins> fake ECHO records (so that the client encrypts data to them) or strip the ECHO record from the response.  However, in the face of an attacker that controls DNS, no encryption scheme can work because the attacker can replace the IP address, thus blocking client </ins> connections, or substituting a unique IP address which is 1:1 with the DNS name that was looked up (modulo DNS wildcards).  Thus,  <ins> allowing the ECHO records in the clear does not make the situation </ins> significantly worse. Clearly, DNSSEC (if the client validates and hard fails) is a defense against this form of attack, but DoH/DPRIVE are also defenses against DNS attacks by attackers on the local network, which is a common case  <ins> where ClientHello and SNI encryption are desired.  Moreover, as noted in the introduction, SNI encryption is less useful without encryption of DNS queries in transit via DoH or DPRIVE mechanisms. </ins>  <ins> 11.2. </ins> Supporting optional record digests and trial decryption opens oneself up to DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any  <ins> known ECHO key, in order to force decryption.  Servers that support </ins> this feature should, for example, implement some form of rate limiting mechanism to limit the damage caused by such attacks.  <ins> 11.3. </ins>  <ins> ECHO requires encrypted DNS to be an effective privacy protection </ins> mechanism.  However, verifying the server's identity from the Certificate message, particularly when using the X509 CertificateType, may result in additional network traffic that may SHOULD take steps to minimize or protect such requests during certificate validation.  <ins> 11.4. </ins> I-D.ietf-tls-sni-encryption lists several requirements for SNI encryption.  In this section, we re-iterate these requirements and  <ins> assess the ECHO design against them. </ins>  <ins> 11.4.1. </ins>  <ins> Since servers process either ClientHelloInner or ClientHelloOuter, and ClientHelloInner contains an HPKE-derived nonce, it is not possible for an attacker to "cut and paste" the ECHO value in a different Client Hello and learn information from ClientHelloInner. This is because the attacker lacks access to the HPKE-derived nonce used to derive the handshake secrets. </ins>  <ins> 11.4.2. </ins> This design depends upon DNS as a vehicle for semi-static public key distribution.  Server operators may partition their private keys however they see fit provided each server behind an IP address has  <ins> the corresponding private key to decrypt a key.  Thus, when one ECHO </ins> key is provided, sharing is optimally bound by the number of hosts that share an IP address.  Server operators may further limit sharing  <ins> by publishing different DNS records containing ECHOConfig values with </ins> different keys using a short TTL.  <ins> 11.4.3. </ins> This design requires servers to decrypt ClientHello messages with  <ins> ClientEncryptedCH extensions carrying valid digests.  Thus, it is </ins> possible 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.  <ins> 11.4.4. </ins>  <ins> As more clients enable ECHO support, e.g., as normal part of Web </ins> browser functionality, with keys supplied by shared hosting  <ins> providers, the presence of ECHO extensions becomes less suspicious </ins> and part of common or predictable client behavior.  In other words,  <ins> if all Web browsers start using ECHO, the presence of this value does </ins> not signal suspicious behavior to passive eavesdroppers. Additionally, this specification allows for clients to send GREASE  <ins> ECHO extensions (see grease-extensions), which helps ensure the </ins> ecosystem handles the values correctly.  <ins> 11.4.5. </ins>  <ins> This design is not forward secret because the server's ECHO key is </ins> static.  However, the window of exposure is bound by the key lifetime.  It is RECOMMENDED that servers rotate keys frequently.  <ins> 11.4.6. </ins> This design permits servers operating in Split Mode to forward connections directly to backend origin servers, thereby avoiding unnecessary MiTM attacks.  <ins> 11.4.7. </ins>  <ins> Assuming ECHO records retrieved from DNS are authenticated, e.g., via </ins> DNSSEC or fetched from a trusted Recursive Resolver, spoofing a server operating in Split Mode is not possible.  See cleartext-dns for more details regarding cleartext DNS.  <ins> Authenticating the ECHOConfigs structure naturally authenticates the </ins> included public name.  This also authenticates any retry signals from the server because the client validates the server certificate against the public name before retrying.  <ins> 11.4.8. </ins> This design has no impact on application layer protocol negotiation. It may affect connection routing, server certificate selection, and client certificate verification.  Thus, it is compatible with multiple protocols.  <ins> 11.5. </ins> Note that the backend server has no way of knowing what the SNI was, but that does not lead to additional privacy exposure because the responsible for verifying the server's identity in its certificate. [[TODO: Some more analysis needed in this case, as it is a little  <ins> odd, and probably some precise rules about handling ECHO and no SNI </ins> uniformly?]]  <ins> 12. </ins>  <ins> 12.1. </ins> IANA is requested to create an entry, encrypted_server_name(0xffce), in the existing registry for ExtensionType (defined in RFC8446), with "TLS 1.3" column values being set to "CH, EE", and "Recommended" column being set to "Yes".  <ins> 12.2. </ins>  <ins> IANA is requested to create an entry, echo_required(121) in the </ins> existing registry for Alerts (defined in RFC8446), with the "DTLS-OK" column being set to "Y".


GREASE I-D.ietf-tls-grease "encrypted_client_hello" extension as follows: <del> Select a supported cipher suite, named group, and padded_length value.  The padded_length value SHOULD be 260 (sum of the maximum DNS name length and TLS encoding overhead) or a multiple of 16 less than 260.  Set the "suite" field to the selected cipher suite.  These selections SHOULD vary to exercise all supported configurations, but MAY be held constant for successive connections to the same server in the same session. Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. </del>  Set the "record_digest" field to a randomly-generated string of hash_length bytes, where hash_length is the length of the hash function associated with the chosen cipher suite. <del> Set the "encrypted_client_hello" field to a randomly-generated string of [TODO] bytes. </del>  If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with

Also define the length of the randomly-generated "encrypted_ch" field.
NAME NAME

GREASE I-D.ietf-tls-grease "encrypted_client_hello" extension as follows:  <ins> Set the "suite" field to a supported cipher suite.  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 "record_digest" field to a randomly-generated string of hash_length bytes, where hash_length is the length of the hash function associated with the chosen cipher suite.  <ins> Set the "enc" field to a randomly-generated valid encapsulated public key output by the HPKE KEM. Set the "encrypted_ch" field to a randomly-generated string of [TODO] bytes. </ins> If the server sends an "encrypted_client_hello" extension, the client MUST check the extension syntactically and abort the connection with


disadvantage that the server has to retain a table of all the server names it supports.]]  The semantics of this structure are simple: any of the listed keys may be used to encrypt the SNI for the associated domain name.  The cipher suite list is orthogonal to the list of keys, so each key may

At the moment, there is no extensibility in ESNIKeys. That means that if we want to add additional attributes, we would be forced to define a new DNS record that gets queried simultaneously. I think we do not want to do that. Hence the extensions field. The text is tentative; please feel free to change in any ways.
LGTM.
With merged, I wonder if the ESNIKeys struct should just be renamed to something not specific to ESNI, but something more like "generic TLS config in DNS" so that it can be more cleanly reused by unrelated features. "ServerConfiguration" comes to mind from the early TLS 1.3 days.
If we were to re-use these keys for other purposes, that seems fine. However, I'm not sure we want to do that. Semi-static keys, for example, should ideally be separate.
The keys yes, but I imagine the extensions field can hold "arbitrary" data, so while the keys would be ESNI-specific, you could also include extensions that have nothing to do with ESNI.
Thinking about this some more, this would also require changing the "_esni." prefix to something else. In any case I can see how this might be out of scope for this spec. In the end it's a matter of deciding whether future TLS extensions that need similar DNS records, should be able to reuse the same structure (so no additional TXT record with its own prefix would be required), or if they should define their own structure (and have a separate TXT record). I'm happy to close this if it goes too much out of scope.
Right, I think it's verging on being out of scope. What do others think? NAME NAME
My preference goes to keeping the name as-is, considering the fact that the role of ESNIKeys is (at least for the moment) to negotiate properties between the client and the fronting server (not the hidden server), and that the only property we need to negotiate between the two is the information necessary for ESNI protection. It is true that the DNS record can covey properties related to the hidden server (as you know, I've argued for using it to carry the server certificate chain). But IMO that's a change of concept, and I prefer keeping the concept simple for the time being.
Ok.

disadvantage that the server has to retain a table of all the server names it supports.]] <ins> A list of extensions that the client can take into consideration when generating a Client Hello message.  The format is defined in I-D.ietf-tls-tls13; Section 4.2.  The purpose of the field is to provide room for additional features in the future; this document does not define any extension. </ins> The semantics of this structure are simple: any of the listed keys may be used to encrypt the SNI for the associated domain name.  The cipher suite list is orthogonal to the list of keys, so each key may


10.4.4. <del> As more clients enable ECHO support, e.g., as normal part of Web browser functionality, with keys supplied by shared hosting providers, the presence of ECHO extensions becomes less suspicious and part of common or predictable client behavior.  In other words, if all Web browsers start using ECHO, the presence of this value does not signal suspicious behavior to passive eavesdroppers. Additionally, this specification allows for clients to send GREASE ECHO extensions (see grease-extensions), which helps ensure the ecosystem handles the values correctly. </del>  10.4.5.

Addresses . cc NAME
There's no explicit signal indicating that ECHO was negotiated for a connection. Thus, clients must perform trial decryption with both possible handshake keys. If we intend to keep this, then we should crisply state that "don't stick out" is limited to explicit signals, i.e., implicit signals such as size-related information is probably out of scope. (It's certainly an open question what is the "best" padding scheme for handshake messages.) cc NAME

10.4.4.  <ins> The only explicit signal indicating possible use of ECHO is the ClientHello "encrypted_client_hello" extension.  Server handshake messages do not contain any signal indicating use or negotiation of ECHO.  Clients MAY GREASE the "encrypted_client_hello" extension, as described in grease-extensions, which helps ensure the ecosystem handles ECHO correctly.  Moreover, as more clients enable ECHO support, e.g., as normal part of Web browser functionality, with keys supplied by shared hosting providers, the presence of ECHO extensions becomes less unusual and part of typical client behavior.  In other words, if all Web browsers start using ECHO, the presence of this value will not signal unusual behavior to passive eavesdroppers. </ins> 10.4.5.


10.2. <del> Supporting optional record digests and trial decryption opens oneself up to DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any known ECHO key, in order to force 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.3.

Addresses . cc NAME
And possibly also include a bit inside ESNIConfig that tells clients whether or not to populate record_digest. (See URL for some relevant definitions.)
Works for me.

10.2.  <ins> Optional record digests 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" extension.  In such settings, servers must perform trial decrypt upon receipt of an empty record digest, which may exacerbate DoS attacks.  Specifically, an adversary may send malicious ClientHello messages, i.e., those which will not decrypt with any known ECHO 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.3.