@@ -114,32 +114,156 @@ pub fn chacha20_state(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u32; 1
114114 s
115115}
116116
117+ /// Fill `out` with successive ChaCha20 keystream blocks: block `b` uses block
118+ /// counter `counter.wrapping_add(b)`, key and nonce fixed (RFC 8439 CTR mode).
119+ ///
120+ /// This is the accelerated entry point the `encryption` AEAD hot path draws its
121+ /// keystream from. It runs the **AVX-512 backend in 16-block strides** when
122+ /// `avx512f` is detected at runtime, and the scalar [`chacha20_block`] reference
123+ /// for the ragged tail and on every non-x86 / non-AVX-512 target. Every backend
124+ /// is a drop-in for the scalar reference — see the module KAT and the
125+ /// `chacha20_keystream_avx512_parity` test that pins byte-for-byte equality.
126+ ///
127+ /// **Constant-time:** all backends are straight-line ARX (add / xor / rotate),
128+ /// no secret-dependent control flow or memory indexing.
129+ pub fn chacha20_keystream ( key : & [ u8 ; 32 ] , counter : u32 , nonce : & [ u8 ; 12 ] , out : & mut [ [ u8 ; 64 ] ] ) {
130+ let mut i = 0usize ;
131+
132+ #[ cfg( target_arch = "x86_64" ) ]
133+ {
134+ if std:: is_x86_feature_detected!( "avx512f" ) {
135+ while i + 16 <= out. len ( ) {
136+ let state = chacha20_state ( key, counter. wrapping_add ( i as u32 ) , nonce) ;
137+ // SAFETY: this arm is only reached after `is_x86_feature_detected!("avx512f")`
138+ // returned true, so every AVX-512F intrinsic in `chacha20_block16_avx512`
139+ // is supported on this CPU. The function reads only its `&state` argument
140+ // and returns an owned array — no raw pointers escape, no aliasing.
141+ let blocks = unsafe { chacha20_block16_avx512 ( & state) } ;
142+ out[ i..i + 16 ] . copy_from_slice ( & blocks) ;
143+ i += 16 ;
144+ }
145+ }
146+ }
147+
148+ // Scalar reference: the ragged tail, and the whole slice on non-AVX-512 targets.
149+ while i < out. len ( ) {
150+ let state = chacha20_state ( key, counter. wrapping_add ( i as u32 ) , nonce) ;
151+ out[ i] = chacha20_block ( & state) ;
152+ i += 1 ;
153+ }
154+ }
155+
156+ /// AVX-512 backend: compute 16 consecutive ChaCha20 keystream blocks in parallel
157+ /// (block `l` uses `state[12].wrapping_add(l)` as its counter, all other words
158+ /// shared). Word-sliced ("vertical") layout — lane `l` of working vector `w`
159+ /// holds word `w` of block `l` — so every round is pure SIMD add/xor/rotate with
160+ /// no cross-lane shuffles; the only transpose is the final little-endian
161+ /// serialization.
162+ ///
163+ /// # Safety
164+ /// Caller must ensure the CPU supports AVX-512F (guarded by
165+ /// `is_x86_feature_detected!("avx512f")` at the single call site in
166+ /// [`chacha20_keystream`]).
167+ #[ cfg( target_arch = "x86_64" ) ]
168+ #[ target_feature( enable = "avx512f" ) ]
169+ unsafe fn chacha20_block16_avx512 ( state : & [ u32 ; 16 ] ) -> [ [ u8 ; 64 ] ; 16 ] {
170+ use core:: arch:: x86_64:: * ;
171+
172+ // One AVX-512 vertical quarter-round: same word indices as the scalar
173+ // `quarter_round`, applied across all 16 blocks at once. Pure ARX.
174+ macro_rules! qr512 {
175+ ( $v: expr, $a: expr, $b: expr, $c: expr, $d: expr) => { {
176+ $v[ $a] = _mm512_add_epi32( $v[ $a] , $v[ $b] ) ;
177+ $v[ $d] = _mm512_rol_epi32:: <16 >( _mm512_xor_si512( $v[ $d] , $v[ $a] ) ) ;
178+ $v[ $c] = _mm512_add_epi32( $v[ $c] , $v[ $d] ) ;
179+ $v[ $b] = _mm512_rol_epi32:: <12 >( _mm512_xor_si512( $v[ $b] , $v[ $c] ) ) ;
180+ $v[ $a] = _mm512_add_epi32( $v[ $a] , $v[ $b] ) ;
181+ $v[ $d] = _mm512_rol_epi32:: <8 >( _mm512_xor_si512( $v[ $d] , $v[ $a] ) ) ;
182+ $v[ $c] = _mm512_add_epi32( $v[ $c] , $v[ $d] ) ;
183+ $v[ $b] = _mm512_rol_epi32:: <7 >( _mm512_xor_si512( $v[ $b] , $v[ $c] ) ) ;
184+ } } ;
185+ }
186+
187+ // Build the 16 initial working vectors. Every word is broadcast identically
188+ // across the 16 lanes EXCEPT the counter (word 12), which gets lane-index
189+ // 0..=15 added (u32 wrapping) — the 16 consecutive block counters.
190+ let lane_index = _mm512_setr_epi32 ( 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 ) ;
191+ let mut orig = [ _mm512_setzero_si512 ( ) ; 16 ] ;
192+ for ( w, o) in orig. iter_mut ( ) . enumerate ( ) {
193+ * o = _mm512_set1_epi32 ( state[ w] as i32 ) ;
194+ }
195+ orig[ 12 ] = _mm512_add_epi32 ( _mm512_set1_epi32 ( state[ 12 ] as i32 ) , lane_index) ;
196+
197+ let mut v = orig;
198+ // 10 double-rounds = 20 rounds, identical schedule to the scalar reference.
199+ for _ in 0 ..10 {
200+ qr512 ! ( v, 0 , 4 , 8 , 12 ) ;
201+ qr512 ! ( v, 1 , 5 , 9 , 13 ) ;
202+ qr512 ! ( v, 2 , 6 , 10 , 14 ) ;
203+ qr512 ! ( v, 3 , 7 , 11 , 15 ) ;
204+ qr512 ! ( v, 0 , 5 , 10 , 15 ) ;
205+ qr512 ! ( v, 1 , 6 , 11 , 12 ) ;
206+ qr512 ! ( v, 2 , 7 , 8 , 13 ) ;
207+ qr512 ! ( v, 3 , 4 , 9 , 14 ) ;
208+ }
209+
210+ // Add the original input state back (RFC 8439 §2.3.1), then de-vectorize:
211+ // `words[w][l]` = final word `w` of block `l`.
212+ let mut words = [ [ 0u32 ; 16 ] ; 16 ] ;
213+ for ( w, dst) in words. iter_mut ( ) . enumerate ( ) {
214+ let summed = _mm512_add_epi32 ( v[ w] , orig[ w] ) ;
215+ let mut tmp = [ 0i32 ; 16 ] ;
216+ // SAFETY: `tmp` is a 16-element (64-byte) i32 array; `_mm512_storeu_si512`
217+ // writes exactly one 512-bit (64-byte) vector, unaligned, in bounds.
218+ _mm512_storeu_si512 ( tmp. as_mut_ptr ( ) . cast ( ) , summed) ;
219+ for ( l, slot) in dst. iter_mut ( ) . enumerate ( ) {
220+ * slot = tmp[ l] as u32 ;
221+ }
222+ }
223+
224+ // Serialize each block little-endian: block `l`, word `w` → bytes 4w..4w+4.
225+ let mut out = [ [ 0u8 ; 64 ] ; 16 ] ;
226+ for ( l, block) in out. iter_mut ( ) . enumerate ( ) {
227+ for w in 0 ..16 {
228+ block[ w * 4 ..w * 4 + 4 ] . copy_from_slice ( & words[ w] [ l] . to_le_bytes ( ) ) ;
229+ }
230+ }
231+ out
232+ }
233+
117234#[ cfg( test) ]
118235mod tests {
119236 use super :: * ;
120237
121- /// RFC 8439 §2.3.2 Known-Answer-Test: the canonical ChaCha20 block. Key =
122- /// `00,01,…,1f`, block counter = 1, nonce = `00 00 00 09 00 00 00 4a 00 00
123- /// 00 00`. This pins the scalar reference; every SIMD backend added later
124- /// MUST reproduce this exact 64-byte keystream (the parity gate).
125- #[ test]
126- fn chacha20_block_rfc8439_kat ( ) {
238+ /// The RFC 8439 §2.3.2 canonical keystream block (key = `00,01,…,1f`, block
239+ /// counter = 1, nonce = `00 00 00 09 00 00 00 4a 00 00 00 00`). The scalar
240+ /// reference and every SIMD backend MUST reproduce this exact 64-byte block.
241+ const RFC8439_KAT_BLOCK : [ u8 ; 64 ] = [
242+ 0x10 , 0xf1 , 0xe7 , 0xe4 , 0xd1 , 0x3b , 0x59 , 0x15 , 0x50 , 0x0f , 0xdd , 0x1f , 0xa3 , 0x20 , 0x71 , 0xc4 , 0xc7 , 0xd1 ,
243+ 0xf4 , 0xc7 , 0x33 , 0xc0 , 0x68 , 0x03 , 0x04 , 0x22 , 0xaa , 0x9a , 0xc3 , 0xd4 , 0x6c , 0x4e , 0xd2 , 0x82 , 0x64 , 0x46 ,
244+ 0x07 , 0x9f , 0xaa , 0x09 , 0x14 , 0xc2 , 0xd7 , 0x05 , 0xd9 , 0x8b , 0x02 , 0xa2 , 0xb5 , 0x12 , 0x9c , 0xd1 , 0xde , 0x16 ,
245+ 0x4e , 0xb9 , 0xcb , 0xd0 , 0x83 , 0xe8 , 0xa2 , 0x50 , 0x3c , 0x4e ,
246+ ] ;
247+
248+ /// The RFC 8439 §2.3.2 key material (key `00..1f`, counter 1, the §2.3.2 nonce).
249+ fn rfc8439_kat_inputs ( ) -> ( [ u8 ; 32 ] , u32 , [ u8 ; 12 ] ) {
127250 let mut key = [ 0u8 ; 32 ] ;
128251 for ( i, b) in key. iter_mut ( ) . enumerate ( ) {
129252 * b = i as u8 ;
130253 }
131254 let nonce: [ u8 ; 12 ] = [ 0x00 , 0x00 , 0x00 , 0x09 , 0x00 , 0x00 , 0x00 , 0x4a , 0x00 , 0x00 , 0x00 , 0x00 ] ;
132- let state = chacha20_state ( & key, 1 , & nonce) ;
133- let block = chacha20_block ( & state ) ;
255+ ( key, 1 , nonce)
256+ }
134257
135- // RFC 8439 §2.3.2 serialized keystream (64 bytes).
136- let expected: [ u8 ; 64 ] = [
137- 0x10 , 0xf1 , 0xe7 , 0xe4 , 0xd1 , 0x3b , 0x59 , 0x15 , 0x50 , 0x0f , 0xdd , 0x1f , 0xa3 , 0x20 , 0x71 , 0xc4 , 0xc7 , 0xd1 ,
138- 0xf4 , 0xc7 , 0x33 , 0xc0 , 0x68 , 0x03 , 0x04 , 0x22 , 0xaa , 0x9a , 0xc3 , 0xd4 , 0x6c , 0x4e , 0xd2 , 0x82 , 0x64 , 0x46 ,
139- 0x07 , 0x9f , 0xaa , 0x09 , 0x14 , 0xc2 , 0xd7 , 0x05 , 0xd9 , 0x8b , 0x02 , 0xa2 , 0xb5 , 0x12 , 0x9c , 0xd1 , 0xde , 0x16 ,
140- 0x4e , 0xb9 , 0xcb , 0xd0 , 0x83 , 0xe8 , 0xa2 , 0x50 , 0x3c , 0x4e ,
141- ] ;
142- assert_eq ! ( block, expected, "ChaCha20 block must match RFC 8439 §2.3.2" ) ;
258+ /// RFC 8439 §2.3.2 Known-Answer-Test on the scalar reference block. This pins
259+ /// the scalar reference; every SIMD backend added later MUST reproduce this
260+ /// exact 64-byte keystream (the parity gate).
261+ #[ test]
262+ fn chacha20_block_rfc8439_kat ( ) {
263+ let ( key, counter, nonce) = rfc8439_kat_inputs ( ) ;
264+ let state = chacha20_state ( & key, counter, & nonce) ;
265+ let block = chacha20_block ( & state) ;
266+ assert_eq ! ( block, RFC8439_KAT_BLOCK , "ChaCha20 block must match RFC 8439 §2.3.2" ) ;
143267 }
144268
145269 /// The quarter-round test vector (RFC 8439 §2.1.1) — the ARX core in
@@ -160,4 +284,44 @@ mod tests {
160284 "quarter-round must match RFC 8439 §2.1.1"
161285 ) ;
162286 }
287+
288+ /// The mandatory SIMD-crypto correctness gate: every block produced by the
289+ /// `chacha20_keystream` dispatcher (AVX-512 backend when `avx512f` is present,
290+ /// scalar tail always) MUST be byte-identical to the scalar
291+ /// [`chacha20_block`] reference for the same key/counter/nonce. `n = 40`
292+ /// spans two full 16-block AVX-512 strides plus an 8-block scalar tail, so
293+ /// both the vectorized body and the ragged-tail path are exercised. If this
294+ /// test is red, no SIMD crypto backend may be committed.
295+ #[ test]
296+ fn chacha20_keystream_dispatch_parity_scalar ( ) {
297+ let mut key = [ 0u8 ; 32 ] ;
298+ for ( i, b) in key. iter_mut ( ) . enumerate ( ) {
299+ * b = ( i as u8 ) . wrapping_mul ( 7 ) . wrapping_add ( 3 ) ;
300+ }
301+ let nonce: [ u8 ; 12 ] = [ 0xde , 0xad , 0xbe , 0xef , 0x00 , 0x11 , 0x22 , 0x33 , 0x44 , 0x55 , 0x66 , 0x77 ] ;
302+ let base = 0x0102_0304u32 ;
303+ let n = 40 ;
304+
305+ let mut ks = vec ! [ [ 0u8 ; 64 ] ; n] ;
306+ chacha20_keystream ( & key, base, & nonce, & mut ks) ;
307+
308+ for ( b, got) in ks. iter ( ) . enumerate ( ) {
309+ let state = chacha20_state ( & key, base. wrapping_add ( b as u32 ) , & nonce) ;
310+ let want = chacha20_block ( & state) ;
311+ assert_eq ! ( * got, want, "keystream block {b} must equal the scalar reference" ) ;
312+ }
313+ }
314+
315+ /// The dispatcher must reproduce the RFC 8439 §2.3.2 KAT at block 0. When the
316+ /// host CPU has AVX-512F this drives the RFC vector THROUGH the AVX-512
317+ /// backend (16 blocks per stride, block 0 uses counter 1), directly pinning
318+ /// the vectorized path to the published answer — not just to the scalar path.
319+ #[ test]
320+ fn chacha20_keystream_dispatch_matches_rfc8439_kat ( ) {
321+ let ( key, counter, nonce) = rfc8439_kat_inputs ( ) ;
322+ // 16 blocks forces the AVX-512 stride when available; block 0 == the KAT.
323+ let mut ks = vec ! [ [ 0u8 ; 64 ] ; 16 ] ;
324+ chacha20_keystream ( & key, counter, & nonce, & mut ks) ;
325+ assert_eq ! ( ks[ 0 ] , RFC8439_KAT_BLOCK , "dispatcher block 0 must match RFC 8439 §2.3.2" ) ;
326+ }
163327}
0 commit comments