|
| 1 | +//! Cross-implementation trust gate for `ndarray::simd::chacha20_keystream`. |
| 2 | +//! |
| 3 | +//! The module KAT (`src/simd_crypto.rs`) pins the scalar reference to the single |
| 4 | +//! RFC 8439 §2.3.2 vector, and `chacha20_keystream_dispatch_parity_scalar` pins |
| 5 | +//! the AVX-512 backend to that scalar reference. This integration test closes the |
| 6 | +//! remaining gap: it proves the *whole dispatcher* is byte-for-byte identical to |
| 7 | +//! an **independent, vetted implementation** — RustCrypto's `chacha20::ChaCha20` |
| 8 | +//! (the IETF 96-bit-nonce / 32-bit-counter variant, RFC 8439) — across a wide |
| 9 | +//! space of random keys, nonces, counters, and lengths. This is the |
| 10 | +//! "reference + KAT, then vectorize with parity" discipline extended to a second |
| 11 | +//! oracle: no consumer (the `encryption` AEAD, a future XChaCha vault) should |
| 12 | +//! draw keystream from the accelerated primitive until it is proven equivalent |
| 13 | +//! to the trusted implementation everywhere, not just at one KAT point. |
| 14 | +//! |
| 15 | +//! ChaCha20 is ARX, so the accelerated path is constant-time by construction; |
| 16 | +//! this test guards *correctness*, which is the property a hand-vectorization |
| 17 | +//! can actually get wrong. |
| 18 | +
|
| 19 | +use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; |
| 20 | +use chacha20::ChaCha20; |
| 21 | +use ndarray::simd::{chacha20_block, chacha20_keystream, chacha20_state}; |
| 22 | + |
| 23 | +/// Deterministic SplitMix64 — a stand-in for a PRNG so the vectors are fixed and |
| 24 | +/// reproducible without a `rand` dev-dependency (and without `Math.random`-style |
| 25 | +/// nondeterminism that would make a failure impossible to bisect). |
| 26 | +struct SplitMix64(u64); |
| 27 | + |
| 28 | +impl SplitMix64 { |
| 29 | + fn next_u64(&mut self) -> u64 { |
| 30 | + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 31 | + let mut z = self.0; |
| 32 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 33 | + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| 34 | + z ^ (z >> 31) |
| 35 | + } |
| 36 | + |
| 37 | + fn fill(&mut self, buf: &mut [u8]) { |
| 38 | + for chunk in buf.chunks_mut(8) { |
| 39 | + let bytes = self.next_u64().to_le_bytes(); |
| 40 | + chunk.copy_from_slice(&bytes[..chunk.len()]); |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +/// The trusted oracle: `n_blocks` of RustCrypto ChaCha20 keystream starting at |
| 46 | +/// block `counter` (produced by encrypting a zero buffer). |
| 47 | +fn rustcrypto_keystream(key: &[u8; 32], counter: u32, nonce: &[u8; 12], n_blocks: usize) -> Vec<u8> { |
| 48 | + let mut cipher = ChaCha20::new_from_slices(key, nonce).expect("valid key/nonce lengths"); |
| 49 | + // Seek to block `counter` (position is in bytes; each block is 64 bytes). |
| 50 | + cipher.seek(u64::from(counter) * 64); |
| 51 | + let mut buf = vec![0u8; n_blocks * 64]; |
| 52 | + cipher.apply_keystream(&mut buf); |
| 53 | + buf |
| 54 | +} |
| 55 | + |
| 56 | +/// The primitive under test, flattened to a byte stream for comparison. |
| 57 | +fn ndarray_keystream(key: &[u8; 32], counter: u32, nonce: &[u8; 12], n_blocks: usize) -> Vec<u8> { |
| 58 | + let mut blocks = vec![[0u8; 64]; n_blocks]; |
| 59 | + chacha20_keystream(key, counter, nonce, &mut blocks); |
| 60 | + blocks.concat() |
| 61 | +} |
| 62 | + |
| 63 | +/// The dispatcher (AVX-512 backend when present, scalar otherwise) must equal the |
| 64 | +/// RustCrypto oracle across a broad, deterministic sample of the parameter space. |
| 65 | +#[test] |
| 66 | +fn keystream_matches_rustcrypto_across_vectors() { |
| 67 | + let mut rng = SplitMix64(0x0DDB_1A5E_5EED_1234); |
| 68 | + |
| 69 | + // Lengths chosen to exercise: sub-stride, exactly one AVX-512 stride (16), |
| 70 | + // multi-stride, and every ragged-tail size in between. |
| 71 | + let block_counts = [1usize, 2, 7, 15, 16, 17, 31, 33, 40, 64]; |
| 72 | + |
| 73 | + for &n in &block_counts { |
| 74 | + // A handful of independent (key, nonce, counter) draws per length. |
| 75 | + for _ in 0..8 { |
| 76 | + let mut key = [0u8; 32]; |
| 77 | + let mut nonce = [0u8; 12]; |
| 78 | + rng.fill(&mut key); |
| 79 | + rng.fill(&mut nonce); |
| 80 | + // Keep counter + n well inside u32 so the RustCrypto oracle (which |
| 81 | + // refuses to wrap its 32-bit counter) stays in its valid range; the |
| 82 | + // wrapping edge is covered separately by the scalar-parity unit test. |
| 83 | + let counter = (rng.next_u64() as u32) & 0x00FF_FFFF; |
| 84 | + |
| 85 | + let got = ndarray_keystream(&key, counter, &nonce, n); |
| 86 | + let want = rustcrypto_keystream(&key, counter, &nonce, n); |
| 87 | + assert_eq!( |
| 88 | + got, want, |
| 89 | + "keystream mismatch vs RustCrypto: n={n} blocks, counter={counter}, key[0]={}, nonce[0]={}", |
| 90 | + key[0], nonce[0] |
| 91 | + ); |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/// The single-block scalar primitive `chacha20_block` (fed via `chacha20_state`) |
| 97 | +/// must equal RustCrypto's first block for the same inputs — pins the low-level |
| 98 | +/// entry point, not just the batched dispatcher. |
| 99 | +#[test] |
| 100 | +fn single_block_matches_rustcrypto() { |
| 101 | + let mut rng = SplitMix64(0xC0FF_EE00_1357_9BDF); |
| 102 | + for _ in 0..64 { |
| 103 | + let mut key = [0u8; 32]; |
| 104 | + let mut nonce = [0u8; 12]; |
| 105 | + rng.fill(&mut key); |
| 106 | + rng.fill(&mut nonce); |
| 107 | + let counter = (rng.next_u64() as u32) & 0x00FF_FFFF; |
| 108 | + |
| 109 | + let got = chacha20_block(&chacha20_state(&key, counter, &nonce)); |
| 110 | + let want = rustcrypto_keystream(&key, counter, &nonce, 1); |
| 111 | + assert_eq!(&got[..], &want[..], "single-block mismatch vs RustCrypto at counter={counter}"); |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +/// Sanity-check the oracle itself is the IETF RFC 8439 variant (not the 64-bit |
| 116 | +/// legacy nonce cipher) by reproducing the §2.3.2 KAT through RustCrypto. If this |
| 117 | +/// fails, the oracle is misconfigured and the parity tests above prove nothing. |
| 118 | +#[test] |
| 119 | +fn rustcrypto_oracle_is_rfc8439_ietf() { |
| 120 | + let mut key = [0u8; 32]; |
| 121 | + for (i, b) in key.iter_mut().enumerate() { |
| 122 | + *b = i as u8; |
| 123 | + } |
| 124 | + let nonce: [u8; 12] = [0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00]; |
| 125 | + let block = rustcrypto_keystream(&key, 1, &nonce, 1); |
| 126 | + |
| 127 | + let expected: [u8; 64] = [ |
| 128 | + 0x10, 0xf1, 0xe7, 0xe4, 0xd1, 0x3b, 0x59, 0x15, 0x50, 0x0f, 0xdd, 0x1f, 0xa3, 0x20, 0x71, 0xc4, 0xc7, 0xd1, |
| 129 | + 0xf4, 0xc7, 0x33, 0xc0, 0x68, 0x03, 0x04, 0x22, 0xaa, 0x9a, 0xc3, 0xd4, 0x6c, 0x4e, 0xd2, 0x82, 0x64, 0x46, |
| 130 | + 0x07, 0x9f, 0xaa, 0x09, 0x14, 0xc2, 0xd7, 0x05, 0xd9, 0x8b, 0x02, 0xa2, 0xb5, 0x12, 0x9c, 0xd1, 0xde, 0x16, |
| 131 | + 0x4e, 0xb9, 0xcb, 0xd0, 0x83, 0xe8, 0xa2, 0x50, 0x3c, 0x4e, |
| 132 | + ]; |
| 133 | + assert_eq!(&block[..], &expected[..], "RustCrypto oracle must reproduce RFC 8439 §2.3.2"); |
| 134 | +} |
0 commit comments