Draft: draft-ietf-tls-esni

Commentary: This PR cleans up the client text , in hopes of making it more precise and organized somewhat more chronologically . It then uses this cleanup to clarify bits we forgot to write down in the accept / reject / HRR machinery : the Server Response is now Determining ECH Acceptance and only discusses the initial accept / reject decision . portion of Handling HelloRetryRequest discussing the accept / reject decision is moved to Determining ECH Acceptence . ECH Acceptance is tidied up to discuss to what happens if you see an older TLS version ( it 's a reject ) . In particular , in DTLS 1.2 , the first server message might be HelloVerifyRequest and not ServerHello or HelloRetryRequest . /Rejected ECH are now  with ClientHelloInner / ClientHelloOuter to fit with the other section names ( lots of VERBing ) . They are unindented from ( 1 ) because that section only talks about the accept / reject decision . This gives us a place to discuss all changes to the handshake . We describe what it actually means to handshake with ClientHelloInner , including implications to the transcript . And ... portion of Handling HelloRetryRequest that discussed constructing the second ClientHello never applied to the ECH Reject case anyway , only ECH Accept . Move this to ( 4 ) . Also expand on it so it 's a bit more clearly defined . I 've also moved HPKE setup out of Encrypting the ClientHello , as that 's only relevant to one of the two " callers " anyway . I 'm not thrilled with this notion of " partial ClientHelloOuterAAD " , but the text used in Offering ECH and Encrypting the ClientHello sections otherwise does n't actually match . Before this PR , both think they are responsible for constructing ClientHelloOuter . this and ( 1 ) , Handling HelloRetryRequest is now empty and removed . forgot to write down the client half of There is now a place for it in Handshaking with ClientHelloInner . say that the ECH reject case ignores the extension , so ECH GREASE says the same thing , to keep them aligned . The Handshaking with ClientHelloOuter section could also do with an editorial pass , but I 've omitted it from this PR because it was getting a bit large as it is . 
 I suspect this is editorial , but it would be helpful to clarify exactly how the client computes its transcript when it generates its keys , based on the permutations of who gets what . IIUC : If the server authenticates the inner SNI , the client must construct the client hello as seen by the back - end server , including any OuterExtensions compression , and uses it in the transcript . If the server authenticates the public SNI , the client uses the entire client hello in the hash as - is , including the ( unreadable ) encryptedclienthello extension . A server that combines the client - facing and back - end functions also has to duplicate this logic , since it can see everything but maybe should n't use it all , and has to go through the motions of formally constructing a proper client hello to a back - end server . I found this all quite subtle to work out and asked some questions of one of the authors to fully understand . I 'll leave it to the editors as to whether the draft needs some text to make this clearer . [ Aside : is it computationally feasible to add some sort of " checksum " that would neutralize the difference in the hashes ? That would make this a little less tricky , IMO ] 
 +1 and test vectors would be good too 
 Yes , agree . This is really tricky : " The client presumes acceptance if the last 8 bytes of are equal to the first 8 bytes of accept_confirmation as defined in Section 7.2 . Otherwise , it presumes rejection . " 
 Here 's what I ended up writing after I solved this : " This issue is that the ECH transcript needs to be calculated from an unsent inner ClientHello message , which differs in a few ways from the EncodedClientHelloInner used in HPKE . Upon ECH acceptance , the transcript from the ClientHelloOuter is discarded , then substituted with one calculated from the unsent inner ClientHello message , and the client random is changed to the inner random value . After that , the ServerHello message is to the transcript as normal . The transcript used for ECH acceptance is separate from the normal transcript . " 
 I think the way to think about this is that there are two possible handshakes here , joined at the first message . Either we handshake with ClientHelloOuter , or we handshake with ClientHelloInner . Whichever , we handshake with , we follow the usual RFC8446 rules , including incorporating the chosen ClientHello into the transcript . Indeed on the server , the protocol even supports a client - facing vs. backend server split where the connection is actually rewritten to use ClientHelloInner . EncodedClientHelloInner , per the name , is just an encoding . We do n't want to send two copies of things like key shares . The actual ClientHello being conveyed is still ClientHelloInner . ( This is important since the transcript needs to reflect all inputs to TLS parameter negotiation . ) But , yes , on the client this gets a little fun because the client needs to offer these two somewhat related ClientHellos , learn which was to used a message later , and then complete the handshake accordingly . 
 Perhaps there 's missing text in " 6.1.4.1 . Accepted ECH " , where client transcript handling could be addressed . My feedback is that it was possible to construct a conformant EncodedClientHelloInner and enclosing ClientHelloOuter from reading the spec as a non - author , but I had to test with other implementations to discover what I was supposed to do with the ClientHelloInner . If you get this part wrong , then you just hit a decryption error after ServerHello . I did understand that I had to update the transcript , but I initially thought I should do it with an EncodedClientHelloInner to the ClientHelloOuter transcript . Once I discovered that was not the case , I had to construct a ClientHelloInner that still had the fields that are compressed in EncodedClientHelloInner . That does n't cross anything in the text , but it was n't obvious to me . 
 I think we 're in agreement here , but is the encoding of ClientHelloInner deterministic enough that the client - facing server ca n't possibly express the expanded ClientHelloInner in a way that breaks the transcript ? 
 It 's decode , not encode , that needs to be deterministic . And yes , it is deterministic . 
 Here 's what I ended up writing to get this working . I do n't have  support done yet , so I expect these comments will change a little bit : 
 Which transcript is used should be better clarified now that landed . Closing as such . ( If we want to track test vectors for ECH , let 's open a new issue . ) 
 Thanks for capping the amount of changes in this PR :)
Paragraph (version A):
Upon receiving the server's response, the client determines whether
or not ECH was accepted (handle-server-response) and proceeds with
the handshake accordingly.  When ECH is rejected, the resulting
connection is not usable by the client for application data.
Instead, ECH rejection allows the client to retry with up-to-date
configuration (rejected-ech).
Paragraph (version B):
Upon receiving the server's response, the client determines whether
or not ECH was accepted (determining-ech-acceptance) and proceeds
with the handshake accordingly.  When ECH is rejected, the resulting
connection is not usable by the client for application data.
Instead, ECH rejection allows the client to retry with up-to-date
configuration (rejected-ech).
Paragraph (version A):
It MUST include an "encrypted_client_hello" extension with a
payload constructed as described in encrypting-clienthello.
Paragraph (version A):
If constructing the second ClientHelloOuter (client-hrr), it reuses
the encryption context computed for the first ClientHelloOuter, and
sets "enc" to the empty string.  Note that the HPKE context maintains
a sequence number, so this operation internally uses a fresh nonce
for each AEAD operation.  Reusing the HPKE context avoids an attack
described in flow-hrr-hijack.
Paragraph (version A):
The client then computes ClientHelloOuterAAD (authenticating-outer)
by constructing a ClientHello with all other extensions determined as
in real-ech.
Paragraph (version B):
Finally, the client encrypts the EncodedClientHelloInner with the
above values, as described in encrypting-clienthello, to construct a
ClientHelloOuter.  It sends this to the server, and processes the
response as described in determining-ech-acceptance.
Paragraph (version B):
Given an EncodedClientHelloInner, an HPKE encryption context and
"enc" value, and a partial ClientHelloOuterAAD, the client constructs
a ClientHelloOuter as follows.
Paragraph (version A):
As described in server-behavior, the server MAY either accept ECH and
use ClientHelloInner or reject it and use ClientHelloOuter.  In
handling the server's response, the client's first step is to
determine which value was used.
Paragraph (version A):
If the server replied with a HelloRetryRequest, then the client
proceeds as described in client-hrr.  Otherwise, if the server
replied with a ServerHello, then the client checks if the last 8
bytes of "ServerHello.random" are equal to "accept_confirmation" as
defined in backend-server.  If so, then it presumes acceptance.
Otherwise, the client presumes rejection.
Paragraph (version A):
If the server used ClientHelloInner, the client proceeds with the
connection as usual, authenticating the connection for the true
server name.
Paragraph (version A):
If the server used ClientHelloOuter, the client proceeds with the
handshake, authenticating for ECHConfig.contents.public_name as
described in auth-public-name.  If authentication or the handshake
fails, the client MUST return a failure to the calling application.
It MUST NOT use the retry configurations.
Paragraph (version B):
As described in server-behavior, the server may either accept ECH and
use ClientHelloInner or reject it and use ClientHelloOuter.  This is
determined by the server's initial message.
Paragraph (version B):
If the message does not negotiate TLS 1.3 or higher, the server has
rejected ECH.  Otherwise, it is either a ServerHello or
HelloRetryRequest.
Paragraph (version B):
If the message is a ServerHello, the client computes
"accept_confirmation" as described in backend-server.  If this value
matches the last 8 bytes of "ServerHello.random", the server has
accepted ECH.  Otherwise, it has rejected ECH.
Paragraph (version B):
If the message is a HelloRetryRequest, the client checks for the
"encrypted_client_hello" extension.  If none is found, the server has
rejected ECH.  Otherwise, if it has a length other than 8, the client
aborts the handshake with a "decode_error" alert.  Otherwise, the
client computes "hrr_accept_confirmation" as described in backend-
server-hrr.  If this value matches the extension payload, the server
has accept ECH.  Otherwise, it has rejected ECH.
Paragraph (version B):
If the server accepts ECH, the client handshakes with
ClientHelloInner as described in accepted-ech.  Otherwise, the client
handshakes with ClientHelloOuter as described in rejected-ech.
Paragraph (version B):
The client behaves as if it had sent ClientHelloInner as the
ClientHello.  That is, it evaluates the handshake using the
ClientHelloInner's preferences, and, when computing the transcript
hash (Section 4.4.1 of RFC8446), it uses ClientHelloInner as the
first ClientHello.
Paragraph (version B):
It computes a second ClientHelloInner based on the first
ClientHelloInner, as in Section 4.1.4 of RFC8446.  The
ClientHelloInner's "encrypted_client_hello" extension is left
unmodified.
Paragraph (version B):
It constructs EncodedClientHelloInner as described in encoding-
inner.
Paragraph (version B):
It constructs a second partial ClientHelloOuterAAD message.  This
message MUST be syntactically valid.  The extensions MAY be copied
from the original ClientHelloOuter unmodified, or omitted.  If not
sensitive, the client MAY copy updated extensions from the second
ClientHelloInner for compression.
Paragraph (version B):
It encrypts EncodedClientHelloInner as described in encrypting-
clienthello, using the second partial ClientHelloOuterAAD, to
obtain a second ClientHelloOuter.  It reuses the original HPKE
encryption context computed in real-ech and uses the empty string
for "enc".
Paragraph (version B):
The HPKE context maintains a sequence number, so this operation
internally uses a fresh nonce for each AEAD operation.  Reusing
the HPKE context avoids an attack described in flow-hrr-hijack.
Paragraph (version B):
The client then sends the second ClientHelloOuter to the server.
However, as above, it uses the second ClientHelloInner for
preferences, and both the ClientHelloInner messages for the
transcript hash.  Additionally, it checks the resulting ServerHello
for ECH acceptance as in determining-ech-acceptance.  If the
ServerHello does not also indicate ECH acceptance, the client MUST
terminate the connection with an "illegal_parameter" alert.
Paragraph (version B):
If the server rejects ECH, the client proceeds with the handshake,
authenticating for ECHConfig.contents.public_name as described in
auth-public-name.  If authentication or the handshake fails, the
client MUST return a failure to the calling application.  It MUST NOT
use the retry configurations.
Paragraph (version A):
When the server sends a HelloRetryRequest, the client checks for the
presence of an "encrypted_client_hello" extension.  If none is found,
then the client presumes rejection and handles the HelloRetryRequest
using ClientHelloOuter.  (Note that the client-facing server does not
send this extension in its HelloRetryRequest.  [[NOTE: This may
change, depending on the outcome of issue#450.]]) Otherwise it
proceeds as follows.
Paragraph (version A):
If the extension's payload has a length other than 8, then the client
aborts the handshake with an "decode_error" alert.  If the payload
length is equal to 8, then the client checks if the payload is equal
to "hrr_accept_confirmation" as defined in backend-server.  If so,
then it presumes acceptance and handles handles the HelloRetryRequest
using ClientHelloInner.  Otherwise, it presumes rejection.
Paragraph (version A):
The client encodes the second ClientHelloInner as in encoding-inner,
using the second ClientHelloOuter for any referenced extensions.  It
then encrypts the new EncodedClientHelloInner value as a second
message with the previous HPKE context as described in encrypting-
clienthello.
Paragraph (version A):
When sending a second ClientHello in response to a HelloRetryRequest,
the client copies the entire "encrypted_client_hello" extension from
the first ClientHello.
Paragraph (version A):
If the server sends an "encrypted_client_hello" extension, the client
MUST check the extension syntactically and abort the connection with
a "decode_error" alert if it is invalid.  It otherwise ignores the
extension and MUST NOT use the retry keys.
Paragraph (version B):
If sending a second ClientHello in response to a HelloRetryRequest,
the client copies the entire "encrypted_client_hello" extension from
the first ClientHello.
Paragraph (version B):
If the server sends an "encrypted_client_hello" extension in either
HelloRetryRequest or EncryptedExtensions, the client MUST check the
extension syntactically and abort the connection with a
"decode_error" alert if it is invalid.  It otherwise ignores the
extension.  It MUST NOT save the "retry_config" value in
EncryptedExtensions.