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