Skip to content

Commit 2260363

Browse files
committed
simd_crypto: RustCrypto cross-parity trust gate + audit doc fixes
The mandatory second oracle before any consumer draws keystream from the accelerated primitive: prove `ndarray::simd::chacha20_keystream` is byte-for-byte identical to RustCrypto's vetted `chacha20::ChaCha20` (the IETF RFC 8439 variant), not just to our own scalar reference. - tests/chacha20_rustcrypto_parity.rs — deterministic SplitMix64 vectors across sub-stride / exact-16-stride / multi-stride / every ragged tail, 8 random (key,nonce,counter) draws each; plus a single-block check and an oracle-sanity KAT (confirms the RustCrypto side is the 96-bit-nonce IETF variant, so parity means something). `chacha20 = "0.9"` added as a dev-dependency ONLY (matches the version chacha20poly1305 0.10 already pulls; no change to the consumer dependency graph). - This is the "reference + KAT, then vectorize with parity" discipline extended to an independent implementation. The `encryption` AEAD stays on RustCrypto's authenticated XChaCha20-Poly1305 construction — the keystream primitive is now trusted, but the Poly1305 + HChaCha20 composition is NOT reimplemented (never roll your own AEAD). Audit follow-ups (sentinel-qa PASS, two P2 doc nits): - Backend-ladder table: AVX-512 marked DONE (was "(next)"). - chacha20_keystream doc: cite the real parity test name. lib simd_crypto 4/4 + integration parity 3/3 green, clippy -D clean, fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6pT32kk6pnuAAqR3JiYqu
1 parent 564d62f commit 2260363

4 files changed

Lines changed: 145 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ approx = { workspace = true, default-features = true }
217217
itertools = { workspace = true }
218218
ndarray-gen = { workspace = true }
219219
criterion = { version = "0.5", features = ["html_reports"] }
220+
# The vetted RustCrypto ChaCha20 stream cipher — used ONLY as the independent
221+
# oracle for the `chacha20_rustcrypto_parity` integration test that pins
222+
# `ndarray::simd::chacha20_keystream` byte-for-byte against a trusted reference.
223+
# Same version chacha20poly1305 0.10 pulls transitively (see Cargo.lock).
224+
chacha20 = "0.9"
220225

221226
[[bench]]
222227
name = "append"

src/simd_crypto.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
//!
3030
//! | Backend | ChaCha20 block strategy |
3131
//! |------------------|----------------------------------------------------------|
32-
//! | scalar (this rev)| the RFC 8439 reference double-round — the CORRECTNESS anchor |
33-
//! | AVX-512 (server) | 4 states in parallel across the 4 columns/diagonals (next) |
32+
//! | scalar | the RFC 8439 reference double-round — the CORRECTNESS anchor |
33+
//! | AVX-512 (server) | 16 blocks in parallel, word-sliced `__m512i` lanes (DONE) |
3434
//! | wasm128 (browser)| v128 `u32x4` lanes, one state, SIMD quarter-round (next) |
3535
//! | NEON (edge) | `uint32x4_t` lanes (next) |
3636
//!
@@ -125,7 +125,9 @@ pub fn chacha20_state(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u32; 1
125125
/// `chacha20_keystream_avx512_parity` test that pins byte-for-byte equality.
126126
///
127127
/// **Constant-time:** all backends are straight-line ARX (add / xor / rotate),
128-
/// no secret-dependent control flow or memory indexing.
128+
/// no secret-dependent control flow or memory indexing. Backend equivalence is
129+
/// pinned by `chacha20_keystream_dispatch_parity_scalar` (byte-for-byte vs the
130+
/// scalar reference across two AVX-512 strides + a scalar tail).
129131
pub fn chacha20_keystream(key: &[u8; 32], counter: u32, nonce: &[u8; 12], out: &mut [[u8; 64]]) {
130132
let mut i = 0usize;
131133

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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

Comments
 (0)