-
Notifications
You must be signed in to change notification settings - Fork 61
chacha20: 64-bit counter support #439
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
chacha20: 64-bit counter support #439
Conversation
In my opinion the counter increment should happen in the backends. Ensuring that the counter is a multiple of 4 is a good idea however, and should help limit the complexity required. (In part this depends on whether Regarding the names, I would prefer only one variant (64-bit counter + 64-bit stream id) be exposed as an RNG. I'm less sure about the ciphers; maybe those don't need to change except to increase the counter size for the legacy and xchacha variants. |
I was just going off of the info in #334. I proposed two other options in there:
Both of those options were implicitly "rejected" (they weren't explicitly rejected) in favor of creating a "newtype" that increments the upper part of the counter when needed. I don't know exactly what @tarcieri meant by that, but this PR is my interpretation of that, and it should function accordingly. Maybe it was supposed to involve more code in the backends. Do you want me to continue this PR, or revert to my other branch with generics, or make a new branch that only uses 64-bit counters in the backend? Or should I be adding code to the backends to implement the "newtype"? I think option 1 is the simplest option out of all of them. All of the backends will behave the same, with no duplicated code as a result of supporting different counters, and no additional branches/if statements in the code. |
Really the main thing I'd like to prevent is having to make every backend generic over both a 32-bit and 64-bit counter. Using a 64-bit counter is another way to achieve that.
The branching doesn't go away: it moves to the 32-bit version to prevent a counter overflow. If that logic fails, it's effectively nonce reuse, which is keystream reuse, which in the context of ChaCha20Poly1305 means authentication key reuse, which can lead to chosen ciphertext attacks, which can lead to full plaintext recovery. IMO it's less dangerous to adapt a 32-bit counter into a 64-bit counter, than to use a 64-bit counter internally but with an additional branch to put a 32-bit cap on it. |
The branching does go away. 64-bit counters behave the exact same as a 32-bit counter except the moment they overflows. Doesn't the |
...except the upper half is now incremented, which is the equivalent of using a different nonce.
We're talking about implementing a 32-bit logical counter using a 64-bit counter in the backend. The
There's no modifications to the backends in this PR? |
This PR added a "new type" ( But I can make a new PR that switches to only 64-bit counters in the backends by referencing my |
I just updated the backends to use a 64-bit counter instead of a 32-bit counter. |
I was able to get all of the tests passing except for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me now! I will give @tarcieri some time to review it, but otherwise it looks good to merge. It would be nice to add KATs for large offsets, but we can do it in a separate PR.
I think
and so on. That way, the counter carrying over will be captured at multiple upper-counter-word positions. And the test would compare |
Do you want me to add those test vectors though? I can add them right quick. I don't think they cover the 64-bit aspect. There would need to be either overflowing or wrapping (or both), or have the upper 32 bits of the block pos set to something |
Something like this would be the 64-bit counter wrapping and overflow test. It passes on all backends and with big endian, but it might be unnecessary: #[test]
fn counter_64bit_overflow_and_wrap_x_rand_chacha() {
use rand_chacha::ChaCha20Rng as OGChaChaRng;
let seed = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
];
let mut rng1 = ChaCha20Rng::from_seed(seed);
let mut rng2 = OGChaChaRng::from_seed(seed);
for block_pos_upper_word in &[0, 1, 2, 3, 4, u32::MAX] {
let block_pos = u32::MAX as u64 | ((*block_pos_upper_word as u64) << 32);
rng1.set_block_pos([u32::MAX, *block_pos_upper_word]);
rng2.set_word_pos(block_pos as u128 * BLOCK_WORDS as u128);
let mut output_1 = [0u8; 256];
let mut output_2 = [0u8; 256];
rng1.fill_bytes(&mut output_1);
rng2.fill_bytes(&mut output_2);
assert_eq!(output_1, output_2);
}
} |
@nstilt1 the comment I linked to earlier had suggested adding upstream test vectors for the counter wrap: rust-random/rand#1654 (comment) If we add them upstream there, we'll have official KATs to test against. |
How does one go about finding these test vectors? I've been looking for a few minutes and only have found this so far. It contains a python script that generates test vectors from the " https://cryptography.io/en/stable/development/custom-vectors/chacha20/ They have a github with the test vector already in a file, no need to run their code: |
The test vectors have been added. Not sure if they belong in |
I see those are licensed under BSD and Apache. Do we have to treat them any special way? |
IANAL but I don't think we have to worry about copyright as it relates to test vectors, especially if you're extracting the raw values and transforming them into Rust syntax. They're just algorithm inputs and outputs, not creative works |
I found the culprit regarding that big endian issue. Replaced the bit shifts with const multiplication because the compiler should be able to optimize it into bit shifts. School is starting back up and I probably won't be able to spend as much time on this. Will try to respond to any queries that arise and fix any bugs. I was thinking about adjusting the counter overflow test one more time to have slightly different starting blocks, but I realized that the backends' parblocks all start from the same block ( |
This started as edits for
rng.rs
, then switched to show how only using 64-bit counters would work and the issues they would cause (only problems existed withChaChaCore
—remaining_blocks()
was preventing issues withChaCha20 Ietf
variants). Then generic counters were green-lit and here we are.Brief rundown of the changes:
All backends use a 64-bit counter and extractstate[12]
andstate[13]
(exceptneon.rs
which extracts the whole 4th row)ChaCha20
usesIetf
variant,ChaCha20Legacy
usesLegacy
variant,ChaCha20Rng
usesLegacy
variant, andXChaCha
usesIetf
variant. We can changeXChaCha
toLegacy
if desiredremaining_blocks()
prevents IETF variant from overflowing (not exactly a change)cipher
backends use either a 32-bit counter or a 64-bit counter based on theVariant::Counter
.rng
backends use a 64-bit counter.Brief rundown of the alternative changes if only using a 64-bit counter (if
remaining_blocks()
was adjusted to count the total available blocks):state[13]
from overflowing (this is only mandatory if the final block of the keystream can be consumed by the IETF Variant. It is also mandatory if we wish forChaChaCore
to not have a bug where after exhausting the keystream, it is unable to return toblock_pos == 0
)soft.rs
uses either a 32-bit counter or a 64-bit counter based onVariant
state[12]
when variant isIetf
, preventingstate[13]
from overflowing after reaching the end of the keystream. This works because the cipher's counter is not meant to wrap, so the extra generated blocks beyond blocku32::MAX
would not be used by the ciphers. If we wanted the cipher to wrap, we would have a problem with this implementation.remaining_blocks
could be updated to allow use of the final block of the keystreamKnown bugs:
ChaCha20
IETF cipher can't use the final block of the keystream. Seechacha20
: Can't use full keystream ofChaCha20
IETF variant #444. It can be fixed inchacha20
'sremaining_blocks()
but it would make the cipher usable after exhausting the keystream. It seems that the only way to fix it is to keep track of a flag/boolean about whether the counter has wrapped or the keystream has been exhausted, or about whether it is the first block or not.ChaCha20Legacy
could wrap on a 32-bit machine if there were 2^32remaining_blocks()
and the user triedapply_keystream()
on 2^32 + 1 blocks. However, that would eat up 256 GiB of RAM and 32-bit machines are limited to 4 GiB of RAM.soft.rs
avx2.rs
sse2.rs
neon.rs
closes #334