Draft: draft-ietf-tls-esni

Commentary: Based on my experience implementing this extension ( see ) , I 'd like to recommend a few changes to the spec . Most notably : the constants so that they 're consistent . the client / server behavior in a few failure scenarios . There are also some editorial changes : " HpkeCipherSuite " to " ECHCipherSuite " . ( An HPKE cipher suite consists of a KEM , KDF , and AEAD , but here a cipher suite is a KDF and an AEAD . ) " " to " OuterExtensions.innerdigest " . " ClientECH.encryptedch " to " ClientECH.payload " . Partially addresses 
 In a normal ECHO handshake with HelloRetryRequest there are 4 ClientHellos sent : ClientHelloOuter1 , ClientHelloInner1 , ClientHelloOuter2 , ClientHelloInner2 . Encryption of ClientHelloInner2 is bound to echohrrkey so the server must have the ECHOConfig used for  in order to process ECHO the second time . And by the time the client receives encrypted handshake traffic it is not useful to signal if  was successfully decrypted , what matters is to know if ECHO is globally successful or not . So one aspect that I find not explained enough is whether the server replying with HRR should use  in the transcript , or instead always takes ClientHelloOuter1 ( data on the wire ) regardless of the first decryption status . The ECHO status accept / reject can be conveyed based on distinction ClientHello2 inner / outer only . This point impacts the number of transcripts the client may have to try , i.e. can be sure the combination ( ClientHelloInner1 , ClientHelloOuter2 ) is never valid . 
 Hi the spec has changed quite a bit since this question was first posed . In particular , we 've landed , which changes the server behavior so that it provides an explicit signal of ECH acceptance in its SH . Hence , no more trial decryption . I 'm wondering if your question still applies ? I believe what to do in case of  is well - specified at this point . There is no such ECH accdeptance signal provided in the HRR , so to determine if ECH was accepted , the client must wait until the SH . Until that point , the client needs to compute two transcripts in parallel : one assuming the ClientHelloOuter was used , and another assuming the ClientHelloInner was used . 
 I do n't think resolves this . If the client has different HRR - relevant preferences ( key share and cipher suite ) between CHInner and CHOuter , I believe  is still a mess because the HRR message does n't contain a signal for whether ECH was accepted . Though I suppose it is a little less complex now that you do n't need to go as far as trial decrypting . Different HRR - relevant preferences means the HRR may be good for one ClientHello but not the other , yet the client needs to manage that state and defer the actual error - handling to when ECH acceptance is known . An option where the HRR message included an ECH acceptance signal would close this , but then we have sticking out woes . The draft touches on this , but somewhat vaguely . We 've been intending that our implementation would always match inner and outer preferences for HRR - sensitive fields , to avoid this case . But having this odd complexity cliff hidden in the spec is poor . I 'd advocate we either require or at least strongly recommend clients do this . 
 suggests to add the following text to the HRR section : after . Likewise , it is an error for the client to offer ECH after the HelloRetryRequest but not before . If the client - facing server accepts ECH for the first ClientHello but not the second , or it accepts ECH for the second ClientHello but not the first , then it MUST abort the handshake with an " illegal_parameter " alert . I think this solves the problem , at least partially . It ensures that the  path aligns with ECH acceptance / rejection . 
 That does n't address the problem . This is about complexity for the client , not the server . Yes , the server needs to enforce consistency between the two modes , but it is easy for it to do this . Imagine you 're the client and your CHOuter has : keyshares = { X25519 } supportedgroups = { X25519 , P-256 , P-384 } ciphersuites = { TLSAES128GCMSHA256 , TLSAES256GCMSHA384 } And your CHInner has : keyshares = { P-256 } supportedgroups = { X25519 , P-256 , P-384 } ciphersuites = { TLSAES256GCMSHA384 , TLSCHACHA20POLY1305SHA256 } Now consider how you have to respond to each of these HRRs . Remember that at the time you process the  , you do n't know if ECH was accepted . = TLSAES256GCMSHA384 ; keyshare = P-384 = TLSAES128CCMSHA256 ; keyshare =  = TLSCHACHA20POLY1305SHA256 ; keyshare = X25519 = TLSAES128GCMSHA256 ; keyshare = P-256 = TLSCHACHA20POLY1305SHA256 ; keyshare = X25519 1 is valid for both  . This is the easy case and you can compute new CHInner and CHOuter values . 2 is valid for neither ClientHello . If you can detect this , you can error immediately . 3 is valid for neither ClientHello , but for messy reasons . The key share is valid for CHOuter but the cipher suite is not . The cipher suite is valid for CHInner but the key share is not . ( Recall that a key share in HRR is allowed if it is in CH.supportedgroups and not CH.keyshares . If it were in CH.keyshares , you should n't have sent  . ) 4 is valid for only CHOuter , so you ca n't error . It is not possible to compute a CHInner , so I guess you drop the extension ? But you still need to make a note to raise an error later if you see SH which claims it did accept ECH . 5 is valid for only CHInner , so you ca n't error . It is not possible to compute a CHOuter , but you have to , so I guess you send something garbage ? But you still need to make a note to raise an error if you see SH which claims it rejected ECH . This is a huge mess . The client can avoid this mess by always matching keyshares , supportedgroups , and ciphersuites between CHInner and CHOuter . That means it can process  without knowing which CH to use . It is not obvious in the spec that you should do this , and the spec allows you to not do this . 
 I see , I was n't thinking about client complexity . Thanks for laying out the various edge cases . I would favor being stricter about how the keyshares , supportedgroups , and ciphersuites are chosen . At the very least , we should guide implementations towards ensuring they are the same in the CHInner and CHOuter . An alternative would be to add an ECH signal to HRR . We ca n't do the same trick we did with the , as this would stick out . But maybe there 's another way ? I thought of using HRR.sessionid , but that could get messy . 
 I think there is still ambiguity in the design about what CH is used by the client - facing server in its own transcript when forwarding CHInner to the backend server . Or else the combination ( ECH accepted before HRR , ECH rejected after  ) can be explicitly forbidden , but I do n't see why / how . 
 Incidentally , this is addressed by This adds the following to client - facing server behavior : after . Likewise , it is an error for the client to offer ECH after the HelloRetryRequest but not before . If either of these conditions occurs , then the client - facing server MUST abort the handshake with an " illegal_parameter " alert . It 's not hard for the client - facing server to enforce this . It just needs to remember if an  was triggered and whether ECH was offered in in the first CH . 
 Ok that will do it . Assuming " ECH not offered " is same as " ECH not accepted " , due to greasing .
Paragraph (version A):
The configuration identifier, equal to "Expand(Extract("",
config), "ech_config_id", Nh)", where "config" is the "ECHConfig"
structure and "Extract", "Expand", and "Nh" are as specified by
the cipher suite KDF.  (Passing the literal """" as the salt is
interpreted by "Extract" as no salt being provided.)  The length
of this value SHOULD NOT be less than 16 bytes unless it is
optional for an application; see optional-configs.
Paragraph (version A):
The HPKE encapsulated key, used by servers to decrypt the
corresponding "encrypted_ch" field.
Paragraph (version B):
The configuration identifier, equal to "Expand(Extract("",
config), "tls13 ech config id", Nh)", where "config" is the
"ECHConfig" structure and "Extract", "Expand", and "Nh" are as
specified by the cipher suite KDF.  (Passing the literal """" as
the salt is interpreted by "Extract" as no salt being provided.)
The length of this value SHOULD NOT be less than 16 bytes unless
it is optional for an application; see optional-configs.
Paragraph (version B):
The HPKE encapsulated key, used by servers to decrypt the
corresponding "payload" field.
Paragraph (version A):
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.
Paragraph (version B):
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_extension" extension.
Paragraph (version A):
where "inner" is the ClientHelloInner structure and "Extract",
"Expand", and "Nh" are as defined by the KDF API in I-D.irtf-cfrg-
hpke.  Then, the client removes and replaces extensions from
ClientHelloInner with a single "outer_extensions" extension.  The
list of "outer_extensions" include those which were removed from
ClientHelloInner, in the order in which they were removed.  The hash
contains the full ClientHelloInner hash computed above.
Paragraph (version A):
This process is reversed by client-facing server.  Specifically, the
server replaces the "outer_extensions" with extensions contained in
ClientHelloOuter.  The server then computes a hash of the
reconstructed ClientHelloInner.  If the hash does not equal
OuterExtensions.hash, the server aborts the connection with an
"illegal_parameter" alert.
Paragraph (version B):
where "inner" is the ClientHelloInner structure and "Extract",
"Expand", and "Nh" are as defined by the KDF API in I-D.irtf-cfrg-
hpke.  Then, the client removes and replaces extensions from
ClientHelloInner with a single "outer_extension" extension.  The list
of outer extensions, OuterExtensions.outer_extensions, includes those
which were removed from ClientHelloInner, in the order in which they
were removed.
Paragraph (version B):
This process is reversed by client-facing server.  Specifically, the
server replaces the "outer_extension" with the corresponding sequence
of extensions in the ClientHelloOuter.  The server then computes the
digest of the reconstructed ClientHelloInner.  If the digest does not
equal OuterExtensions.inner_digest, then the server MUST abort the
connection with a "decrypt_error" alert.
Paragraph (version A):
"encrypted_ch", as computed above.
Paragraph (version B):
"payload", as computed above.
Paragraph (version A):
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
invalid because send-ech requires the client to only offer ECH-
established sessions, and server-behavior requires the server to
decline ECH-established sessions if it did not accept ECH.
Paragraph (version B):
It is an error for the client to offer ECH before the
HelloRetryRequest but not after.  Likewise, it is an error for the
client to offer ECH after the HelloRetryRequest but not before.  If
the client-facing server accepts ECH for the first ClientHello but
not the second, or it accepts ECH for the second ClientHello but not
the first, then the client MUST abort the handshake with an
"illegal_parameter" alert.
Paragraph (version A):
Set the "suite" field to a supported HpkeCipherSuite.  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.
Paragraph (version B):
Set the "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.
Paragraph (version A):
Set the "encrypted_ch" field to a randomly-generated string of L
bytes, where L is the size of the ClientHelloInner message the
client would use given an ECHConfig structure, padded according to
padding.
Paragraph (version B):
Set the "payload" field to a randomly-generated string of L+C
bytes, where C is the ciphertext expansion of selected AEAD scheme
and L is the size of the ClientHelloInner message the client would
use given an ECHConfig structure, padded according to padding.
Paragraph (version A):
Use trial decryption of ClientECH.encrypted_ch with known
ECHConfig and choose the one that succeeds.
Paragraph (version B):
Use trial decryption of ClientECH.payload with known ECHConfig and
choose the one that succeeds.
Paragraph (version A):
The server MUST ignore all PSK identities in the ClientHello which
correspond to ECH PSKs.  ECH PSKs offered by the client are
associated with the ECH name.  The server was unable to decrypt
then ECH name, so it should not resume them when using the
plaintext SNI name.  This restriction allows a client to reject
resumptions in auth-public-name.
Paragraph (version B):
If offered, the server MUST ignore the "pre_shared_key" extension
in the ClientHello.
Paragraph (version A):
If decryption fails, 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.
Paragraph (version B):
If decryption fails, the server MUST abort the connection with a
"decrypt_error" alert.  Otherwise, the server scans ClientHelloInner
for an "outer_extension" extension and substitutes its value with the
values in ClientHelloOuter and validates the ClientHelloInner digest,
as described in outer-extensions.