Skip to main content

ic_cipher/
aes.rs

1//! FIPS 197 AES, with a portable constant-time backend and an optional
2//! hardware-accelerated one.
3//!
4//! # Backend selection
5//!
6//! The backend is chosen once, when a key is expanded, and recorded in the
7//! cipher value. On x86-64 with AES-NI that is the accelerated path; everywhere
8//! else it is the portable one. Selection depends only on the CPU, never on key
9//! material, so it leaks nothing.
10//!
11//! Detection is compile-time when the `aes` target feature is already enabled
12//! for the build (`-C target-cpu=native`, say), and runtime otherwise via
13//! `is_x86_feature_detected!`. Under `no_std` only the compile-time path is
14//! available, because runtime detection needs `std`.
15//!
16//! `ic_ontology::runtime::backend()` reports which one is live, so an agent
17//! deciding whether to push a gigabyte through AES-GCM can ask rather than
18//! guess.
19//!
20//! # Trusting the accelerated path
21//!
22//! The portable backend is validated against the FIPS 197 and SP 800-38A
23//! vectors. The accelerated backend is then validated *against the portable
24//! one*, block for block, across every key length and every batch boundary. It
25//! is not an independent reimplementation to be trusted on its own; it is an
26//! optimization held to the output of something already known to be correct.
27
28pub mod portable;
29
30// Only where something expands them: the real backend on aarch64, and the
31// model under test everywhere. A plain x86 build has neither, and an
32// ungated module would sit there as an unused-macro warning.
33#[cfg(any(
34    test,
35    all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std")
36))]
37mod armv8_rounds;
38
39// The model runs everywhere, including on hosts with no ARM hardware,
40// which is the entire point: it checks the round structure that the real
41// backend cannot be executed to check.
42#[cfg(test)]
43mod armv8_model;
44
45#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"))]
46pub mod x86;
47#[cfg(all(
48    any(target_arch = "x86", target_arch = "x86_64"),
49    not(feature = "std"),
50    target_feature = "aes"
51))]
52pub mod x86;
53
54#[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
55pub mod aarch64;
56
57use ic_core::traits::{Algorithm, BlockCipher, SelfTest};
58use ic_core::{ensure, Result};
59
60pub use portable::BLOCK_LEN;
61
62/// Which implementation a cipher value is using.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Backend {
65    /// Pure Rust, algebraic S-box, no hardware support required.
66    Portable,
67    /// x86-64 AES-NI.
68    Aesni,
69    /// The ARMv8 cryptographic extension.
70    Armv8,
71}
72
73impl Backend {
74    /// Stable identifier, matching `ic_ontology::runtime::Backend`.
75    pub const fn id(self) -> &'static str {
76        match self {
77            Self::Portable => "portable-constant-time",
78            Self::Aesni => "aes-ni",
79            Self::Armv8 => "armv8-crypto",
80        }
81    }
82}
83
84/// Whether the AES-NI backend is usable on this CPU.
85#[inline]
86#[must_use]
87pub fn aesni_available() -> bool {
88    // Detection lives in `ic-core` so the ontology can report the same answer
89    // without depending on this crate.
90    ic_core::cpu::has_aes()
91}
92
93/// Whether the ARMv8 AES extension is usable in this build.
94///
95/// Two conditions, and the second is deliberate. The CPU must support the
96/// extension, and the `aarch64-crypto` feature must be on. The feature exists
97/// because the backend was written on an x86 machine and has never been run by
98/// its author; until CI on an arm64 runner has executed the differential tests,
99/// an ARM build keeps the portable backend, which is slower and known to be
100/// right.
101#[inline]
102#[must_use]
103pub fn armv8_aes_available() -> bool {
104    #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
105    {
106        std::arch::is_aarch64_feature_detected!("aes")
107    }
108    #[cfg(not(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std")))]
109    {
110        false
111    }
112}
113
114/// The backend this build will use for AES.
115pub fn active_backend() -> Backend {
116    if armv8_aes_available() {
117        Backend::Armv8
118    } else if aesni_available() {
119        Backend::Aesni
120    } else {
121        Backend::Portable
122    }
123}
124
125/// The key schedule, in whichever representation the active backend wants.
126///
127/// The portable variant holds 240 bytes of round keys and the SIMD variant
128/// holds register state, so the two differ in size. Boxing the larger one would
129/// need an allocator, which this crate deliberately does not require, and a key
130/// schedule is constructed once per key rather than passed around by value —
131/// so the size difference is accepted.
132#[allow(clippy::large_enum_variant)]
133#[derive(Clone)]
134enum Keys {
135    Portable(portable::Schedule),
136    #[cfg(any(
137        all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
138        all(
139            any(target_arch = "x86", target_arch = "x86_64"),
140            not(feature = "std"),
141            target_feature = "aes"
142        )
143    ))]
144    Aesni(x86::Keys),
145    #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
146    Armv8(aarch64::Keys),
147}
148
149/// Expand a key using whichever backend is active.
150fn expand(key: &[u8]) -> Result<Keys> {
151    // The portable schedule is always built: the accelerated backend consumes
152    // its output rather than duplicating the expansion.
153    let sched = portable::Schedule::expand(key)?;
154
155    #[cfg(any(
156        all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
157        all(
158            any(target_arch = "x86", target_arch = "x86_64"),
159            not(feature = "std"),
160            target_feature = "aes"
161        )
162    ))]
163    if aesni_available() {
164        // SAFETY: `aesni_available()` established the `aes` target feature.
165        let keys = unsafe { x86::Keys::load(&sched) };
166        return Ok(Keys::Aesni(keys));
167    }
168
169    #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
170    if armv8_aes_available() {
171        // SAFETY: `armv8_aes_available()` established the `aes` feature.
172        let keys = unsafe { aarch64::Keys::load(&sched) };
173        return Ok(Keys::Armv8(keys));
174    }
175
176    Ok(Keys::Portable(sched))
177}
178
179impl Keys {
180    #[inline]
181    fn encrypt_block(&self, block: &mut [u8]) -> Result<()> {
182        match self {
183            Keys::Portable(s) => portable::encrypt_block(s, block),
184            #[cfg(any(
185                all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
186                all(
187                    any(target_arch = "x86", target_arch = "x86_64"),
188                    not(feature = "std"),
189                    target_feature = "aes"
190                )
191            ))]
192            // SAFETY: this variant is only constructed after a feature check.
193            Keys::Aesni(k) => unsafe { x86::encrypt_block(k, block) },
194            #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
195            // SAFETY: this variant is only constructed after a feature check.
196            Keys::Armv8(k) => unsafe { aarch64::encrypt_block(k, block) },
197        }
198    }
199
200    #[inline]
201    fn decrypt_block(&self, block: &mut [u8]) -> Result<()> {
202        match self {
203            Keys::Portable(s) => portable::decrypt_block(s, block),
204            #[cfg(any(
205                all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
206                all(
207                    any(target_arch = "x86", target_arch = "x86_64"),
208                    not(feature = "std"),
209                    target_feature = "aes"
210                )
211            ))]
212            // SAFETY: this variant is only constructed after a feature check.
213            Keys::Aesni(k) => unsafe { x86::decrypt_block(k, block) },
214            #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
215            // SAFETY: this variant is only constructed after a feature check.
216            Keys::Armv8(k) => unsafe { aarch64::decrypt_block(k, block) },
217        }
218    }
219
220    #[inline]
221    fn encrypt_blocks(&self, data: &mut [u8]) -> Result<()> {
222        match self {
223            Keys::Portable(s) => {
224                ensure!(
225                    data.len() % BLOCK_LEN == 0,
226                    InvalidLength,
227                    "aes batch must be block-aligned"
228                );
229                for block in data.chunks_exact_mut(BLOCK_LEN) {
230                    portable::encrypt_block(s, block)?;
231                }
232                Ok(())
233            }
234            #[cfg(any(
235                all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
236                all(
237                    any(target_arch = "x86", target_arch = "x86_64"),
238                    not(feature = "std"),
239                    target_feature = "aes"
240                )
241            ))]
242            // SAFETY: this variant is only constructed after a feature check.
243            Keys::Aesni(k) => unsafe { x86::encrypt_blocks(k, data) },
244            #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
245            // SAFETY: this variant is only constructed after a feature check.
246            Keys::Armv8(k) => unsafe { aarch64::encrypt_blocks(k, data) },
247        }
248    }
249
250    fn backend(&self) -> Backend {
251        match self {
252            Keys::Portable(_) => Backend::Portable,
253            #[cfg(any(
254                all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
255                all(
256                    any(target_arch = "x86", target_arch = "x86_64"),
257                    not(feature = "std"),
258                    target_feature = "aes"
259                )
260            ))]
261            Keys::Aesni(_) => Backend::Aesni,
262            #[cfg(all(target_arch = "aarch64", feature = "aarch64-crypto", feature = "std"))]
263            Keys::Armv8(_) => Backend::Armv8,
264        }
265    }
266}
267
268macro_rules! aes_variant {
269    ($name:ident, $id:literal, $disp:literal, $keylen:literal, $kat_key:literal, $kat_ct:literal) => {
270        #[doc = concat!("FIPS 197 ", $disp, ".")]
271        #[derive(Clone)]
272        pub struct $name(Keys);
273
274        impl $name {
275            /// Which backend this instance is using.
276            pub fn backend(&self) -> Backend {
277                self.0.backend()
278            }
279
280            /// Force the portable backend, whatever the CPU supports.
281            ///
282            /// Exists so the accelerated path can be differentially tested
283            /// against the portable one in the same process.
284            pub fn new_portable(key: &[u8]) -> Result<Self> {
285                ensure!(key.len() == $keylen, InvalidLength, $id);
286                Ok(Self(Keys::Portable(portable::Schedule::expand(key)?)))
287            }
288        }
289
290        impl Algorithm for $name {
291            const ID: &'static str = $id;
292            const NAME: &'static str = $disp;
293        }
294
295        impl BlockCipher for $name {
296            const BLOCK_LEN: usize = BLOCK_LEN;
297            const KEY_LEN: usize = $keylen;
298
299            fn new(key: &[u8]) -> Result<Self> {
300                ensure!(key.len() == $keylen, InvalidLength, $id);
301                Ok(Self(expand(key)?))
302            }
303
304            fn encrypt_block(&self, block: &mut [u8]) -> Result<()> {
305                self.0.encrypt_block(block)
306            }
307
308            fn decrypt_block(&self, block: &mut [u8]) -> Result<()> {
309                self.0.decrypt_block(block)
310            }
311
312            fn encrypt_blocks(&self, data: &mut [u8]) -> Result<()> {
313                self.0.encrypt_blocks(data)
314            }
315        }
316
317        impl SelfTest for $name {
318            fn self_test() -> Result<()> {
319                // FIPS 197 Appendix C: plaintext 00112233..ff.
320                let mut key = [0u8; $keylen];
321                ic_core::codec::hex_decode($kat_key.as_bytes(), &mut key)?;
322                let mut want = [0u8; 16];
323                ic_core::codec::hex_decode($kat_ct.as_bytes(), &mut want)?;
324
325                let cipher = <Self as BlockCipher>::new(&key)?;
326                let mut block: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
327                cipher.encrypt_block(&mut block)?;
328                ensure!(ic_core::ct::verify(&want, &block), SelfTestFailed, $id);
329
330                cipher.decrypt_block(&mut block)?;
331                let plain: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
332                ensure!(ic_core::ct::verify(&plain, &block), SelfTestFailed, $id);
333
334                // The self-test must cover whichever backend is actually live,
335                // and the portable one regardless, so a CPU-dependent fault
336                // cannot pass unnoticed.
337                let reference = Self::new_portable(&key)?;
338                let mut a: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
339                reference.encrypt_block(&mut a)?;
340                ensure!(ic_core::ct::verify(&want, &a), SelfTestFailed, $id);
341                Ok(())
342            }
343        }
344    };
345}
346
347aes_variant!(
348    Aes128,
349    "aes-128",
350    "AES-128",
351    16,
352    "000102030405060708090a0b0c0d0e0f",
353    "69c4e0d86a7b0430d8cdb78070b4c55a"
354);
355aes_variant!(
356    Aes192,
357    "aes-192",
358    "AES-192",
359    24,
360    "000102030405060708090a0b0c0d0e0f1011121314151617",
361    "dda97ca4864cdfe06eaf70a0ec0d7191"
362);
363aes_variant!(
364    Aes256,
365    "aes-256",
366    "AES-256",
367    32,
368    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
369    "8ea2b7ca516745bfeafc49904b496089"
370);
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use ic_core::codec::{hex, unhex};
376
377    fn enc<C: BlockCipher>(key: &str, pt: &str) -> String {
378        let c = C::new(&unhex(key).unwrap()).unwrap();
379        let mut b = unhex(pt).unwrap();
380        c.encrypt_block(&mut b).unwrap();
381        hex(&b)
382    }
383
384    #[test]
385    fn fips197_appendix_c_vectors() {
386        assert_eq!(
387            enc::<Aes128>(
388                "000102030405060708090a0b0c0d0e0f",
389                "00112233445566778899aabbccddeeff"
390            ),
391            "69c4e0d86a7b0430d8cdb78070b4c55a"
392        );
393        assert_eq!(
394            enc::<Aes192>(
395                "000102030405060708090a0b0c0d0e0f1011121314151617",
396                "00112233445566778899aabbccddeeff"
397            ),
398            "dda97ca4864cdfe06eaf70a0ec0d7191"
399        );
400        assert_eq!(
401            enc::<Aes256>(
402                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
403                "00112233445566778899aabbccddeeff"
404            ),
405            "8ea2b7ca516745bfeafc49904b496089"
406        );
407    }
408
409    /// NIST SP 800-38A F.1.1 uses this key/block pair; it exercises a schedule
410    /// distinct from the FIPS 197 one.
411    #[test]
412    fn sp800_38a_ecb_vector() {
413        assert_eq!(
414            enc::<Aes128>(
415                "2b7e151628aed2a6abf7158809cf4f3c",
416                "6bc1bee22e409f96e93d7e117393172a"
417            ),
418            "3ad77bb40d7a3660a89ecaf32466ef97"
419        );
420    }
421
422    #[test]
423    fn decryption_inverts_encryption() {
424        let key = [0x42u8; 32];
425        let c = Aes256::new(&key).unwrap();
426        let original: [u8; 16] = core::array::from_fn(|i| (i * 13) as u8);
427        let mut block = original;
428        c.encrypt_block(&mut block).unwrap();
429        assert_ne!(block, original);
430        c.decrypt_block(&mut block).unwrap();
431        assert_eq!(block, original);
432    }
433
434    #[test]
435    fn rejects_wrong_key_and_block_lengths() {
436        assert!(Aes128::new(&[0u8; 17]).is_err());
437        assert!(Aes256::new(&[0u8; 16]).is_err());
438        let c = Aes128::new(&[0u8; 16]).unwrap();
439        assert!(c.encrypt_block(&mut [0u8; 15]).is_err());
440        assert!(c.encrypt_blocks(&mut [0u8; 17]).is_err());
441    }
442
443    /// Whichever backend is active must agree with the portable one exactly.
444    /// On a CPU without AES-NI this compares the portable backend with itself,
445    /// which is vacuous but harmless; on one with it, this is the check that
446    /// makes the acceleration trustworthy.
447    #[test]
448    fn active_backend_agrees_with_portable() {
449        for key_len in [16usize, 24, 32] {
450            let key: Vec<u8> = (0..key_len).map(|i| (i * 11 + 3) as u8).collect();
451
452            macro_rules! compare {
453                ($ty:ty) => {{
454                    let fast = <$ty>::new(&key).unwrap();
455                    let slow = <$ty>::new_portable(&key).unwrap();
456                    for seed in 0..32u8 {
457                        let original: [u8; 16] = core::array::from_fn(|i| seed ^ (i as u8 * 17));
458                        let mut a = original;
459                        let mut b = original;
460                        fast.encrypt_block(&mut a).unwrap();
461                        slow.encrypt_block(&mut b).unwrap();
462                        assert_eq!(a, b, "encrypt, key_len {}", key_len);
463
464                        let mut a = original;
465                        let mut b = original;
466                        fast.decrypt_block(&mut a).unwrap();
467                        slow.decrypt_block(&mut b).unwrap();
468                        assert_eq!(a, b, "decrypt, key_len {}", key_len);
469                    }
470                }};
471            }
472            match key_len {
473                16 => compare!(Aes128),
474                24 => compare!(Aes192),
475                _ => compare!(Aes256),
476            }
477        }
478    }
479
480    /// The batch path must produce the same bytes as repeated single-block
481    /// calls, at every length including the ones that straddle the eight-block
482    /// boundary.
483    #[test]
484    fn batch_matches_single_block() {
485        let c = Aes256::new(&[0x2bu8; 32]).unwrap();
486        for blocks in 0..20usize {
487            let data: Vec<u8> = (0..blocks * BLOCK_LEN).map(|i| (i * 7) as u8).collect();
488
489            let mut batched = data.clone();
490            c.encrypt_blocks(&mut batched).unwrap();
491
492            let mut singly = data.clone();
493            for block in singly.chunks_exact_mut(BLOCK_LEN) {
494                c.encrypt_block(block).unwrap();
495            }
496            assert_eq!(batched, singly, "{blocks} blocks");
497        }
498    }
499
500    #[test]
501    fn backend_is_reported_consistently() {
502        let c = Aes128::new(&[0u8; 16]).unwrap();
503        assert_eq!(c.backend(), active_backend());
504        assert_eq!(
505            Aes128::new_portable(&[0u8; 16]).unwrap().backend(),
506            Backend::Portable
507        );
508        assert_eq!(Backend::Portable.id(), "portable-constant-time");
509    }
510
511    #[test]
512    fn self_tests_pass() {
513        Aes128::self_test().unwrap();
514        Aes192::self_test().unwrap();
515        Aes256::self_test().unwrap();
516    }
517}