Skip to main content

ab_riscv_interpreter/rv64/zk/zkn/zknd/
rv64_zknd_helpers.rs

1//! Opaque helpers for RV64 Zknd extension
2
3use ab_riscv_primitives::prelude::*;
4use const_fn_specialization::const_fn_specialization;
5
6/// Key schedule operations shared across all backends.
7///
8/// Neither `aes64ks1i` nor `aes64ks2` has a hardware mapping on non-riscv64.
9#[expect(
10    clippy::inline_modules,
11    reason = "Small internal API, it is more readable this way"
12)]
13mod ks {
14    use crate::rv32::zk::zkn::zknd::rv32_zknd_helpers::SBOX;
15    use ab_riscv_primitives::prelude::*;
16
17    /// AES key schedule step 1.
18    ///
19    /// Pseudocode (RISC-V Crypto spec Sail source):
20    /// ```text
21    ///   temp = rs1[63:32]
22    ///   if rnum != 0xA: temp = RotWord(temp)
23    ///   temp = SubWord(temp)
24    ///   if rnum != 0xA: temp ^= RCON[rnum]
25    ///   rd = temp | (temp << 32)
26    /// ```
27    #[inline(always)]
28    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
29    pub(super) const fn aes64ks1i(rs1: u64, rnum: Rv64ZkndKsRnum) -> u64 {
30        let w = (rs1 >> 32u8) as u32;
31
32        let rotated = if rnum == Rv64ZkndKsRnum::Final {
33            w
34        } else {
35            w.rotate_right(8)
36        };
37
38        let b0 = u32::from(SBOX[(rotated & 0xff) as usize]);
39        let b1 = u32::from(SBOX[((rotated >> 8u8) & 0xff) as usize]);
40        let b2 = u32::from(SBOX[((rotated >> 16u8) & 0xff) as usize]);
41        let b3 = u32::from(SBOX[((rotated >> 24u8) & 0xff) as usize]);
42        let subbed = b0 | (b1 << 8u8) | (b2 << 16u8) | (b3 << 24u8);
43
44        let result = if let Some(round_constant) = rnum.constant() {
45            subbed ^ u32::from(round_constant)
46        } else {
47            subbed
48        };
49
50        u64::from(result) | (u64::from(result) << 32u8)
51    }
52
53    /// AES key schedule step 2.
54    ///
55    /// Pseudocode (RISC-V Crypto spec):
56    /// ```text
57    ///   w0 = rs1[63:32] ^ rs2[31:0]
58    ///   w1 = rs1[63:32] ^ rs2[31:0] ^ rs2[63:32]
59    ///   rd = w0 | (w1 << 32)
60    /// ```
61    #[inline(always)]
62    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
63    pub(super) const fn aes64ks2(rs1: u64, rs2: u64) -> u64 {
64        let w0 = (rs1 >> 32u8) as u32 ^ rs2 as u32;
65        let w1 = w0 ^ (rs2 >> 32u8) as u32;
66        u64::from(w0) | (u64::from(w1) << 32u8)
67    }
68}
69
70cfg_select! {
71    all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
72        // Nothing, calling native intrinsics
73    }
74    all(
75        target_arch = "x86_64",
76        target_feature = "aes",
77        target_feature = "sse4.1"
78    ) => {
79        /// x86-64 AES-NI implementation
80        #[expect(
81            clippy::inline_modules,
82            reason = "Small internal API, it is more readable this way"
83        )]
84        mod x86_64 {
85            use core::arch::x86_64::{
86                _mm_aesdec_si128, _mm_aesdeclast_si128, _mm_aesimc_si128, _mm_extract_epi64,
87                _mm_set_epi64x, _mm_setzero_si128,
88            };
89
90            /// `_mm_aesdeclast_si128(state, zero)` computes InvShiftRows + InvSubBytes, then XORs
91            /// with the round key. Zero key -> no-op XOR, matching `aes64ds`.
92            #[inline]
93            #[target_feature(enable = "aes,sse4.1")]
94            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
95            pub(super) fn aes64ds(rs1: u64, rs2: u64) -> u64 {
96                let state = _mm_set_epi64x(rs2.cast_signed(), rs1.cast_signed());
97                let zero = _mm_setzero_si128();
98                let result = _mm_aesdeclast_si128(state, zero);
99                _mm_extract_epi64::<0>(result).cast_unsigned()
100            }
101
102            /// `_mm_aesdec_si128(state, zero)` computes InvShiftRows + InvSubBytes + InvMixColumns,
103            /// then XORs with the round key. Zero key -> no-op XOR.
104            #[inline]
105            #[target_feature(enable = "aes,sse4.1")]
106            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
107            pub(super) fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
108                let state = _mm_set_epi64x(rs2.cast_signed(), rs1.cast_signed());
109                let zero = _mm_setzero_si128();
110                let result = _mm_aesdec_si128(state, zero);
111                _mm_extract_epi64::<0>(result).cast_unsigned()
112            }
113
114            /// `_mm_aesimc_si128` applies InvMixColumns to all four 32-bit columns.
115            /// `rs1` is replicated into both halves; we extract the low 64 bits.
116            #[inline]
117            #[target_feature(enable = "aes,sse4.1")]
118            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
119            pub(super) fn aes64im(rs1: u64) -> u64 {
120                let state = _mm_set_epi64x(rs1.cast_signed(), rs1.cast_signed());
121                let result = _mm_aesimc_si128(state);
122                _mm_extract_epi64::<0>(result).cast_unsigned()
123            }
124        }
125    }
126    all(target_arch = "aarch64", target_feature = "aes") => {
127        /// AArch64 AES implementation
128        #[expect(
129            clippy::inline_modules,
130            reason = "Small internal API, it is more readable this way"
131        )]
132        mod aarch64 {
133            use core::arch::aarch64::{
134                vaesdq_u8, vaesimcq_u8, vcombine_u64, vcreate_u64, vdupq_n_u8, vgetq_lane_u64,
135                vreinterpretq_u8_u64, vreinterpretq_u64_u8,
136            };
137
138            /// `vaesdq_u8(state, zero)` computes XOR(zero) then InvShiftRows + InvSubBytes. ARM's
139            /// AESD operates in the same byte order as the RISC-V half-state model when
140            /// `(rs1, rs2)` is loaded little-endian; no swap needed.
141            #[inline]
142            #[target_feature(enable = "aes")]
143            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
144            pub(super) fn aes64ds(rs1: u64, rs2: u64) -> u64 {
145                let state = vreinterpretq_u8_u64(vcombine_u64(vcreate_u64(rs1), vcreate_u64(rs2)));
146                let zero = vdupq_n_u8(0);
147                let result = vaesdq_u8(state, zero);
148                vgetq_lane_u64::<0>(vreinterpretq_u64_u8(result))
149            }
150
151            /// `vaesimcq_u8(vaesdq_u8(state, zero))` maps exactly to `aes64dsm`
152            #[inline]
153            #[target_feature(enable = "aes")]
154            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
155            pub(super) fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
156                let state = vreinterpretq_u8_u64(vcombine_u64(vcreate_u64(rs1), vcreate_u64(rs2)));
157                let zero = vdupq_n_u8(0);
158                let after_sub_shift = vaesdq_u8(state, zero);
159                let result = vaesimcq_u8(after_sub_shift);
160                vgetq_lane_u64::<0>(vreinterpretq_u64_u8(result))
161            }
162
163            #[inline]
164            #[target_feature(enable = "aes")]
165            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
166            pub(super) fn aes64im(rs1: u64) -> u64 {
167                let state = vreinterpretq_u8_u64(vcombine_u64(vcreate_u64(rs1), vcreate_u64(rs1)));
168                let result = vaesimcq_u8(state);
169                vgetq_lane_u64::<0>(vreinterpretq_u64_u8(result))
170            }
171        }
172    }
173    _ => {
174        // Nothing, the software fallback below is always available
175    }
176}
177
178/// Software fallback for aes64ds, aes64dsm, aes64im
179#[expect(
180    clippy::inline_modules,
181    reason = "Small internal API, it is more readable this way"
182)]
183mod soft {
184    use crate::const_utils::ConstRange;
185    use crate::rv32::zk::zkn::zknd::rv32_zknd_helpers::{INV_SBOX, gmul};
186
187    #[inline(always)]
188    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
189    const fn inv_mix_col(col: u32) -> u32 {
190        let s0 = col as u8;
191        let s1 = (col >> 8u8) as u8;
192        let s2 = (col >> 16u8) as u8;
193        let s3 = (col >> 24u8) as u8;
194        let r0 = gmul(s0, 0x0e) ^ gmul(s1, 0x0b) ^ gmul(s2, 0x0d) ^ gmul(s3, 0x09);
195        let r1 = gmul(s0, 0x09) ^ gmul(s1, 0x0e) ^ gmul(s2, 0x0b) ^ gmul(s3, 0x0d);
196        let r2 = gmul(s0, 0x0d) ^ gmul(s1, 0x09) ^ gmul(s2, 0x0e) ^ gmul(s3, 0x0b);
197        let r3 = gmul(s0, 0x0b) ^ gmul(s1, 0x0d) ^ gmul(s2, 0x09) ^ gmul(s3, 0x0e);
198        u32::from(r0) | (u32::from(r1) << 8u8) | (u32::from(r2) << 16u8) | (u32::from(r3) << 24u8)
199    }
200
201    /// Byte in row `row` of column `col` of the 128-bit state `(rs1, rs2)`
202    #[inline(always)]
203    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
204    const fn state_byte(rs1: u64, rs2: u64, col: usize, row: usize) -> u8 {
205        let word = if col < 2 { rs1 } else { rs2 };
206        (word >> ((col % 2) * 32 + row * 8)) as u8
207    }
208
209    /// Apply InvShiftRows + InvSubBytes to the full 128-bit state `(rs1, rs2)` and return
210    /// the low 64-bit half of the result.
211    ///
212    /// State layout: column-major, little-endian 64-bit halves.
213    /// `byte[col*4 + row]` is at bit `(row*8)` of `rs1` for `col < 2`, or bit `(row*8)` of
214    /// `rs2` for `col >= 2`.
215    ///
216    /// InvShiftRows shifts row `r` right by `r` columns (cyclically over 4).
217    /// Output low half contains post-transform columns 0 and 1.
218    #[inline(always)]
219    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
220    pub(super) const fn aes64ds(rs1: u64, rs2: u64) -> u64 {
221        let mut out = 0;
222        for c in ConstRange::new(0usize, 2) {
223            for r in ConstRange::new(0usize, 4) {
224                let src_col = (c + 4 - r) & 3;
225                let b = INV_SBOX[state_byte(rs1, rs2, src_col, r) as usize];
226                out |= u64::from(b) << (c * 32 + r * 8);
227            }
228        }
229        out
230    }
231
232    #[inline(always)]
233    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
234    pub(super) const fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
235        let lo = aes64ds(rs1, rs2);
236        let col0 = inv_mix_col(lo as u32);
237        let col1 = inv_mix_col((lo >> 32u8) as u32);
238        u64::from(col0) | (u64::from(col1) << 32u8)
239    }
240
241    #[inline(always)]
242    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
243    pub(super) const fn aes64im(rs1: u64) -> u64 {
244        let col0 = inv_mix_col(rs1 as u32);
245        let col1 = inv_mix_col((rs1 >> 32u8) as u32);
246        u64::from(col0) | (u64::from(col1) << 32u8)
247    }
248}
249
250#[const_fn_specialization]
251#[inline(always)]
252#[doc(hidden)]
253#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
254pub fn aes64ds(rs1: u64, rs2: u64) -> u64 {
255    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
256    cfg_select! {
257        all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
258            // SAFETY: Compile-time checked for supported feature
259            unsafe { core::arch::riscv64::aes64ds(rs1, rs2) }
260        }
261        all(
262            target_arch = "x86_64",
263            target_feature = "aes",
264            target_feature = "sse4.1"
265        ) => {
266            // SAFETY: Compile-time checked for supported feature
267            unsafe { x86_64::aes64ds(rs1, rs2) }
268        }
269        all(target_arch = "aarch64", target_feature = "aes") => {
270            // SAFETY: Compile-time checked for supported feature
271            unsafe { aarch64::aes64ds(rs1, rs2) }
272        }
273        _ => soft::aes64ds(rs1, rs2),
274    }
275}
276
277#[const_fn_specialization]
278#[inline(always)]
279#[doc(hidden)]
280#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
281pub fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
282    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
283    cfg_select! {
284        all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
285            // SAFETY: Compile-time checked for supported feature
286            unsafe { core::arch::riscv64::aes64dsm(rs1, rs2) }
287        }
288        all(
289            target_arch = "x86_64",
290            target_feature = "aes",
291            target_feature = "sse4.1"
292        ) => {
293            // SAFETY: Compile-time checked for supported feature
294            unsafe { x86_64::aes64dsm(rs1, rs2) }
295        }
296        all(target_arch = "aarch64", target_feature = "aes") => {
297            // SAFETY: Compile-time checked for supported feature
298            unsafe { aarch64::aes64dsm(rs1, rs2) }
299        }
300        _ => soft::aes64dsm(rs1, rs2),
301    }
302}
303
304#[const_fn_specialization]
305#[inline(always)]
306#[doc(hidden)]
307#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
308pub fn aes64im(rs1: u64) -> u64 {
309    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
310    cfg_select! {
311        all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
312            // SAFETY: Compile-time checked for supported feature
313            unsafe { core::arch::riscv64::aes64im(rs1) }
314        }
315        all(
316            target_arch = "x86_64",
317            target_feature = "aes",
318            target_feature = "sse4.1"
319        ) => {
320            // SAFETY: Compile-time checked for supported feature
321            unsafe { x86_64::aes64im(rs1) }
322        }
323        all(target_arch = "aarch64", target_feature = "aes") => {
324            // SAFETY: Compile-time checked for supported feature
325            unsafe { aarch64::aes64im(rs1) }
326        }
327        _ => soft::aes64im(rs1),
328    }
329}
330
331#[const_fn_specialization]
332#[inline(always)]
333#[doc(hidden)]
334#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
335pub fn aes64ks1i(rs1: u64, rnum: Rv64ZkndKsRnum) -> u64 {
336    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
337    cfg_select! {
338        all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
339            // SAFETY: Compile-time checked for supported feature
340            unsafe { core::arch::riscv64::aes64ks1i(rs1, rnum as u8) }
341        }
342        _ => ks::aes64ks1i(rs1, rnum),
343    }
344}
345
346#[const_fn_specialization]
347#[inline(always)]
348#[doc(hidden)]
349#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
350pub fn aes64ks2(rs1: u64, rs2: u64) -> u64 {
351    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
352    cfg_select! {
353        all(not(miri), target_arch = "riscv64", target_feature = "zknd") => {
354            // SAFETY: Compile-time checked for supported feature
355            unsafe { core::arch::riscv64::aes64ks2(rs1, rs2) }
356        }
357        _ => ks::aes64ks2(rs1, rs2),
358    }
359}
360
361#[const_fn_specialization]
362#[inline(always)]
363#[doc(hidden)]
364#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
365pub const fn aes64ds(rs1: u64, rs2: u64) -> u64 {
366    soft::aes64ds(rs1, rs2)
367}
368
369#[const_fn_specialization]
370#[inline(always)]
371#[doc(hidden)]
372#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
373pub const fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
374    soft::aes64dsm(rs1, rs2)
375}
376
377#[const_fn_specialization]
378#[inline(always)]
379#[doc(hidden)]
380#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
381pub const fn aes64im(rs1: u64) -> u64 {
382    soft::aes64im(rs1)
383}
384
385#[const_fn_specialization]
386#[inline(always)]
387#[doc(hidden)]
388#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
389pub const fn aes64ks1i(rs1: u64, rnum: Rv64ZkndKsRnum) -> u64 {
390    ks::aes64ks1i(rs1, rnum)
391}
392
393#[const_fn_specialization]
394#[inline(always)]
395#[doc(hidden)]
396#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
397pub const fn aes64ks2(rs1: u64, rs2: u64) -> u64 {
398    ks::aes64ks2(rs1, rs2)
399}