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