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            /// The AES-NI round keys, when that is the live backend: what the
299            /// fused AES-GCM kernels in `gcm_x86` run their rounds on.
300            #[cfg(all(target_arch = "x86_64", feature = "std"))]
301            #[inline]
302            pub(crate) fn aesni_keys(&self) -> Option<&x86::Keys> {
303                match &self.0 {
304                    Keys::Aesni(k) => Some(k),
305                    _ => None,
306                }
307            }
308
309            /// Force the portable backend, whatever the CPU supports.
310            ///
311            /// Exists so the accelerated path can be differentially tested
312            /// against the portable one in the same process.
313            pub fn new_portable(key: &[u8]) -> Result<Self> {
314                ic_core::module::operational()?;
315                ensure!(key.len() == $keylen, InvalidLength, $id);
316                Ok(Self(Keys::Portable(portable::Schedule::expand(key)?)))
317            }
318        }
319
320        impl Algorithm for $name {
321            const ID: &'static str = $id;
322            const NAME: &'static str = $disp;
323        }
324
325        impl BlockCipher for $name {
326            const BLOCK_LEN: usize = BLOCK_LEN;
327            const KEY_LEN: usize = $keylen;
328
329            fn new(key: &[u8]) -> Result<Self> {
330                ic_core::module::operational()?;
331                ensure!(key.len() == $keylen, InvalidLength, $id);
332                Ok(Self(expand(key)?))
333            }
334
335            fn encrypt_block(&self, block: &mut [u8]) -> Result<()> {
336                self.0.encrypt_block(block)
337            }
338
339            fn decrypt_block(&self, block: &mut [u8]) -> Result<()> {
340                self.0.decrypt_block(block)
341            }
342
343            fn encrypt_blocks(&self, data: &mut [u8]) -> Result<()> {
344                self.0.encrypt_blocks(data)
345            }
346        }
347
348        impl crate::gcm::Ctr32 for $name {
349            fn ctr32_xor(&self, counter: &mut [u8; BLOCK_LEN], data: &mut [u8]) -> Result<()> {
350                #[cfg(any(
351                    all(any(target_arch = "x86", target_arch = "x86_64"), feature = "std"),
352                    all(
353                        any(target_arch = "x86", target_arch = "x86_64"),
354                        not(feature = "std"),
355                        target_feature = "aes"
356                    )
357                ))]
358                if let Keys::Aesni(keys) = &self.0 {
359                    // SAFETY: this variant is only constructed after a feature check.
360                    unsafe { x86::ctr32_xor(keys, counter, data) };
361                    return Ok(());
362                }
363                crate::gcm::ctr32_xor_generic(self, counter, data)
364            }
365        }
366
367        impl SelfTest for $name {
368            fn self_test() -> Result<()> {
369                // FIPS 197 Appendix C: plaintext 00112233..ff.
370                let mut key = [0u8; $keylen];
371                ic_core::codec::hex_decode($kat_key.as_bytes(), &mut key)?;
372                let mut want = [0u8; 16];
373                ic_core::codec::hex_decode($kat_ct.as_bytes(), &mut want)?;
374
375                let cipher = <Self as BlockCipher>::new(&key)?;
376                let mut block: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
377                cipher.encrypt_block(&mut block)?;
378                ensure!(ic_core::ct::verify(&want, &block), SelfTestFailed, $id);
379
380                cipher.decrypt_block(&mut block)?;
381                let plain: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
382                ensure!(ic_core::ct::verify(&plain, &block), SelfTestFailed, $id);
383
384                // The self-test must cover whichever backend is actually live,
385                // and the portable one regardless, so a CPU-dependent fault
386                // cannot pass unnoticed.
387                let reference = Self::new_portable(&key)?;
388                let mut a: [u8; 16] = core::array::from_fn(|i| (i * 0x11) as u8);
389                reference.encrypt_block(&mut a)?;
390                ensure!(ic_core::ct::verify(&want, &a), SelfTestFailed, $id);
391                Ok(())
392            }
393        }
394    };
395}
396
397aes_variant!(
398    Aes128,
399    "aes-128",
400    "AES-128",
401    16,
402    "000102030405060708090a0b0c0d0e0f",
403    "69c4e0d86a7b0430d8cdb78070b4c55a"
404);
405aes_variant!(
406    Aes192,
407    "aes-192",
408    "AES-192",
409    24,
410    "000102030405060708090a0b0c0d0e0f1011121314151617",
411    "dda97ca4864cdfe06eaf70a0ec0d7191"
412);
413aes_variant!(
414    Aes256,
415    "aes-256",
416    "AES-256",
417    32,
418    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
419    "8ea2b7ca516745bfeafc49904b496089"
420);
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use ic_core::codec::{hex, unhex};
426
427    fn enc<C: BlockCipher>(key: &str, pt: &str) -> String {
428        let c = C::new(&unhex(key).unwrap()).unwrap();
429        let mut b = unhex(pt).unwrap();
430        c.encrypt_block(&mut b).unwrap();
431        hex(&b)
432    }
433
434    #[test]
435    fn fips197_appendix_c_vectors() {
436        assert_eq!(
437            enc::<Aes128>(
438                "000102030405060708090a0b0c0d0e0f",
439                "00112233445566778899aabbccddeeff"
440            ),
441            "69c4e0d86a7b0430d8cdb78070b4c55a"
442        );
443        assert_eq!(
444            enc::<Aes192>(
445                "000102030405060708090a0b0c0d0e0f1011121314151617",
446                "00112233445566778899aabbccddeeff"
447            ),
448            "dda97ca4864cdfe06eaf70a0ec0d7191"
449        );
450        assert_eq!(
451            enc::<Aes256>(
452                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
453                "00112233445566778899aabbccddeeff"
454            ),
455            "8ea2b7ca516745bfeafc49904b496089"
456        );
457    }
458
459    /// NIST SP 800-38A F.1.1 uses this key/block pair; it exercises a schedule
460    /// distinct from the FIPS 197 one.
461    #[test]
462    fn sp800_38a_ecb_vector() {
463        assert_eq!(
464            enc::<Aes128>(
465                "2b7e151628aed2a6abf7158809cf4f3c",
466                "6bc1bee22e409f96e93d7e117393172a"
467            ),
468            "3ad77bb40d7a3660a89ecaf32466ef97"
469        );
470    }
471
472    #[test]
473    fn decryption_inverts_encryption() {
474        let key = [0x42u8; 32];
475        let c = Aes256::new(&key).unwrap();
476        let original: [u8; 16] = core::array::from_fn(|i| (i * 13) as u8);
477        let mut block = original;
478        c.encrypt_block(&mut block).unwrap();
479        assert_ne!(block, original);
480        c.decrypt_block(&mut block).unwrap();
481        assert_eq!(block, original);
482    }
483
484    #[test]
485    fn rejects_wrong_key_and_block_lengths() {
486        assert!(Aes128::new(&[0u8; 17]).is_err());
487        assert!(Aes256::new(&[0u8; 16]).is_err());
488        let c = Aes128::new(&[0u8; 16]).unwrap();
489        assert!(c.encrypt_block(&mut [0u8; 15]).is_err());
490        assert!(c.encrypt_blocks(&mut [0u8; 17]).is_err());
491    }
492
493    /// Whichever backend is active must agree with the portable one exactly.
494    /// On a CPU without AES-NI this compares the portable backend with itself,
495    /// which is vacuous but harmless; on one with it, this is the check that
496    /// makes the acceleration trustworthy.
497    #[test]
498    fn active_backend_agrees_with_portable() {
499        for key_len in [16usize, 24, 32] {
500            let key: Vec<u8> = (0..key_len).map(|i| (i * 11 + 3) as u8).collect();
501
502            macro_rules! compare {
503                ($ty:ty) => {{
504                    let fast = <$ty>::new(&key).unwrap();
505                    let slow = <$ty>::new_portable(&key).unwrap();
506                    for seed in 0..32u8 {
507                        let original: [u8; 16] = core::array::from_fn(|i| seed ^ (i as u8 * 17));
508                        let mut a = original;
509                        let mut b = original;
510                        fast.encrypt_block(&mut a).unwrap();
511                        slow.encrypt_block(&mut b).unwrap();
512                        assert_eq!(a, b, "encrypt, key_len {}", key_len);
513
514                        let mut a = original;
515                        let mut b = original;
516                        fast.decrypt_block(&mut a).unwrap();
517                        slow.decrypt_block(&mut b).unwrap();
518                        assert_eq!(a, b, "decrypt, key_len {}", key_len);
519                    }
520                }};
521            }
522            match key_len {
523                16 => compare!(Aes128),
524                24 => compare!(Aes192),
525                _ => compare!(Aes256),
526            }
527        }
528    }
529
530    /// The batch path must produce the same bytes as repeated single-block
531    /// calls, at every length including the ones that straddle the eight-block
532    /// boundary.
533    /// The portable batch path against single blocks, at every length around a
534    /// group boundary.
535    ///
536    /// `batch_matches_single_block` below uses `new`, which on a machine with
537    /// AES-NI selects the hardware backend -- so on this machine it never
538    /// reaches the bitsliced path at all. This one forces the portable backend,
539    /// so the chunking into four-block groups, and the one to three blocks left
540    /// over at the end, are exercised wherever the tests run.
541    ///
542    /// Zero through twelve blocks: below a group, exactly a group, a group plus
543    /// a remainder of each size, and several groups.
544    #[test]
545    fn portable_batch_matches_single_blocks_at_every_length() {
546        for key_len in [16usize, 24, 32] {
547            let key: Vec<u8> = (0..key_len).map(|i| (i * 13 + 1) as u8).collect();
548
549            macro_rules! compare {
550                ($ty:ty) => {{
551                    let c = <$ty>::new_portable(&key).unwrap();
552                    for blocks in 0..13usize {
553                        let data: Vec<u8> =
554                            (0..blocks * BLOCK_LEN).map(|i| (i * 7 + 3) as u8).collect();
555
556                        let mut batched = data.clone();
557                        c.encrypt_blocks(&mut batched).unwrap();
558
559                        let mut singly = data.clone();
560                        for block in singly.chunks_exact_mut(BLOCK_LEN) {
561                            c.encrypt_block(block).unwrap();
562                        }
563                        assert_eq!(batched, singly, "key_len {}, {} blocks", key_len, blocks);
564                    }
565                }};
566            }
567            match key_len {
568                16 => compare!(Aes128),
569                24 => compare!(Aes192),
570                _ => compare!(Aes256),
571            }
572        }
573    }
574
575    #[test]
576    fn batch_matches_single_block() {
577        let c = Aes256::new(&[0x2bu8; 32]).unwrap();
578        for blocks in 0..20usize {
579            let data: Vec<u8> = (0..blocks * BLOCK_LEN).map(|i| (i * 7) as u8).collect();
580
581            let mut batched = data.clone();
582            c.encrypt_blocks(&mut batched).unwrap();
583
584            let mut singly = data.clone();
585            for block in singly.chunks_exact_mut(BLOCK_LEN) {
586                c.encrypt_block(block).unwrap();
587            }
588            assert_eq!(batched, singly, "{blocks} blocks");
589        }
590    }
591
592    #[test]
593    fn backend_is_reported_consistently() {
594        let c = Aes128::new(&[0u8; 16]).unwrap();
595        assert_eq!(c.backend(), active_backend());
596        assert_eq!(
597            Aes128::new_portable(&[0u8; 16]).unwrap().backend(),
598            Backend::Portable
599        );
600        assert_eq!(Backend::Portable.id(), "portable-constant-time");
601    }
602
603    #[test]
604    fn self_tests_pass() {
605        Aes128::self_test().unwrap();
606        Aes192::self_test().unwrap();
607        Aes256::self_test().unwrap();
608    }
609}