Draft: draft-ietf-tls-esni

Commentary: Based on ( review that first ! ) . If an HRR is sent in response to CH , the client does n't know whether ECH was accepted . As a result , it does n't know whether it 's CHI or CHO that needs to be updated . The spec attempts to address this ambiguity by requiring that all HRR - sensitive parameters are the same in CHI and CHO . This criterion is currently underspecified , and it 's not clear how to make sure that future extensions do n't violate it . ( See issue . ) This change solves this ambiguity by adding an explicit signal of ECH acceptance to  . This violates " do n't stick out " whenever an HRR is inevitable for a particular client and server configuration . However , this change is designed to prevent an active attacker from distinguishing real ECH from GREASE ECH by triggering  . by providing an alternative to PR . 
 Rebased . 
 Squashed and rebased . 
 I wanted to jot this down before the holidays , in advance of writing a more rigorous PR . This is a proposal to go fix all our HRR problems in one swoop ( , , , and ) . First , whatever our " do n't stick out " goals are ( ) , sticking out on true  flows is pretty much hopeless if the attacker is allowed to make a second connection and probe server behavior : replay the first ClientHello and then perturb the second ClientHello 's ciphertext . The server ca n't change its mind after the fact . So I 'm going to intentionally give up on that , which means fixing is possible . The proposal is thus : The client sends ClientHelloOuter1 and  as in the current document . If client - facing server accepts ECH , it forwards  to the backend server , as before . If the backend server sends HelloRetryRequest in response , call this HelloRetryRequestInner . The backend server constructs HelloRetryRequestOuter containing the following : An encryptedclienthello extension with the HelloRetryRequestInner , encrypted with the HPKE context ( see open questions ) . Optionally , a cookie extension with the outer cookie ( No other extensions are allowed , though future extensions , if solicited by ClientHelloOuter1 may change this . ( See open questions . ) HelloRetryRequests signals to the client whether ECH was accepted or not ( avoids ) by the presence of the encryptedclienthello extension : If there is no encryptedclienthello extension , the client computes ClientHello2 based on ClientHelloOuter1 and HelloRetryRequest . There is no more ClientHelloInner2 . We 're not doing ECH , and  may not even be valid for  ( the mess ) . Instead , it blindly copies over ClientHelloOuter1 's extension . This is a waste of bytes , but If there is an encryptedclienthello extension ... First , the client decrypts HelloRetryRequestInner . If it fails , it sends decryptfailure and aborts . GREASE ECH is considered to always fail to decrypt and always sends decryptfailure . Otherwise , the client computes ClientHelloInner2 based on HelloRetryRequestInner in the usual way , handling errors in the usual way . The client constructs ClientHelloOuter2 containing the encrypted ClientHelloInner2 and We know the server is accepting ECH at this point , so there is no need to send much of any other fields . See open questions below .  or not , the ServerHello is required to have an ECH acceptance confirmation as usual . The client processes it as follows : If there was no  , do what the document currently says and figure out ECH acceptance here . If there was an HRR , you already know ECH accept or reject . If ECH reject , move and and complete the outer handshake without any fuss . The inner handshake terminated too early to compute handshakesecret anyway . If ECH accept , check the acceptance confirmation and send illegalparameter or so if it 's wrong . ( See open questions below ) I believe this solves our various HRR problems : The client - facing server can send an HRR cookie without coordinating with the inner handshake ( ) . The client does not need to evaluate one HRR against two different ClientHellos , so the two ClientHellos can have arbitrarily different preferences ( , ) . The client knows ECH accept at the first server response , so it does not need to construct two different second ClientHellos ( ) . The ECH extension only changes when solicited ( ) . Thoughts ? The above sketch encrypts HRRInner , but HPKE only allows the sender to encrypt . This is easy to specify though : use the exporters to derive another AEAD key and toss it into the HPKE cipher suite 's AEAD . As for why encrypt it , things should still work if we stick it in the clear . But we may want to for two reasons : backend server tickets , backend server cookies are , today , not particularly constrained w.r.t . what they reveal . A ticket or cookie from one TLS implementation may be different from the other , and   servers may have multiple backend implementations in their anonymity set . It may also vary for other reasons . NewSessionTicket is encrypted , as is the presharedkey extension , so we 've solved it for tickets . is also encrypted , which leaves Encrypting HRRInner fills that in . HRRInner is not authenticated by the HPKE context , an attacker can try to distinguish GREASE by sending a fake cleartext HRRInner and seeing how the client responds . Authenticating HRRInner means you can not spoof an inner  flow . Whether the second property is worthwhile is unclear ( ) , but I think the first property is valuable , at which point we get the second anyway . This design avoids by assembling a cursory ClientHelloOuter2 , rather than actually trying to continue the outer handshake . We would need to decide how to do that . I can think of a few possibilities : specify throwaway values for all the non - extension fields and declare ClientHelloOuter2 contains exactly two extensions , an optional cookie and encryptedclienthello . EncodedClientHelloInner2 does not bother with outerextensions because there is nothing to compress . up a new message type to avoid the throwaway fields . I do n't like option 3 . It 's conceptually kinda clever , but adds unnecessary fuss to the protocol . Consider a client which sends CHO1.keyshare = = CHI1.keyshare and compresses them . Key share preferences are rarely secret and this avoids doubling up potentially large post - quantum values . The backend server now requests  because it wanted a different key share . So CHI2.keyshare ! = CHI1.keyshare and is based on HRRI.keyshare . But the client - facing server probably did the straightforward thing and did n't send HRRO.keyshare because there 's no point . It 'll never process CHO2 . Option 3 would then require that CHO2.keyshare = = CHO1.keyshare because HRRO did not request a change . That means CHO2.keyshare ! = CHI2.keyshare and the client must waste a lot of bytes sending two of them . In contrast , options 1 and 2 say that CHO2.keyshare does n't exist anyway , so we only ever send one key share . It also adds implementation complexity to the client , which now needs to evaluate two different full - featured HRRs in one handshake , despite it being unused . Option 1 and 2 are basically equivalent , though it 's worth noting that HRRO and CHO2 do not quite have the same rules as anymore , so whether we want to stick them in the same message type is interesting ... After  , the acceptance signal does n't do anything . We could just as easily omit it , because the client has already gotten the signal . The above sketch keeps it in , mostly for consistency 's sake : right now we require that backend servers support the echisinner extension . Getting this wrong will be noticed because the client will not successfully complete the connection . If we omit the acceptance signal in  flows , the client will no longer consistently notice . As long as we have this requirement , enforcing it uniformly is more robust , so I 'm thinking we keep it in there . This sketch says to copy CH2.encryptedclienthello on ECH reject . This is a waste of bytes but retains compatibility with RFC8446 's rules . In particular , we know of a compatibility issue in . It seemed from the WG discussion we generally think the RFC8446 rules are wrong , so we could intentionally break them and drop the extension . My inclination is we should try to get the rule changed and implementations fixed , but maybe that should happen separately from ECH and we copy it for now . 
 I 'm curious what your thoughts are . I remember you said at the WG meeting you wanted to avoid in the final design . 
 I 'll need to stew on this for a bit , but I support this idea overall . One request : It seems like some design decisions are aimed at reducing the communication overhead compared to ECH-09 . For now , I would prefer we leave out of t he PR any such optimization that adds implementation complexity . In particular , I think the client should compute a real ClientHelloOuter2 as usual ( as in option ( 3 . ) above ) rather than do something different ( as in option ( 1 . ) or ( 2 . ) ) . The alternatives seem somewhat invasive ; and if their only benefit is improved communication overhead , then given how rare the HRR code path is in practice , I do n't think it 's worth doing . Also , I 'm sold on the privacy benefits of encrypting HRRInner . However , I think we should punt this to a future PR . Two reasons . First , I think this privacy consideration is orthogonal to the issue at hand . Second , the details are n't straight - forward ; we 'll have to agree on how to pick the nonce , associated data , how to pad , and so on . I think we 'll do ourselves a big favor by untangling these things :) 
 The issue with option 3 is n't just communication overhead . It 's also implementation cost in mocking out an outer handshake on the client , even though we already know that it will be ignored . Either of options 1 or 2 would be dramatically simpler to implement and carry lower risk of mishaps from the complexity . 
 I 've updated the description in the PR to make this clearer . The only benefit of option 3 is as a conceptual reference or perhaps something to profile down from . As an actual protocol , it 's a not a good option . 
 I do n't really understand this . Sure , the client gets the signal that the backend server sent the HRR , but without , how can the client be sure the client - facing server did n't send the SH ? The handshake will ultimately fail because the transcripts wo n't match , but not until then . Is n't it better if the client is able to terminate immediately after receiving the SH ? Wait why would the client - facing server send anything at all in this case ? Does n't the backend server send the HRR to the client without ( ideally ) any modification from the client - facing server ? 
 After  , the acceptance signal does n't do anything . We could just as easily omit it , because the client has already gotten the signal . Do you mean a malicious or well - behaved client - facing server ? A well - behaved client - facing server does n't get to change its mind about whether ECH was accepted partway through . If we 've got an HRR from the backend server , the SH will be from the backend server too . If it 's a malicious client - facing server , yeah , we 're relying on how TLS defends against network attackers , including how it uses the transcript . But that 's true of TLS in general . If we 're worried about when the client notices , the transcript figures into handshake encryption , CertificateVerify , and Finished , which are all in the same flight as ServerHello . ( Plus the signal is all of 8 bytes , so it 's not especially cryptographically significant . ) ( But I do n't think we should omit it anyway . Just observing that we could . ) The backend server now requests  because it wanted a different key share . So CHI2.keyshare ! = CHI1.keyshare and is based on HRRI.keyshare . But the client - facing server probably did the straightforward thing and did n't send HRRO.keyshare because there 's no point . It 'll never process CHO2 . Right . I 'm saying I do n't like option 3 because it allows the client - facing server to send HRROuter.key_share . That means the client must have code to handle it . And then we need to reason about how this mess interacts with not wanting to waste bytes in the second ClientHello sending two key shares . And , despite all that complexity , the client - facing server does n't actually care , so we made everything a mess for nothing . Thus , option 3 is a useful conceptual reference , but I do n't think we should actaully do it . 
 Ok , cool . Aha , ok . 
 This sounds good to me then . I 'm sold on encrypting the HRRInner to prevent cookie fingerprinting . 
 I 'm sold as well . I 'd say this issue is ready for text . 
 when do you think you can get to a PR for this ? 
 While attempting to draft a change for , I was confused by this part of section 6.1.4 : " 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 " When does the client learn whether the server processed ClientHelloOuter or  ? If the client sees that the last 8 bytes of were set to the first 8 bytes of , does it just assume that the first ClientHello that the server processed was a ClientHelloInner ? That seems like a big assumption . Could n't the following happen ? : If this can happen , the client will assume , after it receives the SH , that the server processed the first ClientHelloInner , but this is in fact not the case , and the transcript will break . I mean , I guess that 's okay if you do n't want this behavior to succeed , but AFAICT , the draft does n't explicitly say that the client - facing server should n't do this . Should it ? ( Sorry if this is in fact already resolved somewhere in the draft . Also I 'm pretty sure it 's a different issue from ) 
 Yeah , this is one of the HRR headaches . :-) We do n't know until SH whether CHO or CHI was used , so the second ClientHello needs to evaluate both CHO and CHI against HRR . patched the worst of the headache ( what if evaluating  is an error for CHO but not for CHI ? ) , but it 's a really awkward solution . ( I still need to write up the two HRRs solution we 've been muttering about . ) 
 Why ca n't the backend server just set the way it sets I believe it 'll be a different value in the HRR vs SH if we still care about sticking out , which we do n't . ;-) ( Or is that one of the two solutions ? ) 
 ( Oh sorry , by " two HRRs solution " I did n't mean two solutions for HRR , but a single solution involving two HRRs . :-) ) As part of the middlebox compatibility goofiness , a HelloRetryRequest message is encoded as a ServerHello message with a special fixed value for It 's kind of a mess . I suppose we could declare that the HRR indicator is now actually 24 bytes instead of 32 and then reapply the trick ? I dunno , given , I figure this ought to look like a HelloRetryRequest.encryptedclienthello extension ( as sticking out in  flows is hopeless ) . That then gives us room to solve and then ... yeah that is basically the solution I need to write up . :-) 
 Oh haha . Read that too fast . Where is this solution described ? I do n't see it in the open HRR issues . Oh . Is that an implementation detail or part of the spec ? I do n't see that in section 4.1.4 of RFC 8446 . 
 Nowhere yet . :-( It 's the thing we mumbled about at our last sync and I still need to write up a description . Looks like it 's spread out a bit . 4.1.4 says : And then 4.1.3 says : 
  , thanks . Would n't that break middleboxes ? SGTM 
 I just noticed this as I was pondering our various HRR issues . RFC8446 , section 4.1.2 says : The " and present in the HelloRetryRequest " portion is fun . In RFC8446 as written , we 're not allowed to change the extension on the second ClientHello unless the server sent in HelloRetryRequest , which it does n't . With the protocol as - is , it seems we 'll at least need some text to deal with the contradiction . The impact is practice is thankfully limited . If the servers does not enforce consistency between the two ClientHellos , it does n't matter . ( They 're not required to , but they 're not explicitly forbidden from it either , and a stateless server may need to enforce some minimum consistency to avoid getting into a broken state . ) If a server does , ECH 's behavior on HRR will break it . Such a server is supposed to handshake with ClientHelloOuter , so it just affects the retry flow . That 's maybe okay , but mildly annoying . Administrators using server software that checks need to know to first upgrade to a version that does n't check before trying to deploy ECH . We can also add an ECH extension to  . Then , on ECH - reject + HRR , the server perhaps would n't send the extension and the client would be required to replay the extension . This is a waste in many ways , but would work . This would break do n't stick out , but some threat models , do n't stick out with  is hopeless anyway . ( See ) It would also be weirdly asymmetric between ECH and SH . That said , this constraint is also pretty obnoxious . For instance , currently adds a new padding extension codepoint , because the old one is n't defined for other messages . However , the RFC8446 allowance only works for the existing padding code point , so you do n't want to send any other code points in ClientHello , lest the server force you to keep it unchanged in ClientHello2 . So maybe we should confirm no servers did the fully strict check and undo that constraint ? ( Although , for , I personally favor the padding message strategy anyway . ) 
 points out the Go implementation does check CH1 and CH2 consistency . Fortunately , it only checks known extensions , so it 's still compatible with the ECH draft . Checking only know extensions is basically saying we should strike " and present in the HelloRetryRequest " in rfc8446bis and make sure everyone knows not to check this . 
 I do n't think we can just strike this text because the point of that text was that you could n't like change your KeyShares if the HRR did n't tell you to . But I agree we can sharpen this in some sensible way . 
 I figured that case was covered by " defined in the future " , which I 'm not proposing we delete .  is n't defined in the future , so it falls under existing rules . The existing rules ( bullet points over that one ) only let you change keyshare " [ i]f a " key_share " extension was supplied in the HelloRetryRequest " . But perhaps that 's a little roundabout . The rules I 'm thinking of are ( changes emphasized ): Each extension gets to define how it reacts to HRR . It typically is sensitive the corresponding HRR extension , but does n't have to be . RFC8446(bis ) defines the rules for all existing extensions . Future extensions define their own rules . ( And probably if they do n't say anything , we assume they do n't get to change . ) As a server , you can make assumptions based on the rules for extensions you know about . You have to tolerate random changes in extensions you do n't know about , because you do n't know their rules . * I 'm running a probe on a bunch of servers to see how they react to changes in cipher suites , curves , and a random extension . It seems most do n't notice . Some ( I 'm guessing Go ) notice cipher suites and curves . None notice a random extension . That suggests the change will be compatible . 
 I spoke too soon . LibreSSL breaks if you change an extension they do n't recognize . :-( 
 So draft-09 is n't tied up with this , I 've uploaded as a bandaid for the potential compatibility issue . ( I say potential because it 's only a problem if the server checks unknown extensions and has different enough key share preferences from the client that it needs  .  is pretty rare , so it may not actually be an issue . For  , at least , I do n't think the particular servers I found actually trigger  . But it is still a risk for clients or servers with different configurations . )
Paragraph (version B):
If these checks succeed, the client-facing server then forwards the
ClientHelloInner to the appropriate backend server, which proceeds as
in backend-server.  If the backend server responds with a
HelloRetryRequest, the client-facing server forwards it, decrypts the
client's second ClientHelloOuter using the procedure in client-
facing-server-hrr, and forwards the resulting second
ClientHelloInner.  The client-facing server forwards all other TLS
messages between the client and backend server unmodified.
Paragraph (version B):
where HKDF-Expand-Label is defined in RFC8446, Section 7.1, "0"
indicates a string of Hash.length bytes set to zero, and Hash is the
hash function used to compute the transcript hash.
Paragraph (version B):
When the backend server sends HelloRetryRequest in response to the
ClientHello, it similarly confirms ECH acceptance by adding a
confirmation signal to its HelloRetryRequest.  But instead of
embedding the signal in the HelloRetryRequest.random (the value of
which is specified by RFC8446), it sends the signal in an extension.