Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packet number encryption #1079

Merged
merged 18 commits into from
May 3, 2018
Merged

Packet number encryption #1079

merged 18 commits into from
May 3, 2018

Conversation

martinthomson
Copy link
Member

@martinthomson martinthomson commented Jan 29, 2018

This is a little rough, but I think that the bones are here. I'm not all that proud of my analysis of the security of this hack, but I couldn't think of a way of disclaiming its value without also having it seem like we were throwing unlinkability for connection migrations under the bus.

I know that we discussed removing short packet types in favor of putting the lengths under encryption. I will do that separately, just to keep the size of this changeset to something more manageable.

Enables #598

packet numbers MUST be free from side-channels that would reveal the packet
number or its encoded size.


# Error Codes {#errors}
Copy link
Contributor

@mikkelfj mikkelfj Jan 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced this timing attack protection is worthwhile. There is no argument on how an attacker might do the timing. There is no immediately observable reply, and an ACK would not happen just because the packet was guessed. It could affect ACK of a valid packet and a powerful attack could delay valid packets to measure timing. But this must be balanced against the value of guessing the packet number. The value can lead to tracking a migration, but so can tons of other fingerprints.

In conclusion, requiring that a full AEAD validation is performed on essentially all invalid packet headers seems to be unreasonable. However, if it is important, I would MUCH rather prefer a short 16-bit header validation tag that can quickly reject any invalid header in case of a flood attack.

EDIT: of course a short header tag would also be subject to timing attacks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll second this. You can't tell from the network that a packet has been discarded, and opening yourself up to the attack of being forced to decrypt something you already know is either junk or useless doesn't seem worth it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew that this would raise eyebrows :)

Early discard doesn't give any better protection against denial of service. The space of valid packet numbers is >= 50% of the total expressible space, which - with the type outside of protection as it currently is - can be 2^32. That number doesn't go down with guesses. That means that an attacker can always force the receiver to do a full AEAD. Given that, the cost of full side-channel resistance is marginal.

As for observing the timing of a discarded packet, I agree that it is hard. Of course, there is always the next packet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also about being able to reject retransmissions of valid packets early, if you suddenly get a burst of such packets. Not sure how likely that is.

assumed to be its largest possible length (4 octets). Thus, for a short header,
the sampled ciphertext starts at either octet 5 when the connection ID is
omitted, or octet 13 when the connection is present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If packet number protection (for good reasons) is only applied after handshake, this prevents connection migration from looking like a new handshake unless there are special rules for packet numbering during a handshake. Hence, perhaps protetected packet numbers should have a separate numbering space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands now, connection migration in no way looks like a new connection, because no long form handshake packets are used on the new path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands now,

I was thinking a bit ahead - considering the open issue(s?) on the topic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a client-initiated PATH_CHALLENGE/PATH_RESPONSE should be long header packets? But that's totally orthogonal to this PR, I think. So long as the packet number is obscured in both header types, an observer can't tell whether a connection is restarting from zero or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial handshake needs to negotiate encryption and hence cannot encrypt those packets.

Perhaps the packet numbers ought to be encrypted with a version specific non-negotiably algorithm, but that hurts devices that doesn't speak AES natively. This is why I suggest each handshake and migration sequence uses unencrypted, from 0, packet numbers, unique to each connection ID.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest problem for having migration appear to be a new connection is that the packets won't decrypt with the handshake keys. This doesn't change any of that. @mikkelfj separately suggested that we use 0-RTT, and that could work, but it's a pretty big lift.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is true - in 0-RTT the packet numbers could be encrypted.

BTW: if 0-RTT doesn't actively take measures to avoid fingerprinting, that would be an excellent way to track users. This somewhat relates to the replay thing.

Copy link
Contributor

@MikeBishop MikeBishop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine.

plaintext, adding only an authentication tag. Therefore, this document assumes
that each bit of sampled AEAD output contains one bit of entropy and that an
attacker is unable to reduce this without knowledge of the key. Based on this
assumption, the odds of two samples of ciphertext approach the birthday bound
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like you're missing a word here, but I'm not sure what it should be.

protection algorithm might be used.

To prevent an attacker from modifying packet numbers, values of packet numbers
are transitively authenticated using the packet number protection. A falsified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was authenticated using the AD from the packet protection, not the packet number protection?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. Got used to typing packet number protection.

packet numbers MUST be free from side-channels that would reveal the packet
number or its encoded size.


# Error Codes {#errors}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll second this. You can't tell from the network that a packet has been discarded, and opening yourself up to the attack of being forced to decrypt something you already know is either junk or useless doesn't seem worth it.

number. All subsequent packets contain a packet number that is incremented by
one, see ({{packet-numbers}}).
The first Initial packet that is sent by a client contains a packet number of 0.
All subsequent packets contain a packet number that is incremented by one, see
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think elsewhere we've said "at least one" in order to allow for skipping the occasional packet number.

@@ -401,7 +401,8 @@ Version:
Packet Number:

: Octets 13 to 16 contain the packet number. {{packet-numbers}} describes the
use of packet numbers.
use of packet numbers. Packet numbers are protected separate to the packet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You use "separately from" on 679, which feels more natural. (Again, possible dialect difference.) Same on line 486-487.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, separately from feels more natural

@@ -709,8 +714,8 @@ packets MUST use connection ID selected by the client.
The packet number is an integer in the range 0 to 2^62-1. The value is used in
determining the cryptographic nonce for packet encryption. Each endpoint
maintains a separate packet number for sending and receiving. The packet number
for sending MUST increase by at least one after sending any packet, unless
otherwise specified (see {{initial-packet-number}}).
for sending MUST starts at zero for the first packet set and MUST increase by at
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/starts/start/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and s/set/sent

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, epic fail :)

Copy link
Contributor

@ianswett ianswett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: If we encode the packet number length inside the encryption, which seems nice, does the decrypter have to assume the longest possible packet number length, then check the first byte after decryption to determine the actual length?

@@ -401,7 +401,8 @@ Version:
Packet Number:

: Octets 13 to 16 contain the packet number. {{packet-numbers}} describes the
use of packet numbers.
use of packet numbers. Packet numbers are protected separate to the packet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, separately from feels more natural

@@ -709,8 +714,8 @@ packets MUST use connection ID selected by the client.
The packet number is an integer in the range 0 to 2^62-1. The value is used in
determining the cryptographic nonce for packet encryption. Each endpoint
maintains a separate packet number for sending and receiving. The packet number
for sending MUST increase by at least one after sending any packet, unless
otherwise specified (see {{initial-packet-number}}).
for sending MUST starts at zero for the first packet set and MUST increase by at
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and s/set/sent

@@ -866,6 +867,85 @@ the connection that is hosted on stream 0. This sequence number is not visible
to QUIC.


## Packet Number Protection {#pn-encrypt}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move to varint packet numbers inside encryption, determining the length of the varint with this approach seems not completely obvious. If we stick with this approach instead of the overlapping crypto operations, it would be good to add small section on removing packet number protection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: If we encode the packet number length inside the encryption, which seems nice, does the decrypter have to assume the longest possible packet number length, then check the first byte after decryption to determine the actual length?

Since the encryption is XOR based, it suffice to decrypt the first byte with XOR to decide the length, or, just the first two bits, actually. In praxis it might be simpler/faster to XOR with the maximum length, then read as necessary. Escpecially if the packet is guaranteed to be long enought and when fast unaligned 8 byte reads are available.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What @mikkelfj says. Say we choose an encoding with a 4 octet maximum length, then you copy 4 octets out, XOR those, then decode from that buffer. You then know the length and can discard the excess.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something like:

  1. Set a barrier at the start of the chosen ciphertext input.
  2. If the barrier is further than 8 bytes from the beginning of the packet number, XOR 8 bytes from beginning of packet number,
  3. If the barrier is closer than that, XOR up to the barrier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this isn't missed by people reviewing, I've taken this suggestion. That way we can make stronger assertions about birthday bounds.

Copy link
Member Author

@martinthomson martinthomson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick response.

@@ -866,6 +867,85 @@ the connection that is hosted on stream 0. This sequence number is not visible
to QUIC.


## Packet Number Protection {#pn-encrypt}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What @mikkelfj says. Say we choose an encoding with a 4 octet maximum length, then you copy 4 octets out, XOR those, then decode from that buffer. You then know the length and can discard the excess.

assumed to be its largest possible length (4 octets). Thus, for a short header,
the sampled ciphertext starts at either octet 5 when the connection ID is
omitted, or octet 13 when the connection is present.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest problem for having migration appear to be a new connection is that the packets won't decrypt with the handshake keys. This doesn't change any of that. @mikkelfj separately suggested that we use 0-RTT, and that could work, but it's a pretty big lift.

protection algorithm might be used.

To prevent an attacker from modifying packet numbers, values of packet numbers
are transitively authenticated using the packet number protection. A falsified
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. Got used to typing packet number protection.

@@ -709,8 +714,8 @@ packets MUST use connection ID selected by the client.
The packet number is an integer in the range 0 to 2^62-1. The value is used in
determining the cryptographic nonce for packet encryption. Each endpoint
maintains a separate packet number for sending and receiving. The packet number
for sending MUST increase by at least one after sending any packet, unless
otherwise specified (see {{initial-packet-number}}).
for sending MUST starts at zero for the first packet set and MUST increase by at
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, epic fail :)

packet protection algorithm. This might happen if a packet with a short header
contains minimal data and uses a packet number encoding that is shorter than 4
octets. Additional zero octets are added to the end of the sequence to reach
the required amount of data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to use the last N bytes of the packet as IV.

By doing so, I think that we can remove the zero-padding rule introduced in this paragraph, since it is guaranteed (at least for the algorithms being defined here) that the length of the AEAD tag is longer than that required for the IV.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this, and it does have that advantage. However, I have been told that reading that far ahead is problematic for some implementations, performance-wise.

The odds of actually having a short packet are tiny. For a 1 octet packet number, you need to have <3 octets of payload. For a 2 octet packet number, then you need <2 octets of payload. And right now only PADDING is one octet. PING will eventually go back to 1 octet as well. That's for a 2^-56 bound. At 2^-60, STREAM, BLOCKED, and STREAM_ID_BLOCKED can be 2 octets. MAX_DATA also, if you believe that <64 octets is ever something you would see on the wire.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of zero padding, the input could sampled from protected packet content including cipher text and, for short cipher text, also the authentication tag.

But

I don't like that it is not possible to do AEAD verification in-place without modifying the packet content. Thus AEAD should cover the encrypted packet number, not the plain text packet number. However, then it does not work to use the cipher nor the tag as input.

Copy link
Member

@kazuho kazuho Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinthomson Thank you for elaborating the issue behind the design decision. That makes sense to me.

@mikkelfj

Instead of zero padding, the input could sampled from protected packet content including cipher text and, for short cipher text, also the authentication tag.

The issue is that it is impossible to tell where the ciphertext or the authentication tag begins, assuming that the length of the packet number field will become part of the protected packet number.

Consider the case where we have protected packet number (1 octet), ciphertext (1 octet), tag (16 octets).

The length of the packet number is encoded within the protected packet number. We need to find entropy from the octets that are guaranteed to be not part of the protected packet number.

The text in the PR proposes to take from the fifth octet of the encrypted triplet (i.e. protected packet number, ciphertext, tag). That would mean that the minimum length of the entropy would be 14 bytes. Hence zero padding.

I don't like that it is not possible to do AEAD verification in-place without modifying the packet content.

My assumption was that we would nevertheless be required to modify the packet header before feeding it as AAD to AEAD, assuming that we would either have greasing (in the packet header) or at least one spin bit. Is that no longer the case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that it is impossible to tell where the ciphertext or the authentication tag begins

I see. But I don't think it would be a big deal to expose the length bits in clear text. But then again, zero padding is also fine.

My assumption was that we would nevertheless be required to modify the packet header

I would assume you could apply AEAD after spin bits etc.. However, packet numbers are different because they are the IV for the AEAD itself and because their encryption is derived from the same AEAD.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my "barrier" comment above. The only requirement is that one does not re-encrypt the bytes used for the IV, because that would lead to trouble. So, set a barrier before these bytes, and never XOR bytes after that barrier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what your proposal is there. What I have described takes at least 14 octets always. I think that's enough, even with the birthday bound on 112 bits being pretty low: with AES you have to re-key long before you hit that many packets, and I'd say that you'd want to do the same with ChaCha even if it remains cryptographically sound for far longer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I have in mind is the following:

  1. Assume that after encryption but before packet number encryption the packet is composed of encrypted payload (ccc...ccc), packet number (pp), and other header bytes (h)
    hhhhhhppppccccccc...ccc

After packet number encryption the "p" bytes will be encrypted (P). Some of the leading bytes in the ciphertext may also be re-encrypted (C):

    hhhhhhPPPPCCCCccc...ccc

This only works if the receiver can find out whether the "cipher text input" starts, without knowing the exact length of the packet encoding. At that point it only knows the position of the first packet number byte, and the length of the packet. I would like a way to specify the length of the "PN encryption" and the offset of the ciphertext sample without requiring the receiver to exactly know the packet number length.

My proposition is to set the "offset" value to MIN(pn_start + 8, packet_length-16), and to encrypt the bytes between "pn_start" and "offset".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to propose something similar to @huitema but then thought zero padding might do just as well.

However, I think it is better to take the tail content when the default start overreach because then you can always consume the content directly without moving it to a separate buffer. It's a quite simple solution.


Use of the same key and nonce for encryption for encryption can weaken
encryption. For the schemes described, protecting two packet numbers with the
same key and nonce would reveal the packet number. For packet number protection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for encryption twice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I say two "different" packet numbers? Basically, this is the standard admonition against reuse of key and nonce.

This algorithm samples 16 octets from the packet ciphertext. This input is used
as the AES initialization vector (IV). This value is input to AES that is keyed
using the current packet protection key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is this 16 or 14?

property that AEAD algorithms do not guarantee. Therefore, no strong assurances
about the general security of this mechanism can be proven.

Use of the same key and nonce for encryption for encryption can weaken
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"for encryption" twice


~~~
encoded = EncodePacketNumber(packet_number)
counter = DecodeLE(sample[0..3])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing sample definition, or is it just assumed to be the same as with AES?

attacker is unable to reduce this without knowledge of the key. Based on this
assumption, the odds of two samples of ciphertext being identical approach the
birthday bound for the size of the sample (that is, two to the negative power of
half the number of sampled bits).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible simplification of these three paragraphs:

Assuming all AEAD functions used are reducible to PRFs, an (encoded) packet number p is encrypted as:

    p XOR PRF(pn_key, m),

where m is an L-bit string sampled from a protected packet’s ciphertext. This construction is secure
against chosen plaintext attacks (IND-CPA) if the distribution of m is uniform over {0,1}^L {{KatzLindell}}.
Since AEAD schemes are semantically secure, we assume each bit of sampled ciphertext contains one bit
of entropy. Thus, two ciphertexts will collide, possibly leading to nonce reuse, with probability bounded by
2^(-L/2), i.e., the birthday bound. Use of the same key and nonce more than once can weaken the
guarantees provided by this protection. For the schemes described, protecting two different packet
numbers with the same key and nonce reveals the exclusive OR of those packet numbers, which might be
used to compromise confidentiality.

KatzLindell: Katz, Jonathan, and Yehuda Lindell. Introduction to modern cryptography. CRC press, 2014.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's the right formulation: we assume that the AEAD is a PRF. It's not guaranteed to be by the contract, but it's probably true for the AEADs here. Thanks for the text. I'll probably want to make the assumption much more prominent though. Also, the general form here is using AES or ChaCha as a PRF, so that means that we can factor out the cipher-specific parts.

I'll see what I can do.

algorithms for AEAD_AES_128_GCM, AEAD_AES_128_CCM, AEAD_AES_256_GCM,
AEAD_AES_256_CCM (all AES AEADs are defined in {{!RFC5116}}), and
AEAD_CHACHA20_POLY1305 ({{!CHACHA=RFC7539}}).

Copy link
Contributor

@huitema huitema Feb 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this whole section hard to understand and hard to implement. I would like to see much more uniformity between the various constructs. I get the following:

  1. We are going to use the same encryption algorithm for PN as the encryption algorithm part of AEAD.

  2. There will be a specific key for PN encryption, different from the key used for data encryption.

  3. The PN encryption will use a 16 byte sample of the packet's ciphertext as nonce.

  4. Our AEAD specification tells us that the nonce is obtained by combining a preset IV with a sequence number. In our case, we will use the first 4 bytes of the sample as a 32 bit sequence number, and the last 12 bytes of the sample as IV, zero padded to the length required by the selected algorithm if necessary.

  5. Conceptually, AEAD algorithm encode an N byte field by XOR it with a crypto-graphically generated stream of data, plus a 16 byte MAC. When doing PN encryption, we only use the first N bytes of the output, and do not compute or check the MAC. That is, PN encryption uses the "XOR" subset of the AEAD algorithm.

  6. Using only the XOR part of the AEAD algorithm requires specific optimizations depending on the specific AEAD algorithm. These optimizations are presented in the following sections for AES and CHACHA20.

  7. One way to test that a PN encryption implementation is correct is to compute the AEAD encryption of the PN with the selected IV and sequence number, and verify that the AEAD algorithm provides for the same initial bytes as the PN encryption implementation.

Do we agree on that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can consider this an application of the AEAD, but I think that would be an error. I prefer to think of this as using the same primitives that the AEAD uses. That is, we build AEAD_AES_128_GCM using AES, and so we use AES here.

There are commonalities in the design, and I suspect that if you abuse your AEAD interface hard enough, you can get the AEAD to produce the right bits, but that's probably inefficient (you don't need the MAC, but the AEAD will calculate one) and not a general solution (there is no guarantee that the AEAD puts the MAC at the end, or doesn't do other post-processing that would might this design).

The reason for that is part of what we sample from the ciphertext of the packet is fed into the counter part and most AEADs don't let you tweak that (all AEADs I've seen have an internal counter that starts from 0 for each invocation). We need that counter because the 96-bit nonce doesn't have enough entropy. 128 bits ensures that we don't have to worry about collisions, because we can only send 2^62 packets.

So 1 - shared primitives, not same, 2 - agreed, 3 - this will depend on AEAD, unfortunately, 4 - that's internal AEAD details, 5 - that's AEAD-specific, 6 - see point about shared primitives, 7 - this might be true for the current AEADs if you can abuse them enough, but it's not generally true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Kazuho has a plausible generalization with his CTR mode API. Maybe we should find a way to document that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't know that there is a CTR mode for the underlying primitive. I'm happy to include some advice along the lines of "for AES, this is just CTR mode", or a hint that in general CTR mode is a good design, but any further generalization is a presumption that might make it harder to deploy a new cipher.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand @martinthomson's point that if a stream cipher (or block cipher in CTR mode) exists beneath an AEAD is an implementation detail. Hence we need a mapping.

I also agree with @huitema that it would help implementors if the AES algorithm was explained as CTR mode. That would help people come up with an implementation that shares the code between the cipher suites.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CCM mode has a 16 byte header that includes the nonce - it won't be encrypted unless it is taken care of explicitly for that mode and if it isn't encrypted, encrypting the packer number is pointless.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikkelfj: I believe you have misunderstood how CCM is used in TLS (and presumably in QUIC).

The inputs to CCM encryption are:

  • A nonce N
  • The key
  • The plaintext
  • The associated data A

The inputs to CCM decryption are:

  • A nonce N
  • The key
  • The ciphertext
  • The associated data A

In some versions of CCM, you explicitly render N on the wire, but in TLS 1.3, you would not do this, but rather (as in AES-GCM), derive it from the PN. There is then no need to render the nonce on the wire, and it behaves as GCM does.

Copy link
Contributor

@mikkelfj mikkelfj Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is reassuring to hear. I originally found an older RFC 3610, but I then checked TLS 1.3 draft 23 https://tools.ietf.org/html/draft-ietf-tls-tls13-23#appendix-B.4 which referred to RFC 5116 https://tools.ietf.org/html/rfc5116#section-5.3 which in turn referred via https://tools.ietf.org/html/rfc5116#ref-CCM to NIST https://csrc.nist.gov/publications/detail/sp/800-38c/final appendix A, which lists said header with NONCE - but it wasn't very clearly formulated, so I (apparently incorrectly) assumed it was similar to https://tools.ietf.org/html/rfc3610 except for fixed parameters chosen in the header (as I would have expected).

If I very carefully read https://tools.ietf.org/html/draft-ietf-tls-tls13-23#section-5.3, I can see the nonce is implicit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this resolved now? Does the text below address @huitema and @kazuho 's concerns?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so.

@britram
Copy link
Contributor

britram commented Feb 2, 2018

The section "Receiving Protected Packets" needs to be updated as part of this PR; at least

Failure to unprotect a packet does not necessarily indicate the existence of a
protocol error in a peer or an attack. The truncated packet number encoding
used in QUIC can cause packet numbers to be decoded incorrectly if they are
delayed significantly.

no longer holds.

I'd also suggest adding a more in-depth outline of what a receiver should do with a given packet to this section -- I went through the exercise of doing this myself, and it alleviated many of my concerns about the complexity of this approach.

@martinthomson
Copy link
Member Author

@britram, that statement is still true.

I've noted the suggestion. Monday.

@mikkelfj
Copy link
Contributor

mikkelfj commented Mar 26, 2018

I mentioned a variation of this design on the mailing list but thought it better to make a note here as well.

  1. The packet number is removed from the header and the AEAD protected content. This is safe because the packet number is the IV for AEAD.
  2. The last 16 octets of the AEAD message (typically the full tag) is AES encrypted using a traffic key derived for this purpose - but the tag is stored as is, we only need the encrypted value temporarily.
  3. The encrypted tag value is truncated to the length of the packet number and the packet number is encrypted by XOR'ing with this value.
  4. The encrypted packet number is appended to the packet content after the AEAD tag.
  5. The lenght of the packet number must be known. It can be fixed length, the length can be stored in the packet header, or it could be variable length with the last octet remainting some unencrypted bits.

The packet number is not useful without the tag and the tag is not useful without the packet number so it makes sense to store them together, typically in a single cache line. This also speaks for keeping the packet number length encoded last if not fixed length.

This approach avoids any modification to the AEAD packet content which can therefore be decoded without modification which is both good for hardware offload and is cache and synchronization friendly for multi-processor systems.

The approach does not consume more space than an unencrypted packet number.
Due to the linear dependency of packet number encryption there is an estimated latency of 24ns on current hardware because AES cannot take advantage of parallel operations. However, if there are multiple queued packets it might be possible to have the packet number decrypted efficiently by feeding the AES pipeline from multiple packets and then sending the result to the relevant receiving thread, all without additional bus traffic cause by buffer modifications.

@mikkelfj
Copy link
Contributor

mikkelfj commented Mar 26, 2018

The above modification helps CPU cache but it does not help hardware that wants to decrypt while receiving data before the entire packet has been seen.

This can be achieved by moving the encrypted packet number before the AEAD content, and therefore before the packet header. The packet number would use the first encrypted bytes of the packet rather than the AEAD tag to feed its encryption.

This is not as clean, but it would allow processing to start before the full packet has been seen.
Additionally, if a 1 octet checksum is added to the packet number, the packet number decryption could very quickly reject random packet content. This addtional benefit could pay for the addtional encryption overhead in DoS rejection.

@MikeBishop
Copy link
Contributor

Note that #1077 has landed, so this can be retargeted/rebased onto master.

@martinthomson martinthomson changed the base branch from key-schedule-cleanup to master April 4, 2018 05:46
@MikeBishop MikeBishop mentioned this pull request Apr 9, 2018
@mikkelfj mikkelfj mentioned this pull request Apr 9, 2018
@kazuho
Copy link
Member

kazuho commented Apr 23, 2018

@mikkelfj

I'm not sure what you mean by stream cipher in the PN context? You cannot have the next PN depend on the previous due to packet loss.

Because PN leak is a privacy concern, you need to guarantee that PN checksum is secure in cryptographical sense. Using several octets of a block cipher for the purpose (as explained in #1079 (comment)) is secure in sense that the probability of collision depends on the block cipher.

The downside of using a block cipher is that so far none of the proposal using block ciphers provide crypto-agility. The only proposal that has agility is the one proposed in this PR. However, it relies on a stream cipher (i.e. AES-CTR or Chacha20).

The issue with stream ciphers is that it is extremely fragile to side-channel attacks as @ad-l has pointed out. I can think of several ways to shuffle the bits in a secure and somewhat efficient way (e.g. determine the bit order using the output of the stream cipher and use bmi2 instruction for shuffling), but I do not think that it would be easy enough for us to define as a standard.

As to a possible checksum - see my earlier comment in this PR - essentially any reasonable multiplication + shift could do:
#1079 (comment)

As stated above, the checksum needs to be cryptographically secure, assuming that we want to prevent side channel attacks. I do not think the proposal meets that goal.

@mikkelfj
Copy link
Contributor

As stated above, the checksum needs to be cryptographically secure, assuming that we want to prevent side channel attacks. I do not think the proposal meets that goal.

I don't think it needs to be that strong, but PN's then need to be segregated per path. But otherwise yes, I share those concerns, I also mentioned similar concerns in my comment. However, side channel attacks are near impossible to avoid to some degree when you need to have DoS protection.

This is a little rough, but I think that the bones are here.

I know that we discussed removing short packet types in favor of putting the
lengths under encryption.  I will do that separately, just to keep the size of
this changeset to something more manageable.
This is pretty onerous, so I'd like to see if people think that this is
acceptable.  The cost is that all packets need to have their AEAD run the whole
way through.  The gain is that there will be no side-channels for attackers to exploit.
@@ -346,7 +346,7 @@ Packet Number:

: The Packet Number is a 32-bit field that follows the two connection IDs.
{{packet-numbers}} describes the use of packet numbers. Packet numbers are
protected separate to the packet payload.
protected separately to the packet payload.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should read "Packet numbers are protected separately from the packet payload."

@martinthomson
Copy link
Member Author

@kazuho

As far as I understand, any PNE scheme without accompanying checksum is fragile to side-channel attacks, because the probability of the receiver dropping the packet without decrypting the payload is proportional to the value of the decrypted PN field. An attacker that has access to side-channel information can use that information to guess the value of the PN field.

The PR mandates that receivers not do that (though I concede that that could easily turn into a 6919 "MUST (BUT WE KNOW YOU WON'T)"). In short, the checksum you are looking for is the integrity check that is integrated into the AEAD.

Copy link
Contributor

@janaiyengar janaiyengar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really close. A few comments.

encryption algorithm.

For packets with a long header, the ciphertext starting
immediately after the packet number is used (that is, octet 17 onwards).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. This needs to be fixed.

@@ -859,6 +865,11 @@ used for QUIC packet protection is AEAD that is negotiated for use with the TLS
connection. For example, if TLS is using the TLS_AES_128_GCM_SHA256, the
AEAD_AES_128_GCM function is used.

QUIC packets are protected prior to applying packet number encryption
({{pn-encrypt}}). Thus, the unprotected packet number is part of the AAD. When
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/AAD/AEAD/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AAD is correct, but it's not clear and it's the first use of AAD, so I have reworded to use consistent terminology.

algorithms for AEAD_AES_128_GCM, AEAD_AES_128_CCM, AEAD_AES_256_GCM,
AEAD_AES_256_CCM (all AES AEADs are defined in {{!RFC5116}}), and
AEAD_CHACHA20_POLY1305 ({{!CHACHA=RFC7539}}).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this resolved now? Does the text below address @huitema and @kazuho 's concerns?

@@ -345,7 +345,8 @@ Payload Length:
Packet Number:

: The Packet Number is a 32-bit field that follows the two connection IDs.
{{packet-numbers}} describes the use of packet numbers.
{{packet-numbers}} describes the use of packet numbers. Packet numbers are
protected separately to the packet payload.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/to/from/. Or is this because of Englishes?

@@ -452,7 +453,8 @@ Destination Connection ID:
Packet Number:

: The length of the packet number field depends on the packet type. This field
can be 1, 2 or 4 octets long depending on the short packet type.
can be 1, 2 or 4 octets long depending on the short packet type. Packet
numbers are protected separate to the packet payload.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto. Also, "separately".

subsequently protected as normal. \[\[Editor's Note: This isn't ideal, because
it creates a "cheat" where the client assumes a value. That's a problem, so I'm
tempted to suggest that this include any value less than 2^30 so that normal
processing works - and can be properly exercised.]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also makes a client that uses a 0-length CID vulnerable to injection of an arbitrary Retry packet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0-length connection ID is only possible after the client has received a Retry, after which it will discard any Retry packets that it receives. So I don't think so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, of course.

The packet number field contains a packet number, which increases with each
packet sent, see {{packet-numbers}} for details.
The packet number field contains a packet number, which is protected separately
from the rest of the packet (see {{QUIC-TLS}} for details). The underlying
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're using "separately from" here. I would make it the same in other places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded all of these to emphasize additional confidentiality protection, rather than separateness, which is a tiny bit misleading. That also neatly avoids the to/from/than problems I seem to have.

packet number is used.

For packets with a short header, the packet number length is not known before
decryption, so it is assumed to be the smaller of the maximum possible packet
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused. Why is the packet number length not known before hand? Doesn't the short packet header still indicate the length?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in preparation for going to varint packet numbers. It would need a complete rewrite if this assumption were lifted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this PR doesn't change the short header to use varint packet numbers... unless I missed something. Assuming I didn't, it looks like one of three things need to happen then:

  1. An additional PR that changes short headers to varint packet numbers needs to be merged first, and then this rebased on that.
  2. This PR should also change the short headers to varint packet numbers.
  3. This PR should be written to the current format of short header packet numbers.

If we are going to do it, I don't see why we don't just do (2) right now. Do you expect that to be a huge change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given where this PR is right now, given that the chairs have green-lighted this, and given that we're unicorning, I recommend landing this PR as is. Nick, can you open an issue to track this editorial issue, so that it doesn't get dropped?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can open an issue, no problem. But we have waited and discussed all this time to ensure we get a good solution. Can't we take a bit longer just to iron out the details and get a complete solution merged all at once? It should be trivial enough to change the short header packet number encoding to varints.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have fixed the incorrect statement, but left the design unchanged.

@martinthomson martinthomson merged commit f4b5283 into master May 3, 2018
@martinthomson martinthomson deleted the pn-encrypt branch May 3, 2018 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.