Subject: [TLS] ECH/ESNI - is accept confirmation calculation brittle in the face of errors?

From: Stephen Farrell <[email-masked]>
Date: Wed, 17 Mar 2021 02:06:06 +0000

Hi all,

I raised a github issue [1] related to (but different from)
this but would prefer a discussion via email if that's ok.
Depending how this goes, I can create a new GH issue I guess.

ECH draft-09 (and -10) requires the client to verify that the
server has processed ECH by comparing 8 octets in the
ServerHello.random with a value one can only know if you know
the inner CH octets and (pretty much all) the rest of the
h/s so far. That seemingly removes the need for the client to
maybe decrypt twice (once based on inner CH, once based on
outer) to see what works. That can be implemented ok for
the nominal case. As far as I can see that was the result of
github issue #274 [2] back in Aug/Sep 2020.

But in error cases that seems to require the client to
possibly process the same ServerHello extensions twice,
which could have side effects, is generally ickky and
I think shows that this aspect of the design maybe needs a
change. That's not much different from trial decryption
really. In my current, (and still horrible;-), OpenSSL code
the minimal effect is memory leaks for error cases that are
tricky to handle, but can be handled, although at the expense
of probably making the code even more brittle. (Note: I'm
neither defending nor attacking the way OpenSSL handles
extensions here:-)

The specific problem is one needs the handshake secret to
calculate the 8 octets one expects in the ServerHello.random,
but generating the handshake secret typically involves a key
share ServerHello extension and might in future involve some
PQC stuff, so I think we can't say for sure which subset of
extensions are needed for the calculation. And if the first
time doesn't give the right answer (e.g. if the client used
the wrong ECHConfig) then you gotta process the ServerHello
incl. its extensions a 2nd time.

I think the requirement we ought try meet here is that any
confirmation value needs to be something the client can
calculate using values known before the SH is rx'd plus the
ServerHello as a (mostly) unprocessed buffer. That could be
e.g. some hash of the encoded inner CH, ECHConfig and the
ServerHello maybe. Some of those issues were discussed in [2]
already and in an interim meeting I missed but I reckon it
might be worthwhile thinking again.

Thanks,
S.

[1] https://github.com/tlswg/draft-ietf-tls-esni/issues/399
[2] https://github.com/tlswg/draft-ietf-tls-esni/issues/274

---

From: David Benjamin <[email-masked]>
Date: Wed, 17 Mar 2021 09:52:07 -0400

Oh, that is a good observation.

If the client has the same ServerHello-sensitive preferences (version, key
share, supported groups, cipher suites, PSKs) between inner and outer
ClientHello, it doesn't need to reprocess. But if it has different
preferences, it needs to resolve this circular dependency. Most of the
ServerHello-sensitive preferences are actually currently required to match,
otherwise HRR doesn't work, but hopefully we'll fix HRR and remove this
awkward restriction. But the key word is *most* because PSKs do differ
between the two. It would be weird if the client could be tricked into
completing an outer ClientHello handshake with an inner ClientHello PSK. I
suspect it's "merely" weird, but we should not so easily get
implementations into self-inconsistent states.

The stuff in ServerHello is sufficiently constrained that this
is probably not fatal. Realistically, the two handshakes are sufficiently
intertwined on the client at this point that it'll probably need to be
careful about the inner/outer point regardless. But I agree it's not great
and worth looking at again. I think the mechanism in
https://github.com/tlswg/draft-ietf-tls-esni/pull/287 did not have this
circular dependency, but it was introduced in
https://github.com/tlswg/draft-ietf-tls-esni/pull/353.

I forget, did we need to bind it to the actual handshake secret, or was the
transcript and ClientHelloInner.random sufficient? That would avoid the
circular processing dependency.

David

On Tue, Mar 16, 2021 at 10:06 PM Stephen Farrell <[email-masked]>
wrote:

> _______________________________________________
> TLS mailing list
> [email-masked]
> https://www.ietf.org/mailman/listinfo/tls
>

---

From: Christopher Patton <[email-masked]>
Date: Thu, 18 Mar 2021 07:35:43 -0700

> I forget, did we need to bind it to the actual handshake secret, or was
> the transcript and ClientHelloInner.random sufficient? That would avoid the

As I recall, it was decided to bind the acceptance signal to the handshake
signal in order to mitigate some specific, active, "don't-stick-out"
attacks.

Chris P.

---

From: Christian Huitema <[email-masked]>
Date: Thu, 18 Mar 2021 09:55:08 -0700

> signal in order to mitigate some specific, active, "don't-stick-out"

The specific suggestion to tie the acceptance signal to the handshake 
secret was made I believe to simplify the security analysis. There are a 
variety of attacks in which a man in the middle can play games with 
specific extensions. Tying the signal to the handshake secret provides a 
robust defense against such games, and simplifies the analysis of the 
security properties. It also has nice 'don't stick out' properties, but 
those are not the only reasons.

-- Christian Huitema

---

From: Stephen Farrell <[email-masked]>
Date: Thu, 18 Mar 2021 17:24:01 +0000

Hiya,

On 18/03/2021 16:55, Christian Huitema wrote:
> specific extensions.

I missed the bit that allowed extension-specific games - can
you point me at that?

> Tying the signal to the handshake secret provides a 

And one consequence is that you end up doing more or less
all the work of trial decryption to handle the case where
the wrong ECHConfig was used by the client. If we do stick
with wanting that binding then I think it may be cleaner to
actually just do the trial decryption (of an encrypted
extension like the earlier nonce) and then declare victory
or fallback to checking the outer after that - at least in
that case one can expect to be able to use existing code more
easily and with less liklihood of breaking things or leaving
leaks.

Or, and maybe even better, think again about those attacks
and see if they can be avoided without the handshake-secret,
or if they still seem compelling enough to justify the awkward consequences.

Cheers,
S.

PS: An aside - I reckon it's too late to try not stick out
with the server hello - the client hello has given the game
away lready. That's a bummer really but I think it's game-
over for that entry on the wishlist with ECH.

---

From: Christian Huitema <[email-masked]>
Date: Thu, 18 Mar 2021 11:54:47 -0700

On 3/18/2021 10:24 AM, Stephen Farrell wrote:
>> secret was made I believe to simplify the security analysis. There 
>> are a variety of attacks in which a man in the middle can play games 
>> with specific extensions.
>
>> Tying the signal to the handshake secret provides a robust defense 
>> against such games, and simplifies the analysis of the security 
>> properties. It also has nice 'don't stick out' properties, but those 
>> are not the only reasons.
>

I agree that you have a strong argument there, Stephen, one that was not 
developed during the discussion on the mailing list and on GitHub. If I 
paraphrase, trial decryption has two big components: first computing the 
master secret based on the trial hypothesis, and then deriving the 
handshake key and verifying whether it actually decrypts the server's 
message. You measure that most of the work is done when "computing the 
master secret". If the hint depends on the master secret, then there is 
little difference in cost between "computing the master secret and 
checking the hint" versus "computing the master secret and trying to 
decrypt the message". We could just as well do trial decryption.

Well, OK. That's exactly the kind of things that we learn from doing 
implementations. That means we should either go for a much simpler hint 
of acceptance, or just be happy with trial decryption. There were a 
bunch of options reviewed in GitHub issue #274 
(https://github.com/tlswg/draft-ietf-tls-esni/issues/274). Should we 
reopen that discussion?

> or if they still seem compelling enough to justify the awkward 

I think that the requirements are to (1) make sure that the hint proves 
that the server properly decoded the ECH data and (2) make sure that the 
hint is different for every attempt. The embedding in the server random 
is probably the only option, given that TLS 1.3 very much restricts the 
number of unencrypted extensions in the Server Hello.

> over for that entry on the wishlist with ECH.

Yes. We have to rely on effective greasing. I don't have a great 
confidence in that.

-- Christian Huitema

---

From: David Benjamin <[email-masked]>
Date: Thu, 18 Mar 2021 15:17:48 -0400

> (https://github.com/tlswg/draft-ietf-tls-esni/issues/274). Should we

I don't think I'd agree that *most* of the work is in the secret
computation per se. Actually doing trial decryption with the secret
requires reaching down into the record layer. This is especially onerous
for QUIC, where the record layer is offloaded to another protocol (often
outside the TLS library). I think going back to full trial decryption would
still considerably increase the cost of implementing ECH over the current
draft.

That said, I do agree that even the first part seems more onerous than
expected and it's worth reconsidering things. We switched using just the
randoms to the full handshake secret to mitigate some issues. Perhaps
there's an intermediate point that still mitigates those issues but avoids
the expensive part of the secret computation?

What if we kept the ClientHelloInner...ServerHelloECHConf transcript as in
the current draft, but derived the signal exclusively from that? (Or from
the client random and that transcript, though the transcript includes the
client random anyway.) We'll still bind in the ServerHello extensions, and
it still includes the encrypted inner ClientHello random, as in the randoms
formulation. It doesn't do the job as clearly as the handshake secret did,
but maybe it'll work? Implementation-wise, this variant would not require
actually processing the ServerHello extensions.

David

---

From: Stephen Farrell <[email-masked]>
Date: Thu, 18 Mar 2021 20:07:22 +0000

Hiya,

On 18/03/2021 19:17, David Benjamin wrote:
 > computation per se. Actually doing trial decryption with
 > the secret requires reaching down into the record layer.
 > This is especially onerous for QUIC, where the record layer
 > is offloaded to another protocol (often outside the TLS
 > library).

Ah, fair point - I've not looked at QUIC at all. But how
are encrypted extensions and tickets handled in such cases?
If those are handled in the original library then a trial
decryption should be possible too maybe?

> What if we kept the ClientHelloInner...ServerHelloECHConf transcript
> as in the current draft, but derived the signal exclusively from
> that? (Or from the client random and that transcript, though the
> transcript includes the client random anyway.) We'll still bind in
> the ServerHello extensions, and it still includes the encrypted inner
> ClientHello random, as in the randoms formulation. It doesn't do the
> job as clearly as the handshake secret did, but maybe it'll work?
> Implementation-wise, this variant would not require actually
> processing the ServerHello extensions.

Right - if that's ok security-wise then it'd be fine for
implementation, better than either the current approach
or full trial decryption. We could I suppose consider just
keeping the current expand/extract construct but replacing
the handshake-secret with something else based just on that
same input buffer.

Cheers,
S.

---

From: David Benjamin <[email-masked]>
Date: Fri, 19 Mar 2021 17:58:55 -0400

> decryption should be possible too maybe?

QUIC replaces the record layer and keeps the TLS handshake. So as the TLS
handshake progresses, it hands keys (handshake traffic keys, etc.), to
QUIC to install. When TLS needs to write an EncryptedExtensions or so, it
hands QUIC cleartext to encrypt. When QUIC decrypts a record and sees a
CRYPTO frame, it hands the results to TLS to process as handshake messages.
The draft is here:
https://datatracker.ietf.org/doc/draft-ietf-quic-tls/

In all the stacks I'm aware of, there are QUIC-specific APIs to install a
custom record layer of sorts. And thus the nuisance with trial decryption
and friends. Every new feature we push down to the record layer changes
that record layer interface. This means more coordination between QUIC and
TLS. Something like trial decryption would require TLS hand QUIC both sets
of handshake keys. Then QUIC would need to do the trial decryption and
report back to TLS which set of keys won.

(If you're working on an OpenSSL ECH implementation, you probably won't see
this because OpenSSL doesn't have APIs for QUIC yet.)

David

---

From: Christian Huitema <[email-masked]>
Date: Fri, 19 Mar 2021 16:59:38 -0700

On 3/19/2021 2:58 PM, David Benjamin wrote:
> On Thu, Mar 18, 2021 at 4:07 PM Stephen Farrell<[email-masked]>
> wrote:
>
>> Hiya,
>>
>>> I don't think I'd agree that*most*  of the work is in the secret

That, or try race two connection contexts corresponding to the two 
trials, and keep the first that succeeds. Which would indeed require TLS 
to tell QUIC that "the key might be K1 or K2". That's also very 
complicated. For the QUIC client, the current text is much better.

Let's come back to the original discussion. If I look back at the 
discussion of issue 274 
(https://github.com/tlswg/draft-ietf-tls-esni/issues/274), we ended up 
at some point with a set of alternatives, including these two:

     * accept_confirmation = PRF(ClientHelloInner.random, 
ServerHello.random[0:24])
     * accept_confirmation = PRF(ClientHelloInner.random, 
ServerHello.KeyShare)

The issue was closed by PR #287 
(https://github.com/tlswg/draft-ietf-tls-esni/pull/287/files) which 
selected the first of these two options. But this was later updated by 
PR #353 (https://github.com/tlswg/draft-ietf-tls-esni/pull/353), which 
changed that to the second option, the one including the server key 
share. The correction allows mitigating an attack in which the 
interceptor copies the server hello but substitutes its own key share, 
and thus can test whether ECH was used.

Look at the comment, "This change is definitely more invasive than the 
existing signal, but not all that hard to implement. (E.g., 
cloudflare/go#38 <https://github.com/cloudflare/go/pull/38>.)" later in 
the thread.

We do have new information that this is somewhat costly to implement 
because it requires computing two handshake secrets on the client. On 
the other hand, it seems that the cost is only there if the client 
starts by testing the wrong hypothesis. If the client is fairly certain 
that the server supports ECH, the CPU overhead ought to be minimal -- 
cases in which the client is using the wrong ECH key, or when an 
interceptor interfered with the exchange.

-- Christian Huitema

---

From: Stephen Farrell <[email-masked]>
Date: Sat, 20 Mar 2021 00:22:47 +0000

Hiya,

I agree with your analysis except for the very last part...

> interceptor interfered with the exchange.

The CPU overhead isn't a problem from my POV.

The problem is that the case where the wrong ECH was used is
kinda like doing 1.5 handshakes. And as/if new ServerHello
extensions come into play (esp PQC), or when we deal with old
code interacting with but unrelated to ECH, or with custom-
extension handling code that we'll never see, or with some
other random change to the transcript, any of that might
expose the brittleness of a 1.5 handshake design.

As it happens, I've fixed the memory leaks this error case
causes but its still messy - someone changing some unrelated
bit of OpenSSL code in a year or two for some other reason
could make my clean-up code break something in unpredictable
ways. (Should my code end up upstream etc. etc.)

So, quoting you out of order:

 > Look at the comment, "This change is definitely more >
 > invasive than the existing signal, but not all that hard to
 > implement. (E.g., cloudflare/go#38 <https://github.com
 > /cloudflare/go/pull/38>.)" later in the thread.

I think the right thing here would be to analyse that attack
again and re-evaluate which of the two answers now seems
best. For me, the github issue discussion didn't leave
behind enough information to do that. (Or I need to stare
at it some more maybe:-)

Cheers,
S.

---

From: Christopher Patton <[email-masked]>
Date: Tue, 23 Mar 2021 15:52:41 -0700

> behind enough information to do that. (Or I need to stare

If we constrain the model so that the "don't stick out" attacker is
passive, then PR#287 (https://github.com/tlswg/draft-ietf-tls-esni/pull/287
suffices). The attack described at the top of PR#353 (
https://github.com/tlswg/draft-ietf-tls-esni/pull/353) is what led everyone
to agree that the acceptance confirmation needed to be derived from the
secret shared by the client and backend server. It's an active "don't stick
out" attack. Basically, the attacker replies to the client's CH with a
bogus SH; and by observing the client's reply, it can learn whether the
client sent real or GREASE ECH. There are a number of variants of this
basic attack, some of which can be pulled off without actually tipping off
most clients.

If we intend to thwart active "don't stick out" distinguishers, then PR#353
seems necessary to me. The key difference between PR#287 and PR#353 is
that, in the latter, the secret used to derive the acceptance signal is
authenticated. This isn't true in the former. I don't think trying to find
a middle ground between PR#353 and PR#287 will be fruitful, unless we can
hack in backend-server authentication by some means other than the
handshake signature.

Chris P.

---

From: Stephen Farrell <[email-masked]>
Date: Tue, 23 Mar 2021 23:48:30 +0000

Hiya,

> If we constrain the model so that the "don't stick out" attacker is

The attack is more one that allows the adversary to
distinguish a real ECH attempt vs GREASE, right? So
we need to start out assuming that GREASEd CH's are
fairly well done. (Which is fine but might need to
be explained some more in the draft maybe.)

> client sent real or GREASE ECH. 

Doesn't that imply that the 2nd flight message sizes, and
the timing of producing (and consuming) those, mustn't allow
the observer to distinguish the reaction to a real ECH vs. a
GREASEd one?

That may be a bit of an aside, but it may also mean that if
we don't believe we can get constant time such that the
passive attack here fails, then we also aren't really that
worried about these active attacks. I'm also not sure we yet
know how to use padding so that sizes don't give away the
game. (I know I'm not yet sure about that for sure:-)

> There are a number of variants of this

Ack. And maybe there are other active attacks that could
distinguish a real ECH attempt vs. GREASE based on the
adversary interacting with the server - mightn't e.g. the
timing of handling a simple replay of the ECH octets reveal
if those were GREASE or not?

> hack in backend-server authentication by some means other than the

You may well be right. It is hard to see a possible middle
ground. There is also the 3rd option of the original use of
a nonce in EncryptedExtensions - we might or might not
discard that due tothe QUIC argument raised earlier I guess.

S.

---

From: Christian Huitema <[email-masked]>
Date: Tue, 23 Mar 2021 20:52:26 -0700

On 3/23/2021 4:48 PM, Stephen Farrell wrote:
>> passive, then PR#287 
>> (https://github.com/tlswg/draft-ietf-tls-esni/pull/287

Yes, a secret dependent nonce will also be able to verify that the 
server has processed the ECH. But the nonce can only be sent as an 
encrypted extension. That's fairly late in the process. That might work 
for TLS over TCP, because the client can read the entire flight before 
sending any information to the server. But a QUIC client cannot really 
"remain silent until it has seen the nonce", it may have to send some 
transport level acks in between. In the attack case, if the hint in the 
HelloRandom matches the ECH case, the client will assume an ECH 
handshake key, and it will not be able to decrypt the QUIC Handshake 
packets sent by the attacker. The attacker will be able to detect that, 
and thus distinguish ECH from !ECH.

-- Christian Huitema