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