Skip to main content

ic_cipher/
gcm_siv.rs

1//! AES-GCM-SIV (RFC 8452): authenticated encryption that survives nonce reuse.
2//!
3//! # What this is for
4//!
5//! GCM fails catastrophically when a nonce repeats. Two messages under the same
6//! key and nonce share a keystream, so their XOR is the XOR of the plaintexts,
7//! and the authentication key falls out of the pair — every message under that
8//! key becomes forgeable, not just the two that collided. That is not a
9//! theoretical concern; it is the most common way GCM deployments fail.
10//!
11//! GCM-SIV derives its keystream from the tag, and the tag from the plaintext.
12//! Repeat a nonce and the worst an attacker learns is whether two plaintexts
13//! were equal. Everything else holds. The price is that encryption needs two
14//! passes over the plaintext, so it cannot be streamed.
15//!
16//! Reach for it when nonces come from anywhere you do not fully control: a
17//! distributed system without a shared counter, a device that might be restored
18//! from a snapshot, a protocol where the peer picks the nonce.
19//!
20//! # Verification status
21//!
22//! Both key sizes agree with all 50 cases of RFC 8452 appendix C -- C.1, C.2 and
23//! C.3 -- and are registered [`Available`][ic_ontology_status] in the ontology.
24//! AES-GCM-SIV is an IETF RFC rather than a NIST standard, so it is excluded
25//! from the FIPS approved mode on that ground alone; nothing about this
26//! implementation is the reason.
27//!
28//! What *is* verified:
29//!
30//! - [`crate::polyval`] is checked against the GHASH construction of RFC 8452
31//!   Appendix A, over a GHASH that published GCM vectors validate. That is a
32//!   genuine independent oracle for the hardest component.
33//! - AES beneath it is validated against FIPS 197 and SP 800-38A.
34//! - The structural properties below: determinism, nonce-misuse behaviour that
35//!   differs from CTR mode, and tamper rejection.
36//!
37//! What is **not** verified: that the assembly of those pieces — the key
38//! derivation, the length block, the tag and counter bit-twiddling — matches
39//! what other implementations compute. No published RFC 8452 vector is wired
40//! in. An implementation can pass every test here and still interoperate with
41//! nothing, which is exactly the failure this project takes seriously enough to
42//! label rather than gloss. Wire in a vector from RFC 8452 Appendix C before
43//! trusting this against another party.
44//!
45//! [ic_ontology_status]: https://docs.rs/ic-ontology
46
47use crate::polyval::Polyval;
48use ic_core::traits::{Aead, Algorithm, BlockCipher, SelfTest};
49use ic_core::{ensure, Result, Zeroize};
50
51/// Nonce width. RFC 8452 fixes this at 96 bits.
52pub const NONCE_LEN: usize = 12;
53/// Tag width.
54pub const TAG_LEN: usize = 16;
55const BLOCK_LEN: usize = 16;
56
57/// Derive the per-nonce message keys (RFC 8452 section 4).
58///
59/// Each 16-byte derivation block is a little-endian 32-bit counter followed by
60/// the nonce, and only the first eight bytes of each AES output are used. The
61/// discarded half is what stops the derived keys from being a permutation of
62/// the key itself.
63fn derive_keys<C: BlockCipher>(
64    key_cipher: &C,
65    nonce: &[u8],
66    auth_key: &mut [u8; 16],
67    enc_key: &mut [u8],
68) -> Result<()> {
69    let mut block = [0u8; BLOCK_LEN];
70    block[4..].copy_from_slice(nonce);
71
72    let mut take = |counter: u32, out: &mut [u8]| -> Result<()> {
73        block[0..4].copy_from_slice(&counter.to_le_bytes());
74        let mut b = block;
75        key_cipher.encrypt_block(&mut b)?;
76        out.copy_from_slice(&b[..8]);
77        b.zeroize();
78        Ok(())
79    };
80
81    take(0, &mut auth_key[0..8])?;
82    take(1, &mut auth_key[8..16])?;
83    for (i, chunk) in enc_key.chunks_mut(8).enumerate() {
84        take(2 + i as u32, chunk)?;
85    }
86    block.zeroize();
87    Ok(())
88}
89
90/// POLYVAL over the padded AAD, the padded message, and the length block.
91fn authenticate(auth_key: [u8; 16], aad: &[u8], message: &[u8]) -> [u8; 16] {
92    let mut p = Polyval::new(auth_key);
93    p.update_padded(aad);
94    p.update_padded(message);
95
96    // The length block is bit counts, little-endian, AAD first. Including it is
97    // what stops an attacker moving bytes between the AAD and the message.
98    let mut lengths = [0u8; BLOCK_LEN];
99    lengths[0..8].copy_from_slice(&((aad.len() as u64) * 8).to_le_bytes());
100    lengths[8..16].copy_from_slice(&((message.len() as u64) * 8).to_le_bytes());
101    p.update_block(&lengths);
102    p.finish()
103}
104
105/// CTR mode as RFC 8452 defines it: a little-endian 32-bit counter in the first
106/// four bytes, the rest of the block fixed.
107///
108/// Note this is *not* the big-endian counter GCM uses. Same idea, different
109/// byte order, and getting it wrong produces a cipher that decrypts its own
110/// output and nobody else's.
111fn ctr_xor<C: BlockCipher>(cipher: &C, counter_block: &[u8; 16], in_out: &mut [u8]) -> Result<()> {
112    let mut block = *counter_block;
113    for chunk in in_out.chunks_mut(BLOCK_LEN) {
114        let mut keystream = block;
115        cipher.encrypt_block(&mut keystream)?;
116        for (b, k) in chunk.iter_mut().zip(keystream.iter()) {
117            *b ^= *k;
118        }
119        keystream.zeroize();
120
121        let counter = u32::from_le_bytes(block[0..4].try_into().unwrap());
122        block[0..4].copy_from_slice(&counter.wrapping_add(1).to_le_bytes());
123    }
124    block.zeroize();
125    Ok(())
126}
127
128/// Declare an AES-GCM-SIV over one AES key size.
129macro_rules! gcm_siv {
130    ($name:ident, $cipher:ty, $key_len:literal, $id:literal, $disp:literal,
131     $kat_ct:literal, $kat_tag:literal) => {
132        #[doc = concat!("RFC 8452 ", $disp, ". See the module docs on verification status.")]
133        pub struct $name {
134            key: [u8; $key_len],
135        }
136
137        impl Algorithm for $name {
138            const ID: &'static str = $id;
139            const NAME: &'static str = $disp;
140        }
141
142        impl Drop for $name {
143            fn drop(&mut self) {
144                self.key.zeroize();
145            }
146        }
147
148        impl Aead for $name {
149            const KEY_LEN: usize = $key_len;
150            const NONCE_LEN: usize = NONCE_LEN;
151            const TAG_LEN: usize = TAG_LEN;
152
153            fn new(key: &[u8]) -> Result<Self> {
154                ic_core::module::operational()?;
155                ensure!(key.len() == $key_len, InvalidLength, "aes-gcm-siv key");
156                let mut k = [0u8; $key_len];
157                k.copy_from_slice(key);
158                Ok(Self { key: k })
159            }
160
161            fn seal_detached(
162                &self,
163                nonce: &[u8],
164                aad: &[u8],
165                in_out: &mut [u8],
166                tag: &mut [u8],
167            ) -> Result<()> {
168                ic_core::module::operational()?;
169                ensure!(nonce.len() == NONCE_LEN, InvalidLength, "aes-gcm-siv nonce");
170                ensure!(tag.len() == TAG_LEN, InvalidLength, "aes-gcm-siv tag");
171
172                let key_cipher = <$cipher>::new(&self.key)?;
173                let mut auth_key = [0u8; 16];
174                let mut enc_key = [0u8; $key_len];
175                derive_keys(&key_cipher, nonce, &mut auth_key, &mut enc_key)?;
176                let message_cipher = <$cipher>::new(&enc_key)?;
177                enc_key.zeroize();
178
179                // The tag is computed over the *plaintext*, which is what makes
180                // the keystream depend on it and the whole thing misuse
181                // resistant. It is also why this cannot be streamed.
182                let mut s = authenticate(auth_key, aad, in_out);
183                auth_key.zeroize();
184                for (b, n) in s.iter_mut().zip(nonce.iter()) {
185                    *b ^= *n;
186                }
187                s[15] &= 0x7f;
188                message_cipher.encrypt_block(&mut s)?;
189                tag.copy_from_slice(&s);
190
191                let mut counter_block = s;
192                counter_block[15] |= 0x80;
193                ctr_xor(&message_cipher, &counter_block, in_out)?;
194                counter_block.zeroize();
195                s.zeroize();
196                Ok(())
197            }
198
199            fn open_detached(
200                &self,
201                nonce: &[u8],
202                aad: &[u8],
203                in_out: &mut [u8],
204                tag: &[u8],
205            ) -> Result<()> {
206                ic_core::module::operational()?;
207                ensure!(nonce.len() == NONCE_LEN, InvalidLength, "aes-gcm-siv nonce");
208                ensure!(tag.len() == TAG_LEN, InvalidLength, "aes-gcm-siv tag");
209
210                let key_cipher = <$cipher>::new(&self.key)?;
211                let mut auth_key = [0u8; 16];
212                let mut enc_key = [0u8; $key_len];
213                derive_keys(&key_cipher, nonce, &mut auth_key, &mut enc_key)?;
214                let message_cipher = <$cipher>::new(&enc_key)?;
215                enc_key.zeroize();
216
217                let mut counter_block = [0u8; BLOCK_LEN];
218                counter_block.copy_from_slice(tag);
219                counter_block[15] |= 0x80;
220                ctr_xor(&message_cipher, &counter_block, in_out)?;
221                counter_block.zeroize();
222
223                let mut s = authenticate(auth_key, aad, in_out);
224                auth_key.zeroize();
225                for (b, n) in s.iter_mut().zip(nonce.iter()) {
226                    *b ^= *n;
227                }
228                s[15] &= 0x7f;
229                message_cipher.encrypt_block(&mut s)?;
230
231                let ok = ic_core::ct::verify(&s, tag);
232                s.zeroize();
233                if !ok {
234                    // Never hand back unauthenticated plaintext.
235                    in_out.zeroize();
236                    return Err(ic_core::err!(AuthenticationFailed, $id));
237                }
238                Ok(())
239            }
240        }
241
242        impl SelfTest for $name {
243            /// RFC 8452 appendix C, plus the structural properties a vector
244            /// cannot express.
245            ///
246            /// The vector is the part that says this computes AES-GCM-SIV
247            /// rather than something self-consistent. The checks after it cover
248            /// what no vector does: that a tampered tag is refused, that
249            /// changed associated data is refused, and that a failed open
250            /// clears the buffer instead of releasing unauthenticated
251            /// plaintext.
252            ///
253            /// All 50 published cases run in
254            /// `crates/ironcrypto/tests/vectors.rs`; this is the one the module
255            /// checks at startup, where FIPS 140-3 wants a known-answer test and
256            /// not a test suite.
257            fn self_test() -> Result<()> {
258                // Key, nonce and associated data are shared between the two
259                // published cases; only the key length and the answer differ.
260                let mut key = [0u8; $key_len];
261                key[0] = 0x01;
262                let kat = <Self as Aead>::new(&key)?;
263
264                let mut nonce_kat = [0u8; NONCE_LEN];
265                nonce_kat[0] = 0x03;
266
267                let mut buf = [0x02u8, 0, 0, 0, 0, 0, 0, 0];
268                let mut tag = [0u8; TAG_LEN];
269                kat.seal_detached(&nonce_kat, &[0x01], &mut buf, &mut tag)?;
270
271                let mut want_ct = [0u8; 8];
272                let mut want_tag = [0u8; TAG_LEN];
273                ic_core::codec::hex_decode($kat_ct, &mut want_ct)?;
274                ic_core::codec::hex_decode($kat_tag, &mut want_tag)?;
275                ensure!(
276                    ic_core::ct::verify(&want_ct, &buf) && ic_core::ct::verify(&want_tag, &tag),
277                    SelfTestFailed,
278                    $id
279                );
280
281                let cipher = <Self as Aead>::new(&[0x42u8; $key_len])?;
282                let nonce = [0x24u8; NONCE_LEN];
283
284                let mut a = *b"self-test message";
285                let mut tag_a = [0u8; TAG_LEN];
286                cipher.seal_detached(&nonce, b"aad", &mut a, &mut tag_a)?;
287
288                // Deterministic: the same inputs give the same output.
289                let mut b = *b"self-test message";
290                let mut tag_b = [0u8; TAG_LEN];
291                cipher.seal_detached(&nonce, b"aad", &mut b, &mut tag_b)?;
292                ensure!(a == b && tag_a == tag_b, SelfTestFailed, $id);
293
294                // Round trip.
295                cipher.open_detached(&nonce, b"aad", &mut a, &tag_a)?;
296                ensure!(&a == b"self-test message", SelfTestFailed, $id);
297
298                // A tampered tag is rejected and the buffer is cleared.
299                let mut bad = tag_a;
300                bad[0] ^= 1;
301                ensure!(
302                    cipher.open_detached(&nonce, b"aad", &mut b, &bad).is_err(),
303                    SelfTestFailed,
304                    $id
305                );
306                // Changing the AAD must also fail.
307                let mut c = *b"self-test message";
308                let mut tag_c = [0u8; TAG_LEN];
309                cipher.seal_detached(&nonce, b"aad", &mut c, &mut tag_c)?;
310                ensure!(
311                    cipher
312                        .open_detached(&nonce, b"other", &mut c, &tag_c)
313                        .is_err(),
314                    SelfTestFailed,
315                    $id
316                );
317                Ok(())
318            }
319        }
320    };
321}
322
323gcm_siv!(
324    Aes128GcmSiv,
325    crate::Aes128,
326    16,
327    "aes-128-gcm-siv",
328    "AES-128-GCM-SIV",
329    // RFC 8452 appendix C.1: an 8-byte plaintext with one byte of associated
330    // data. Result = 1e6daba35669f427 3b0a1a2560969cdf790d99759abd1508,
331    // split here into the ciphertext and the tag the API returns separately.
332    b"1e6daba35669f427",
333    b"3b0a1a2560969cdf790d99759abd1508"
334);
335gcm_siv!(
336    Aes256GcmSiv,
337    crate::Aes256,
338    32,
339    "aes-256-gcm-siv",
340    "AES-256-GCM-SIV",
341    // RFC 8452 appendix C.2, the same inputs at the larger key length.
342    // Result = 1de22967237a8132 91213f267e3b452f02d01ae33e4ec854.
343    b"1de22967237a8132",
344    b"91213f267e3b452f02d01ae33e4ec854"
345);
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    fn seal(cipher: &Aes256GcmSiv, nonce: &[u8], aad: &[u8], pt: &[u8]) -> (Vec<u8>, [u8; 16]) {
352        let mut buf = pt.to_vec();
353        let mut tag = [0u8; TAG_LEN];
354        cipher
355            .seal_detached(nonce, aad, &mut buf, &mut tag)
356            .unwrap();
357        (buf, tag)
358    }
359
360    #[test]
361    fn round_trips_at_every_length_boundary() {
362        let cipher = Aes256GcmSiv::new(&[0x11u8; 32]).unwrap();
363        let nonce = [0x22u8; NONCE_LEN];
364
365        for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 255, 256] {
366            let pt = vec![0x5au8; len];
367            let (mut ct, tag) = seal(&cipher, &nonce, b"aad", &pt);
368            assert_eq!(ct.len(), len, "length is preserved");
369            if len > 0 {
370                assert_ne!(ct, pt, "and the plaintext is not passed through");
371            }
372            cipher.open_detached(&nonce, b"aad", &mut ct, &tag).unwrap();
373            assert_eq!(ct, pt, "round trip at {len} bytes");
374        }
375    }
376
377    /// The defining property. Under a repeated nonce, GCM-SIV must *not* behave
378    /// like CTR mode: for CTR, `C1 ^ C2 == P1 ^ P2` exactly, which is what makes
379    /// GCM's nonce reuse fatal. Here the keystream depends on the plaintext
380    /// through the tag, so that relation must fail.
381    #[test]
382    fn nonce_reuse_does_not_leak_the_plaintext_xor() {
383        let cipher = Aes256GcmSiv::new(&[0x33u8; 32]).unwrap();
384        let nonce = [0x44u8; NONCE_LEN];
385
386        let p1 = vec![0xaau8; 64];
387        let mut p2 = vec![0xaau8; 64];
388        p2[0] ^= 0x01; // differ in a single bit
389
390        let (c1, _) = seal(&cipher, &nonce, b"", &p1);
391        let (c2, _) = seal(&cipher, &nonce, b"", &p2);
392
393        let cipher_xor: Vec<u8> = c1.iter().zip(c2.iter()).map(|(a, b)| a ^ b).collect();
394        let plain_xor: Vec<u8> = p1.iter().zip(p2.iter()).map(|(a, b)| a ^ b).collect();
395        assert_ne!(
396            cipher_xor, plain_xor,
397            "a one-bit plaintext change must change the whole keystream"
398        );
399
400        // And almost every byte should differ, not just the one that changed.
401        let differing = cipher_xor.iter().filter(|b| **b != 0).count();
402        assert!(
403            differing > 48,
404            "only {differing} of 64 ciphertext bytes changed; the keystream is \
405             not depending on the plaintext"
406        );
407    }
408
409    /// Encryption is deterministic, which is the trade GCM-SIV makes: equal
410    /// plaintexts under one nonce are visibly equal, and nothing worse.
411    #[test]
412    fn encryption_is_deterministic() {
413        let cipher = Aes256GcmSiv::new(&[0x55u8; 32]).unwrap();
414        let nonce = [0x66u8; NONCE_LEN];
415        let (a, ta) = seal(&cipher, &nonce, b"aad", b"same message");
416        let (b, tb) = seal(&cipher, &nonce, b"aad", b"same message");
417        assert_eq!((a, ta), (b, tb));
418    }
419
420    #[test]
421    fn every_input_is_authenticated() {
422        let cipher = Aes256GcmSiv::new(&[0x77u8; 32]).unwrap();
423        let nonce = [0x88u8; NONCE_LEN];
424        let (ct, tag) = seal(&cipher, &nonce, b"aad", b"message");
425
426        // Wrong nonce.
427        let mut buf = ct.clone();
428        let mut other_nonce = nonce;
429        other_nonce[0] ^= 1;
430        assert!(cipher
431            .open_detached(&other_nonce, b"aad", &mut buf, &tag)
432            .is_err());
433
434        // Wrong AAD.
435        let mut buf = ct.clone();
436        assert!(cipher
437            .open_detached(&nonce, b"aae", &mut buf, &tag)
438            .is_err());
439
440        // Tampered ciphertext.
441        let mut buf = ct.clone();
442        buf[0] ^= 1;
443        assert!(cipher
444            .open_detached(&nonce, b"aad", &mut buf, &tag)
445            .is_err());
446
447        // Tampered tag.
448        let mut buf = ct.clone();
449        let mut bad = tag;
450        bad[15] ^= 1;
451        assert!(cipher
452            .open_detached(&nonce, b"aad", &mut buf, &bad)
453            .is_err());
454    }
455
456    /// A failed open must not leave plaintext in the caller's buffer.
457    #[test]
458    fn a_failed_open_clears_the_buffer() {
459        let cipher = Aes256GcmSiv::new(&[0x99u8; 32]).unwrap();
460        let nonce = [0xaau8; NONCE_LEN];
461        let (ct, tag) = seal(&cipher, &nonce, b"", b"secret plaintext");
462
463        let mut buf = ct;
464        let mut bad = tag;
465        bad[0] ^= 1;
466        assert!(cipher.open_detached(&nonce, b"", &mut buf, &bad).is_err());
467        assert!(
468            buf.iter().all(|b| *b == 0),
469            "the buffer still held data after a failed open"
470        );
471    }
472
473    /// AAD and message must not be interchangeable: moving a byte across the
474    /// boundary has to change the tag. That is what the length block is for.
475    #[test]
476    fn the_aad_boundary_is_authenticated() {
477        let cipher = Aes256GcmSiv::new(&[0xbbu8; 32]).unwrap();
478        let nonce = [0xccu8; NONCE_LEN];
479        let (_, tag_a) = seal(&cipher, &nonce, b"abc", b"def");
480        let (_, tag_b) = seal(&cipher, &nonce, b"ab", b"cdef");
481        assert_ne!(tag_a, tag_b, "the aad/message split must be bound in");
482    }
483
484    #[test]
485    fn key_sizes_and_lengths_are_checked() {
486        assert!(Aes128GcmSiv::new(&[0u8; 16]).is_ok());
487        assert!(Aes128GcmSiv::new(&[0u8; 32]).is_err());
488        assert!(Aes256GcmSiv::new(&[0u8; 16]).is_err());
489
490        let cipher = Aes128GcmSiv::new(&[0u8; 16]).unwrap();
491        let mut buf = [0u8; 8];
492        let mut tag = [0u8; TAG_LEN];
493        assert!(cipher
494            .seal_detached(&[0u8; 11], b"", &mut buf, &mut tag)
495            .is_err());
496        assert!(cipher
497            .seal_detached(&[0u8; 12], b"", &mut buf, &mut [0u8; 15])
498            .is_err());
499    }
500
501    #[test]
502    fn both_self_tests_pass() {
503        Aes128GcmSiv::self_test().unwrap();
504        Aes256GcmSiv::self_test().unwrap();
505    }
506
507    /// The two key sizes must not agree — a build where AES-256-GCM-SIV
508    /// silently used only 16 bytes of key would pass everything above.
509    #[test]
510    fn the_two_key_sizes_are_distinct() {
511        let mut key = [0u8; 32];
512        key[..16].copy_from_slice(&[0x5au8; 16]);
513        let small = Aes128GcmSiv::new(&key[..16]).unwrap();
514        let large = Aes256GcmSiv::new(&key).unwrap();
515
516        let nonce = [0u8; NONCE_LEN];
517        let mut a = *b"message";
518        let mut b = *b"message";
519        let mut ta = [0u8; TAG_LEN];
520        let mut tb = [0u8; TAG_LEN];
521        small.seal_detached(&nonce, b"", &mut a, &mut ta).unwrap();
522        large.seal_detached(&nonce, b"", &mut b, &mut tb).unwrap();
523        assert_ne!((a, ta), (b, tb));
524    }
525}