Original text:
negotiating ECH, including servers which do not implement this specification.  7.2. When the client-facing server accepts ECH, it forwards the

Reviewer comments:
It is an error for the client to offer ECH after HRR but not before. Likewise, it is an error for the client to offer ECH before HRR but not after. This change ensures the server aborts in this case.
Suppose we hit one of the messy HRR cases (URL), where the HRR is valid for the outer ClientHello, but not the inner ClientHello. The client then cannot construct a valid inner ClientHello. How would that square with this text? Are you envisioning the client just generates a random ECH extension, or...?
I don't think this is a new problem. I see the PR as clarifying existing behavior. Can you please propose text for the HRR nightmare in ? :-)
Ah, you're right. Sorry, I misread this PR as introducing this requirement. In principle, we only need the server to require ECH in CH2 if it decided to accept ECH on CH1. That would allow the client to not send ECH the second time, but maybe we don't want to do that, depending on what we do for ... Do we know what we want to do for ? Simplest seems to be saying clients MUST align them. (Though how does that work with later HRR-sensitive extensions? Are funny ECH-adding proxies a thing?) Or maybe it's just a SHOULD? Or maybe we come up with some other protocol trick so ECH acceptance is visible at HRR (which would probably reopen the don't-stick-out can of worms yet again). Of these, only the MUST avoids collision with this PR's (existing) client requirement.
Yeah, that's my preference. This logic is already complicated enough. The primary pain point I see is enumerating (and prescribing) what is a HRR-sensitive field.
Same, with that pain point being the big question in my head. Whether an HRR is valid for a ClientHello isn't an implementation decision, so in theory it's enumerable. But more extensions may come in later. And in the other direction, even a very strong SHOULD means we need to pick a client behavior for the weird case and allow for it in server behavior.
Regardless of where we land on , this PR seems appropriate to me.
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 ClientHelloInner1 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 ClientHelloInner1 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 ClientHelloInner1 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 NAME 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 HRR 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 don't think resolves this. If the client has different HRR-relevant preferences (key share and cipher suite) between CHInner and CHOuter, I believe HRR is still a mess because the HRR message doesn't contain a signal for whether ECH was accepted. Though I suppose it is a little less complex now that you don'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. URL 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 HRR path aligns with ECH acceptance/rejection.
That doesn'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 HRR, you don't know if ECH was accepted. ciphersuite = TLSAES256GCMSHA384; keyshare = P-384 ciphersuite = TLSAES128CCMSHA256; keyshare = P-521 ciphersuite = TLSCHACHA20POLY1305SHA256; keyshare = X25519 ciphersuite = TLSAES128GCMSHA256; keyshare = P-256 ciphersuite = TLSCHACHA20POLY1305SHA256; keyshare = X25519 1 is valid for both ClientHellos. 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 shouldn't have sent HRR.) 4 is valid for only CHOuter, so you can'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 can'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 cipher_suites between CHInner and CHOuter. That means it can process HRR 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 wasn'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 can't do the same trick we did with the URL, 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 HRR) can be explicitly forbidden, but I don't see why/how.
Incidentally, this is addressed by URL 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 HRR 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.
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.

Revised text:
negotiating ECH, including servers which do not implement this specification. <ins> 7.1.1. 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 either of these conditions occurs, then the client-facing server MUST abort the handshake with an "illegal_parameter" alert. </ins> 7.2. When the client-facing server accepts ECH, it forwards the