Skip to main content

ic_cipher/
sealer.rs

1//! An AEAD that chooses its own nonces.
2//!
3//! Every [`Aead`] takes a nonce from its caller, and under AES-GCM or
4//! ChaCha20-Poly1305 a nonce used twice under one key is catastrophic: GCM
5//! leaks its authentication subkey, ChaCha20 XORs the two plaintexts. The rule
6//! is easy to state and easy to break, because nothing in `seal_detached`'s
7//! signature tells you that the bytes you pass it are the dangerous ones.
8//!
9//! [`Sealer`] takes the nonce out of the caller's hands. It builds each one as
10//! SP 800-38D section 8.2.1's deterministic construction does: a 4-byte fixed
11//! field naming the sender, then a 64-bit invocation counter, big-endian. It
12//! returns the nonce it used, so the receiver can be sent it. It refuses rather
13//! than wrap when the counter runs out.
14//!
15//! [`Opener`] is the receiving side. It accepts only the sender's fixed field
16//! and only counters it has not passed, so a replayed or reordered message is
17//! refused before the AEAD runs.
18//!
19//! ```
20//! use ic_cipher::{Aes256Gcm, sealer::{Opener, Sealer}};
21//!
22//! // A key for this session only: from a key exchange and a KDF, not a constant.
23//! let key = [0x2a; 32];
24//! let mut tx = Sealer::<Aes256Gcm>::new(&key, *b"c->s")?;
25//! let mut rx = Opener::<Aes256Gcm>::new(&key, *b"c->s")?;
26//!
27//! let mut msg = *b"ship it";
28//! let mut tag = [0u8; 16];
29//! let nonce = tx.seal(b"header", &mut msg, &mut tag)?;
30//! rx.open(&nonce, b"header", &mut msg, &tag)?;
31//! assert_eq!(&msg, b"ship it");
32//!
33//! // The same message again is a replay, and is refused.
34//! assert!(rx.open(&nonce, b"header", &mut msg, &tag).is_err());
35//! # Ok::<(), ic_core::Error>(())
36//! ```
37//!
38//! ## What the counter cannot know
39//!
40//! A counter is unique only within the object that holds it. Two `Sealer`s on
41//! the same key and fixed field both start at zero and reuse every nonce, and a
42//! process that restarts and builds a new one does the same. So:
43//!
44//! - **Use a key that lives no longer than its `Sealer`**: one derived for the
45//!   session, as TLS 1.3 and HPKE do. A long-lived key needs the counter
46//!   persisted and restored with [`Sealer::resume`], or a nonce-misuse-resistant
47//!   AEAD such as AES-256-GCM-SIV.
48//! - **Give every sender under one key its own fixed field**, such as one per
49//!   direction. Better still, give each direction its own key.
50//!
51//! The 12-byte nonce is required: it is the length every AEAD here takes and
52//! the one SP 800-38D's construction is defined for.
53
54use core::fmt;
55use ic_core::traits::Aead;
56use ic_core::{ensure, Result};
57
58/// The nonce length a sealer builds: a 4-byte fixed field and an 8-byte counter.
59pub const NONCE_LEN: usize = 12;
60
61/// The fixed field's length.
62pub const FIXED_LEN: usize = 4;
63
64/// Assemble `fixed || counter`, refusing the last counter value so that a
65/// sealer which has used it cannot wrap.
66fn nonce(fixed: &[u8; FIXED_LEN], counter: u64) -> Result<[u8; NONCE_LEN]> {
67    ensure!(
68        counter != u64::MAX,
69        CounterExhausted,
70        "sealer counter exhausted; it never reuses a nonce"
71    );
72    let mut n = [0u8; NONCE_LEN];
73    n[..FIXED_LEN].copy_from_slice(fixed);
74    n[FIXED_LEN..].copy_from_slice(&counter.to_be_bytes());
75    Ok(n)
76}
77
78/// Check that `A` takes the 12-byte nonce this construction builds.
79fn check_nonce_len<A: Aead>() -> Result<()> {
80    ensure!(
81        A::NONCE_LEN == NONCE_LEN,
82        Unsupported,
83        "sealer needs an AEAD with a 12-byte nonce"
84    );
85    Ok(())
86}
87
88/// Seals messages under nonces it builds itself, each used once.
89pub struct Sealer<A: Aead> {
90    aead: A,
91    fixed: [u8; FIXED_LEN],
92    counter: u64,
93}
94
95impl<A: Aead> Sealer<A> {
96    /// A sealer whose first nonce is `fixed || 0`.
97    ///
98    /// `fixed` names this sender. Any other party sealing under `key` must use
99    /// a different one.
100    pub fn new(key: &[u8], fixed: [u8; FIXED_LEN]) -> Result<Self> {
101        Self::resume(key, fixed, 0)
102    }
103
104    /// A sealer that continues from `counter`, the value a previous sealer on
105    /// this key and fixed field reported from [`Sealer::counter`] when it was
106    /// last used.
107    ///
108    /// Restoring a counter lower than one already used reuses nonces. Persist
109    /// the counter before sending what it sealed, not after.
110    pub fn resume(key: &[u8], fixed: [u8; FIXED_LEN], counter: u64) -> Result<Self> {
111        check_nonce_len::<A>()?;
112        Ok(Self {
113            aead: A::new(key)?,
114            fixed,
115            counter,
116        })
117    }
118
119    /// The counter the next seal will use.
120    pub fn counter(&self) -> u64 {
121        self.counter
122    }
123
124    /// Encrypt `in_out` in place, write the tag, and return the nonce used.
125    ///
126    /// The receiver needs the nonce, or the counter, to open the message. On
127    /// failure the counter does not advance.
128    pub fn seal(
129        &mut self,
130        aad: &[u8],
131        in_out: &mut [u8],
132        tag: &mut [u8],
133    ) -> Result<[u8; NONCE_LEN]> {
134        let n = nonce(&self.fixed, self.counter)?;
135        self.aead.seal_detached(&n, aad, in_out, tag)?;
136        self.counter += 1;
137        Ok(n)
138    }
139}
140
141impl<A: Aead> fmt::Debug for Sealer<A> {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.debug_struct("Sealer")
144            .field("fixed", &self.fixed)
145            .field("counter", &self.counter)
146            .finish_non_exhaustive()
147    }
148}
149
150/// Opens messages from one [`Sealer`], refusing replays and reordering.
151pub struct Opener<A: Aead> {
152    aead: A,
153    fixed: [u8; FIXED_LEN],
154    next: u64,
155}
156
157impl<A: Aead> Opener<A> {
158    /// An opener for the sender whose fixed field is `fixed`.
159    pub fn new(key: &[u8], fixed: [u8; FIXED_LEN]) -> Result<Self> {
160        check_nonce_len::<A>()?;
161        Ok(Self {
162            aead: A::new(key)?,
163            fixed,
164            next: 0,
165        })
166    }
167
168    /// The lowest counter the next open will accept.
169    pub fn next_counter(&self) -> u64 {
170        self.next
171    }
172
173    /// Verify and decrypt `in_out` in place.
174    ///
175    /// Refuses a nonce with another sender's fixed field, or a counter lower
176    /// than the last one opened plus one, before the AEAD runs. Counters may
177    /// skip forward, so a lost message does not stall the stream, but never go
178    /// back. On any failure the accepted counter does not advance, and
179    /// `in_out` holds no unauthenticated plaintext.
180    pub fn open(&mut self, nonce: &[u8], aad: &[u8], in_out: &mut [u8], tag: &[u8]) -> Result<()> {
181        ensure!(
182            nonce.len() == NONCE_LEN,
183            InvalidLength,
184            "sealer nonce must be 12 bytes"
185        );
186        // Neither the fixed field nor the counter is secret: both travel in
187        // the clear beside the ciphertext.
188        ensure!(
189            nonce[..FIXED_LEN] == self.fixed,
190            AuthenticationFailed,
191            "sealer nonce is from another sender"
192        );
193        let mut c = [0u8; 8];
194        c.copy_from_slice(&nonce[FIXED_LEN..]);
195        let counter = u64::from_be_bytes(c);
196        ensure!(
197            counter >= self.next && counter != u64::MAX,
198            AuthenticationFailed,
199            "sealer nonce replayed or out of order"
200        );
201        self.aead.open_detached(nonce, aad, in_out, tag)?;
202        self.next = counter + 1;
203        Ok(())
204    }
205}
206
207impl<A: Aead> fmt::Debug for Opener<A> {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.debug_struct("Opener")
210            .field("fixed", &self.fixed)
211            .field("next", &self.next)
212            .finish_non_exhaustive()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::{Aes128Gcm, Aes256Gcm};
220    #[cfg(not(ic_approved_only))]
221    use crate::{Aes256GcmSiv, ChaCha20Poly1305};
222    use ic_core::ErrorKind;
223
224    const KEY: [u8; 32] = [7; 32];
225
226    fn seal_one<A: Aead>(tx: &mut Sealer<A>, msg: &[u8]) -> ([u8; NONCE_LEN], [u8; 64], [u8; 16]) {
227        let mut buf = [0u8; 64];
228        buf[..msg.len()].copy_from_slice(msg);
229        let mut tag = [0u8; 16];
230        let n = tx.seal(b"aad", &mut buf[..msg.len()], &mut tag).unwrap();
231        (n, buf, tag)
232    }
233
234    /// The nonce is `fixed || counter` big-endian, and the counter advances
235    /// by one per seal: SP 800-38D 8.2.1's deterministic construction.
236    #[test]
237    fn nonces_are_the_fixed_field_then_a_counter() {
238        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
239        for i in 0..3u64 {
240            let (n, _, _) = seal_one(&mut tx, b"m");
241            assert_eq!(&n[..4], b"abcd");
242            assert_eq!(n[4..], i.to_be_bytes());
243        }
244        assert_eq!(tx.counter(), 3);
245    }
246
247    /// What a sealer produces is what the bare AEAD produces under the nonce
248    /// it reports, so it adds no format of its own.
249    #[test]
250    fn a_seal_is_the_bare_aead_under_the_reported_nonce() {
251        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
252        seal_one(&mut tx, b"first");
253        let (n, ct, tag) = seal_one(&mut tx, b"second");
254        let bare = Aes256Gcm::new(&KEY).unwrap();
255        let mut buf = *b"second";
256        let mut t = [0u8; 16];
257        bare.seal_detached(&n, b"aad", &mut buf, &mut t).unwrap();
258        assert_eq!(&ct[..6], &buf);
259        assert_eq!(tag, t);
260    }
261
262    #[test]
263    fn every_twelve_byte_aead_round_trips() {
264        fn round<A: Aead>() {
265            let key = [9u8; 32];
266            let key = &key[..A::KEY_LEN];
267            let mut tx = Sealer::<A>::new(key, *b"wxyz").unwrap();
268            let mut rx = Opener::<A>::new(key, *b"wxyz").unwrap();
269            for _ in 0..3 {
270                let (n, mut ct, tag) = seal_one(&mut tx, b"hello");
271                rx.open(&n, b"aad", &mut ct[..5], &tag).unwrap();
272                assert_eq!(&ct[..5], b"hello");
273            }
274        }
275        round::<Aes128Gcm>();
276        round::<Aes256Gcm>();
277        #[cfg(not(ic_approved_only))]
278        round::<ChaCha20Poly1305>();
279        #[cfg(not(ic_approved_only))]
280        round::<Aes256GcmSiv>();
281    }
282
283    #[test]
284    fn a_replay_or_an_earlier_counter_is_refused() {
285        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
286        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
287        let (n0, ct0, t0) = seal_one(&mut tx, b"zero");
288        let (n1, ct1, t1) = seal_one(&mut tx, b"one!");
289        let mut buf = ct1;
290        rx.open(&n1, b"aad", &mut buf[..4], &t1).unwrap();
291        assert_eq!(rx.next_counter(), 2);
292        // The same message again: a replay.
293        let mut buf = ct1;
294        let e = rx.open(&n1, b"aad", &mut buf[..4], &t1).unwrap_err();
295        assert_eq!(e.kind(), ErrorKind::AuthenticationFailed);
296        // Counter 0 arriving late: refused, though authentic.
297        let mut buf = ct0;
298        let e = rx.open(&n0, b"aad", &mut buf[..4], &t0).unwrap_err();
299        assert_eq!(e.kind(), ErrorKind::AuthenticationFailed);
300        assert_eq!(rx.next_counter(), 2, "a refusal does not move the window");
301    }
302
303    #[test]
304    fn a_skipped_counter_is_accepted_so_a_lost_message_does_not_stall() {
305        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
306        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
307        seal_one(&mut tx, b"lost");
308        let (n, mut ct, t) = seal_one(&mut tx, b"kept");
309        rx.open(&n, b"aad", &mut ct[..4], &t).unwrap();
310        assert_eq!(&ct[..4], b"kept");
311    }
312
313    #[test]
314    fn another_senders_fixed_field_is_refused() {
315        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"s->c").unwrap();
316        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"c->s").unwrap();
317        let (n, mut ct, t) = seal_one(&mut tx, b"hi");
318        assert!(rx.open(&n, b"aad", &mut ct[..2], &t).is_err());
319        assert_eq!(rx.next_counter(), 0);
320    }
321
322    #[test]
323    fn a_forgery_does_not_advance_the_opener() {
324        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
325        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
326        let (n, mut ct, mut t) = seal_one(&mut tx, b"hi");
327        t[0] ^= 1;
328        assert!(rx.open(&n, b"aad", &mut ct[..2], &t).is_err());
329        assert_eq!(rx.next_counter(), 0);
330    }
331
332    /// The last counter value is never used, so a sealer cannot wrap to zero.
333    #[test]
334    fn an_exhausted_sealer_refuses_rather_than_wrapping() {
335        let mut tx = Sealer::<Aes256Gcm>::resume(&KEY, *b"abcd", u64::MAX - 1).unwrap();
336        seal_one(&mut tx, b"last");
337        let mut buf = *b"over";
338        let mut tag = [0u8; 16];
339        let e = tx.seal(b"", &mut buf, &mut tag).unwrap_err();
340        assert_eq!(e.kind(), ErrorKind::CounterExhausted);
341        assert_eq!(tx.counter(), u64::MAX);
342    }
343
344    #[test]
345    fn resume_continues_where_the_last_sealer_stopped() {
346        let mut a = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
347        seal_one(&mut a, b"0");
348        seal_one(&mut a, b"1");
349        let mut b = Sealer::<Aes256Gcm>::resume(&KEY, *b"abcd", a.counter()).unwrap();
350        let (n, _, _) = seal_one(&mut b, b"2");
351        assert_eq!(n[4..], 2u64.to_be_bytes());
352    }
353
354    #[test]
355    fn debug_shows_no_key() {
356        let tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
357        let s = format!("{tx:?}");
358        assert!(s.contains("counter") && !s.contains("aead"), "{s}");
359    }
360}