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::*;
4
5/// Key schedule operations shared across all backends.
6///
7/// Neither `aes64ks1i` nor `aes64ks2` has a hardware mapping on non-riscv64.
8#[cfg(not(all(not(miri), target_arch = "riscv64", target_feature = "zknd")))]
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) 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) 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(
72        not(miri),
73        target_arch = "riscv64",
74        target_feature = "zknd"
75    ) => {
76        // Nothing, calling native intrinsics
77    }
78    all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
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        /// Software fallback for aes64ds, aes64dsm, aes64im
175        #[expect(
176            clippy::inline_modules,
177            reason = "Small internal API, it is more readable this way"
178        )]
179        mod soft {
180            use crate::rv32::zk::zkn::zknd::rv32_zknd_helpers::{INV_SBOX, gmul};
181
182            #[inline(always)]
183            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
184            fn inv_mix_col(col: u32) -> u32 {
185                let s0 = col as u8;
186                let s1 = (col >> 8u8) as u8;
187                let s2 = (col >> 16u8) as u8;
188                let s3 = (col >> 24u8) as u8;
189                let r0 = gmul(s0, 0x0e) ^ gmul(s1, 0x0b) ^ gmul(s2, 0x0d) ^ gmul(s3, 0x09);
190                let r1 = gmul(s0, 0x09) ^ gmul(s1, 0x0e) ^ gmul(s2, 0x0b) ^ gmul(s3, 0x0d);
191                let r2 = gmul(s0, 0x0d) ^ gmul(s1, 0x09) ^ gmul(s2, 0x0e) ^ gmul(s3, 0x0b);
192                let r3 = gmul(s0, 0x0b) ^ gmul(s1, 0x0d) ^ gmul(s2, 0x09) ^ gmul(s3, 0x0e);
193                u32::from(r0) | (u32::from(r1) << 8u8) | (u32::from(r2) << 16u8) | (u32::from(r3) << 24u8)
194            }
195
196            /// Apply InvShiftRows + InvSubBytes to the full 128-bit state `(rs1, rs2)` and return
197            /// the low 64-bit half of the result.
198            ///
199            /// State layout: column-major, little-endian 64-bit halves.
200            /// `byte[col*4 + row]` is at bit `(row*8)` of `rs1` for `col < 2`, or bit `(row*8)` of
201            /// `rs2` for `col >= 2`.
202            ///
203            /// InvShiftRows shifts row `r` right by `r` columns (cyclically over 4).
204            /// Output low half contains post-transform columns 0 and 1.
205            #[inline(always)]
206            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
207            pub(super) fn aes64ds(rs1: u64, rs2: u64) -> u64 {
208                let state_byte = |col: usize, row: usize| -> u8 {
209                    let word = if col < 2 { rs1 } else { rs2 };
210                    (word >> ((col % 2) * 32 + row * 8)) as u8
211                };
212
213                let mut out = 0;
214                for c in 0..2usize {
215                    for r in 0..4usize {
216                        let src_col = (c + 4 - r) & 3;
217                        let b = INV_SBOX[state_byte(src_col, r) as usize];
218                        out |= u64::from(b) << (c * 32 + r * 8);
219                    }
220                }
221                out
222            }
223
224            #[inline(always)]
225            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
226            pub(super) fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
227                let lo = aes64ds(rs1, rs2);
228                let col0 = inv_mix_col(lo as u32);
229                let col1 = inv_mix_col((lo >> 32u8) as u32);
230                u64::from(col0) | (u64::from(col1) << 32u8)
231            }
232
233            #[inline(always)]
234            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
235            pub(super) fn aes64im(rs1: u64) -> u64 {
236                let col0 = inv_mix_col(rs1 as u32);
237                let col1 = inv_mix_col((rs1 >> 32u8) as u32);
238                u64::from(col0) | (u64::from(col1) << 32u8)
239            }
240        }
241    }
242}
243
244#[inline(always)]
245#[doc(hidden)]
246#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
247pub fn aes64ds(rs1: u64, rs2: u64) -> u64 {
248    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
249    cfg_select! {
250        all(
251            not(miri),
252            target_arch = "riscv64",
253            target_feature = "zknd"
254        ) => {
255            // SAFETY: Compile-time checked for supported feature
256            unsafe {
257                core::arch::riscv64::aes64ds(rs1, rs2)
258            }
259        }
260        all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
261            // SAFETY: Compile-time checked for supported feature
262            unsafe {
263                x86_64::aes64ds(rs1, rs2)
264            }
265        }
266        all(target_arch = "aarch64", target_feature = "aes") => {
267            // SAFETY: Compile-time checked for supported feature
268            unsafe {
269                aarch64::aes64ds(rs1, rs2)
270            }
271        }
272        _ => { soft::aes64ds(rs1, rs2) }
273    }
274}
275
276#[inline(always)]
277#[doc(hidden)]
278#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
279pub fn aes64dsm(rs1: u64, rs2: u64) -> u64 {
280    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
281    cfg_select! {
282        all(
283            not(miri),
284            target_arch = "riscv64",
285            target_feature = "zknd"
286        ) => {
287            // SAFETY: Compile-time checked for supported feature
288            unsafe {
289                core::arch::riscv64::aes64dsm(rs1, rs2)
290            }
291        }
292        all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
293            // SAFETY: Compile-time checked for supported feature
294            unsafe {
295                x86_64::aes64dsm(rs1, rs2)
296            }
297        }
298        all(target_arch = "aarch64", target_feature = "aes") => {
299            // SAFETY: Compile-time checked for supported feature
300            unsafe {
301                aarch64::aes64dsm(rs1, rs2)
302            }
303        }
304        _ => { soft::aes64dsm(rs1, rs2) }
305    }
306}
307
308#[inline(always)]
309#[doc(hidden)]
310#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
311pub fn aes64im(rs1: u64) -> u64 {
312    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
313    cfg_select! {
314        all(
315            not(miri),
316            target_arch = "riscv64",
317            target_feature = "zknd"
318        ) => {
319            // SAFETY: Compile-time checked for supported feature
320            unsafe {
321                core::arch::riscv64::aes64im(rs1)
322            }
323        }
324        all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
325            // SAFETY: Compile-time checked for supported feature
326            unsafe {
327                x86_64::aes64im(rs1)
328            }
329        }
330        all(target_arch = "aarch64", target_feature = "aes") => {
331            // SAFETY: Compile-time checked for supported feature
332            unsafe {
333                aarch64::aes64im(rs1)
334            }
335        }
336        _ => { soft::aes64im(rs1) }
337    }
338}
339
340#[inline(always)]
341#[doc(hidden)]
342#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
343pub fn aes64ks1i(rs1: u64, rnum: Rv64ZkndKsRnum) -> u64 {
344    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
345    cfg_select! {
346        all(
347            not(miri),
348            target_arch = "riscv64",
349            target_feature = "zknd"
350        ) => {
351            // SAFETY: Compile-time checked for supported feature
352            unsafe {
353                core::arch::riscv64::aes64ks1i(rs1, rnum as u8)
354            }
355        }
356        _ => { ks::aes64ks1i(rs1, rnum) }
357    }
358}
359
360#[inline(always)]
361#[doc(hidden)]
362#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
363pub fn aes64ks2(rs1: u64, rs2: u64) -> u64 {
364    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
365    cfg_select! {
366        all(
367            not(miri),
368            target_arch = "riscv64",
369            target_feature = "zknd"
370        ) => {
371            // SAFETY: Compile-time checked for supported feature
372            unsafe {
373                core::arch::riscv64::aes64ks2(rs1, rs2)
374            }
375        }
376        _ => { ks::aes64ks2(rs1, rs2) }
377    }
378}