Change:
This PR describes a three pass stream cipher option. The three passes are: 1) Encrypt the server-id bytes using the nonce bytes 2) Encrypt the nonce bytes using the server-id bytes 3) Encrypt the server-id bytes using the nonce bytes This is effectively a much simplified version of the FFX algorithm. The effect is complete encryption of server-id and nonce, and thus resiliency against bit flipping attacks. With the simple stream cipher, and attacker can flip bits in the server ID, resulting in a different decrypted value which could be used in attacks. With the triple stream cipher, flipping any bit will result in random flips of every other bits, which would be easy for servers or load balancers to detect. Of course, full encryption also has these properties. But full encryption requires fairly long CID, while stream encryption works reasonably with small CID.
Thanks for writing this. If I can get clarity on the threat model, I would see no reason to retain the original algorithm, and would simply replace it with this one. But I don't quite understand the bit flipping attack against the old algorithm. I can certainly flip the server ID bits to cause the LB to send it to a different server. But I can also do this by generating random CIDs -- only the addition of zero padding, or unallocated server ID space, gives the LB a chance of detecting invalid CIDs. How would LBs detect tampering using the 3 pass technique?
I would detect LB tampering by making the server ID bit longer than strictly necessary. With the three pass method, tampering or random generation will cause every server ID bit to have a random value. Forgeries will statistically be detected as out of range, with probability . Note that it should also be possible to make the nonce bit shorter than the original spec, because we are no more relying on the randomness of the nonce value. Each server could set the nonce to some kind of sequence number, incremented at each CID allocation. The size of the number should be enough to cover all allocations during a CID encryption key epoch, instead of twice that to cover the birthday paradox if using random allocations.
The bit flipping attack against the old algorithm involves using a side channel to obtain the server ID in existing CID, then flipping selected bits to target a specific server.
Another point about stream ciphers. Since we are using an algorithm similar to PN encryption, it is possible to "mask" some bits -- such as for example the two topmost bits. That means it is possible to use 6 bits of the "first octet" as part of the server-id. This is also possible for the clear method, and for the divider method.
I don't think this is correct. What is not clear in the current language is that the server may choose to use these 6 (5 given an outstanding PR) for length encoding, independent of what the LB wants. This is for crypto offload purposes. Encrypting the length bits would create problems.
NAME Sorry to be obtuse, but I'm still not following the threat model that the 3-pass algorithm addresses. I agree that avoiding the birthday problem is a nice result of your proposal. LB tampering detection: Yes, with stream cipher the only way to detect a randomly generated CID is for the server ID space to be >>> than the number of servers. The LB can then simply discard any packet that maps to an invalid server (unless it has a long header). But this true of the existing design, and isn't unique to 3-pass. For the whole time I've worked on this, I've dealt with people very concerned about DoS vs. a single server in the pool, and people who don't think this is a problem at all. I (weakly) tend toward the latter camp. (Also, if it is a problem, the plaintext algorithm is a walking DoS vector, but that's a different issue). Nevertheless, let's assume that it's a problem, and that the attacker has obtained both the positions of the server ID bytes and the server ID for two servers in the pool via a side-channel. Then I guess the attack is to connect to server A, gather a number valid CIDs via NCID frames that have different nonces, and then xor the SID field to route to server B. Is that accurate? If so, do you find that compelling? It seems strictly limited to the number of CIDs I can get from a valid server, so why not just DoS those CIDs to that server instead?
The current stream cipher proposal can only detect tampering if the server-id space is sparse, for a very specific definition of sparse. The property you want is, "if you pick at random a number of the same size as the server ID, there is a fixed probability X that this this a valid number". A linear numbering of the servers will not achieve that, even if you prepend a bunch of zeroes in front of the serial number. The three-pass proposal detects tampering even if the server ID space is used linearly, e.g. even if only numbers lower than N are valid, with N much smaller than the maximum number allowed by the size of the field. This is because the CID is effectively encrypted. In the absence of effective detection of tampering, the malleability of the stream cipher is dangerous. Suppose an attacker does a valid connection, and obtains a series of CID. Suppose the attacker also knows that there are 1024 servers in your pool. For each CID, it can flip the bottom 10 bits of the stream-ID and obtain 1023 new CID, which will all be recognized as valid by the load-balancer or by the firewall. Are you really sure that attackers cannot put that to good use? (Good for them, of course, not so good for the defender.)
Thanks for clarifying. I increasingly favor this change. The alternative would be that configuration agents SHOULD disperse server IDs over the available space, but that could be difficult if the server ID is an IPv4 address or something. I'll sleep on it, but I think I'll accept this PR next week.
Way back in , NAME said that his three-pass algorithm meant that But we never really did that: the nonce is in the range 8..16 octets, which is inherited from the original single-pass algorithm. NAME asked if we could shorten the minimum nonce to 32 bits to reduce overall CID length; if it were, we could support 2^24 servers with an 8-byte CID! This is compelling and we should definitely shorten the nonce to some value. But 32 bits means you need to roll over the keys for every 4 billion CIDs a server issues; given that some QUIC implementations are quite profligate issuing CIDs, is that enough? Even another byte would get us to a trillion CIDs while supporting up to 2^16 servers in 8 octets. On the other hand, the config agent can always pick a longer nonce length if the key lifetime is too short. This is how much we want to allow people to do something potentially dumb, so it's a good spot for discussion.
I think some flexibility and discussion of the pros and cons here would be very useful. I think 32 bits is a reasonable min for our use cases, but maybe there's a reason that would cause issues I'm not aware of? We are not profligate issuers of connection IDs and I think we just issue one extra one at any given point in time.
NAME wrote: "This is compelling and we should definitely shorten the nonce to some value. But 32 bits means you need to roll over the keys for every 4 billion CIDs a server issues; given that some QUIC implementations are quite profligate issuing CIDs, is that enough?" Short answer: yes. Assume a server handles 2,048 connections per second, and allocates 16 CID per connection. That leaves 17 bits, 128K connections, 64K seconds, about 18 hours. Long answer: to make that really work, you need a good rollover mechanism. Something like, if the load balancer observes that one server has rolled over, it triggers rollover for all other servers in the pool.
Most connections need 1 or 2 CIDs, so I think the practical bounds are higher. Also, 2048 connections isn't that much, but 2048 per second is quite a bit based on my experience.
So I wonder if there's an attack that continually changes the DCID it uses, which will cause some servers to issue way more CIDs than usual, thus forcing a rollover inside the normal key management cycle. Two countermeasures: servers SHOULD limit CID issuance to a sensible limit, and (as NAME says) do a good rollover mechanism.
NAME on a separate note, are there security implications for (noncelen < sidlen)? I can't see any, but you would know better than I.
Perhaps I'm over-thinking this, because I'm not sure nonce re-use actually matters here. Assuming that a single server isn't using multiple server IDs, we are essentially encrypting the same plaintext over and over again. So even if the nonce is re-used, that is only amounting to the exact same CID being issued again (modulo random bits in the server use field or where the length would go). While this isn't great, as long as it's not done in the same connection, it doesn't provide any useful information to the observer to correlate clients. If the observer is positioned to see all CIDs in use for the server pool, then over long time scales it may observer that two CIDs match, which for QUIC-LB means they were issued by the same server. Thanks to the config rotation bits, it can likely infer that the keys haven't rotated. But that has very little to do with correlating clients, which is the whole point of the document. A server that rolls over its connection IDs is going to have to generate new Stateless Reset keys, but this is a totally local operation. Am I not thinking about this correctly?
Encryption performs a bijection from the space of tuples to the space of numbers of size nbsid*nbnonce. I don't see a particular risk in having noncelen smaller or larger than sid_len. There is a risk in densely using the space of encrypted numbers. look at the decryption size. If an adversary picks a number at random, that number will decrypt to . The shorter the ID, the denser your utilization of the space, the larger the likelihood that the randomly picked "s" will correspond to a valid server, and thus the adversary's message hitting a server instead of being dropped at the LB. Same goes for the nonce, the adversary has more chances to get lucky if the space of nonces is densely used. But then, decryption will fail and the message will be ignored. So the main issue I see is that randomly creating SID might yield slightly more effective resource-consumption attacks.
Put another way, maybe Sec 11.6 is wrong for QUIC-LB: URL
The attacker will not be able to directly correlate cid values and servers. Changing one bit in the nonce statistically changes half the bits in the encrypted sid+nonce.
Thanks, can we change to a 32 bit nonce and add some more text about the risks and trade-offs of shorter vs longer nonces? I feel someone else might be better at the risks/trade-offs bit, but I'll write a PR to change the value.
I already have one to go up on monday
SG, I'll drop mine then. martinduke NAME wrote:
On the issue of nonce reuse: I toyed with idea of being permissive about this, but for the record here's why there's a problem: At the extreme, say a server uses an independent nonce counter for each of its connections, so the encrypted bits follow a predictable pattern for CIDs 1..n issued over the life of the connection. First, if the plaintext bits (first octet and server-use) are the same, we can't demultiplex connections, and everyone will have everyone else's stateless reset tokens. So at a minimum, the server has to keep the overall CID unique. But even it's unique, if the encrypted bits follow this pattern, then a client connected to that server can identify other clients linked to that server.
Section 5.3 specifies a Shared-State Retry Service describes a token format in which the token include the ODCID, a client IP encoded on 128 bits, and a 20 octets data-time, plus additional data. The token is encrypted using AES ECB. This seems sub-optimal: 1) Using AES GCM or another AEAD format seems more natural. AEAD checks will immediately detect an invalid token, while using ECB forces reliance on invalidity heuristics. 2) If using AEAD, there is no need to encode the IP address in the token. It can be derived from the IP header and placed in a pseudo header. The pseudo header can then be authenticated as part of AEAD decryption. 3) The pseudo header approach can be used to authenticate other fields, e.g. verify that the DCID matches the SCID sent in the Retry packet. 4) Encoding the time as 64 bits time64_t seems more natural than ASCII encoding, and also shorter.
The editor draft also encodes the server's SCID in the token. That too could be moved to a pseudo-header, since the field is assume present in the DCID of the post retry Initial.
This was closed in error, as deals with the Stream Cipher.
I filed a very similar issue and then closed it when I realized the duplication. The text of that issue: I'm not an encryption expert, but if IIUC it's insufficiently hard to forge a shared-state retry token. By inducing a Retry, an attacker can obtain the Retry SCID Length, and then focus entirely on an ODCIDs that allow the CID + CID Length part of the token to be a multiple of 16 B. For example, if RSCIDL= 10 B, then make ODCIDL = 20 B -> 32 Bytes for the block. This then breaks Retry forgery into three separate problems: (1) Obtaining the mapping of IP Address to the first 16 Bytes of the token. A well-positioned observer could build a database of these in the time scale between token key rotations. (2) Generate lots of Retry tokens with an ODCID of the correct length, so there is a range of valid CID blocks. (3) Obtain a valid Retry every few seconds, using the right ODCID length, so that we have a valid timestamp. Thus, the attacker has a database of valid encrypted IP addresses valid encrypted CIDs (really, it's the CID length that would cause validation to fail) valid timestamps As the spec uses AES-ECB, these blocks can be mixed and matched to create valid Retry tokens. This is not exactly trivially open to attack [1], but it does feel like we're conceding a lot of entropy here. I would like someone to propose an alternate design that restores some of that entropy. [1] Step (1) seems to require a fairly privileged position in the network.
NAME Could I trouble you for a PR on this?
Sure. I can basically propose what I am doing in picoquic. And yes, the repetitive nature of ECB enables attacks.
NAME please review PR

Discussion:
4.4. <ins> The Triple Stream Cipher CID algorithm is a variant of the stream cipher algorithm stream-cipher-cid-algorithm that provides true cryptographic protection and also mitigates the malleability issues with a simple stream cipher application.  The CID format is the the same as depicted in stream-cipher-cid-format. 4.4.1. The configuration agent actions are the same as in stream-cipher-cid- format-configuration. 4.4.2. Upon receipt of a QUIC packet that is not of type Initial or 0-RTT, the load balancer extracts as many of the earliest octets from the destination connection ID as necessary to match the nonce length. The server ID immediately follows. The load balancer decrypts the nonce and the server ID using the following three pass algorithm: Pass 1: The load balancer decrypts the server ID using 128-bit AES Electronic Codebook (ECB) mode, much like QUIC header protection. The encrypted nonce octets are zero-padded to 16 octets.  AES-ECB encrypts this encrypted nonce using its key to generate a mask which it applies to the encrypted server id.  This provides an intermediate value of the server ID, referred to as server-id intermediate. server_id_intermediate = encrypted_server_id ^ AES-ECB(key, padded- encrypted-nonce) Pass 2: The load balancer decrypts the nonce octets using 128-bit AES Electronic Codebook (ECB) mode, much like QUIC header protection, using the server-id intermediate as "nonce" for this pass.  The server-id intermediate octets are zero-padded to 16 octets.  AES-ECB encrypts this padded server-id intermediate using its key to generate a mask which it applies to the encrypted nonce.  This provides the decrypted nonce value. nonce = encrypted_nonce ^ AES-ECB(key, padded-server_id_intermediate) Pass 3: The load balancer decrypts the server ID using 128-bit AES Electronic Codebook (ECB) mode, much like QUIC header protection. The nonce octets are zero-padded to 16 octets.  AES-ECB encrypts this nonce using its key to generate a mask which it applies to the intermediate server id.  This provides the dercypted server ID. server_id = server_id_intermediate ^ AES-ECB(key, padded-nonce) This three pass algorithm is a simplified version of the FFX algorithm.  It has the same connection ID length requirements as the Stream Cipher CID Algorithm, with the additional property that each encrypted nonce value depends on all server ID bits, and each encrypted server ID bit depends on all nonce bits and all server ID bits.  This mitigates attacks against stream ciphers in which attackers simply flip encrypted server-ID bits. The output of the decryption is the server ID that the load balancer uses for routing. 4.4.3. When generating a routable connection ID, the server writes arbitrary bits into its nonce octets, and its provided server ID into the server ID octets.  Servers MAY opt to have a longer connection ID beyond the nonce and server ID.  The nonce and additional bits MAY encode additional information, but SHOULD appear essentially random to observers. The server encrypts the server ID using exactly the algorithm as described in triple-stream-cipher-load-balancer-actions, performing the three passes in reverse order. 4.5. </ins> The Block Cipher CID Algorithm, by using a full 16 octets of plaintext and a 128-bit cipher, provides higher cryptographic protection and detection of non-compliant connection IDs.  However, it also requires connection IDs of at least 17 octets, increasing overhead of client-to-server packets. <del> 4.4.1. </del> <ins> 4.5.1. </ins> The configuration agent assigns a server ID to every server in its pool, and determines a server ID length (in octets) sufficiently

The configuration agent also selects an 16-octet AES-ECB key to use for connection ID decryption. <del> 4.4.2. </del> <ins> 4.5.2. </ins> Upon receipt of a QUIC packet, the load balancer reads the first octet to obtain the config rotation bits.  It then decrypts the

opaque server data in that order.  The load balancer uses the server ID octets for routing. <del> 4.4.3. </del> <ins> 4.5.3. </ins> When generating a routable connection ID, the server MUST choose a connection ID length between 17 and 20 octets.  The server writes its