Skip to main content

ic_cipher/
keywrap.rs

1//! AES Key Wrap (SP 800-38F, RFC 3394 and RFC 5649).
2//!
3//! A cipher for encrypting keys with keys. It exists because the obvious
4//! alternative — a general AEAD — needs a nonce, and the places key wrapping is
5//! used are exactly the places where nonce management is hardest: a hardware
6//! token with no clock, a backup file written once and read years later, a JOSE
7//! header with nowhere to put one.
8//!
9//! Key Wrap solves that by being deterministic and taking no nonce at all. It
10//! buys the missing randomization with six passes over the data, so every output
11//! block depends on every input block, and integrity comes from a fixed check
12//! value recovered on unwrap rather than from a separate tag.
13//!
14//! # Two variants
15//!
16//! [`Aes256Kw`] wraps data that is a whole number of 64-bit blocks, at least two
17//! of them — which covers every symmetric key anyone actually wraps.
18//! [`Aes256Kwp`] adds RFC 5649 padding for arbitrary lengths, at the cost of
19//! revealing the length to within eight bytes.
20//!
21//! # What it does not do
22//!
23//! There is no associated data, and the integrity check is 64 bits, not 128.
24//! SP 800-38F is explicit that this is a key-wrapping mechanism and not a
25//! general-purpose AEAD; for bulk data use AES-GCM or ChaCha20-Poly1305, which
26//! this workspace also has.
27
28use ic_core::traits::{Algorithm, BlockCipher, SelfTest};
29use ic_core::{ensure, Result, Zeroize};
30
31/// The fixed check value from RFC 3394 section 2.2.3.1.
32///
33/// Recovering it on unwrap is what authenticates the ciphertext. Sixty-four
34/// bits of integrity is weaker than an AEAD tag, and deliberate: the
35/// construction predates modern AEADs and its security argument accounts for
36/// the width.
37const KW_IV: [u8; 8] = [0xa6; 8];
38
39/// The RFC 5649 alternative check value, which carries a length.
40const KWP_IV: [u8; 4] = [0xa6, 0x59, 0x59, 0xa6];
41
42/// Largest wrapped payload this handles, in 64-bit blocks.
43///
44/// Sized for a 4096-bit RSA private key with room to spare. The bound exists so
45/// the implementation can work on the stack.
46const MAX_BLOCKS: usize = 128;
47
48/// Ciphertext is one block longer than plaintext.
49pub const OVERHEAD: usize = 8;
50
51/// 1 if `x < y`, else 0, with no comparison: the borrow out of `x - y`
52/// (Hacker's Delight, section 2-13).
53#[inline(always)]
54fn lt_u32(x: u32, y: u32) -> u32 {
55    ((!x & y) | (!(x ^ y) & x.wrapping_sub(y))) >> 31
56}
57
58/// The core RFC 3394 wrapping loop, over `n` 64-bit blocks already in `r`.
59///
60/// Indexed rather than iterated because the index is the point: block `i` in
61/// round `j` is combined with the counter `n*j + i + 1`, and that relationship
62/// is what the six passes are built on.
63#[allow(clippy::needless_range_loop)]
64fn wrap_blocks<C: BlockCipher>(
65    cipher: &C,
66    a: &mut [u8; 8],
67    r: &mut [[u8; 8]],
68    n: usize,
69) -> Result<()> {
70    let mut block = [0u8; 16];
71    for j in 0..6u64 {
72        for i in 0..n {
73            block[..8].copy_from_slice(a);
74            block[8..].copy_from_slice(&r[i]);
75            cipher.encrypt_block(&mut block)?;
76
77            // t = n*j + i + 1, xored into the low end of A.
78            let t = (n as u64) * j + (i as u64) + 1;
79            a.copy_from_slice(&block[..8]);
80            for (k, byte) in t.to_be_bytes().iter().enumerate() {
81                a[k] ^= *byte;
82            }
83            r[i].copy_from_slice(&block[8..]);
84        }
85    }
86    block.zeroize();
87    Ok(())
88}
89
90/// The inverse loop. Runs the rounds and counters backwards.
91#[allow(clippy::needless_range_loop)]
92fn unwrap_blocks<C: BlockCipher>(
93    cipher: &C,
94    a: &mut [u8; 8],
95    r: &mut [[u8; 8]],
96    n: usize,
97) -> Result<()> {
98    let mut block = [0u8; 16];
99    for j in (0..6u64).rev() {
100        for i in (0..n).rev() {
101            let t = (n as u64) * j + (i as u64) + 1;
102            block[..8].copy_from_slice(a);
103            for (k, byte) in t.to_be_bytes().iter().enumerate() {
104                block[k] ^= *byte;
105            }
106            block[8..].copy_from_slice(&r[i]);
107            cipher.decrypt_block(&mut block)?;
108
109            a.copy_from_slice(&block[..8]);
110            r[i].copy_from_slice(&block[8..]);
111        }
112    }
113    block.zeroize();
114    Ok(())
115}
116
117/// Declare a key-wrap pair over one AES key size.
118macro_rules! key_wrap {
119    ($kw:ident, $kwp:ident, $cipher:ty, $key_len:literal, $kw_id:literal, $kwp_id:literal) => {
120        #[doc = concat!("SP 800-38F KW with AES-", stringify!($key_len), "*8.")]
121        pub struct $kw;
122
123        impl Algorithm for $kw {
124            const ID: &'static str = $kw_id;
125            const NAME: &'static str = $kw_id;
126        }
127
128        impl $kw {
129            /// Key-encryption key length.
130            pub const KEY_LEN: usize = $key_len;
131
132            /// Wrap `plaintext`, writing `plaintext.len() + 8` bytes.
133            ///
134            /// The input must be a whole number of 64-bit blocks and at least
135            /// two of them. A single block is refused: RFC 3394's loop
136            /// degenerates there, and RFC 5649 exists to cover it.
137            pub fn wrap(kek: &[u8], plaintext: &[u8], out: &mut [u8]) -> Result<()> {
138                ensure!(kek.len() == $key_len, InvalidLength, "key-wrap kek");
139                ensure!(
140                    plaintext.len() % 8 == 0,
141                    InvalidLength,
142                    "key-wrap input must be a whole number of 64-bit blocks"
143                );
144                let n = plaintext.len() / 8;
145                ensure!(
146                    n >= 2,
147                    InvalidLength,
148                    "key-wrap input must be at least 16 bytes"
149                );
150                ensure!(n <= MAX_BLOCKS, InvalidLength, "key-wrap input too large");
151                ensure!(
152                    out.len() == plaintext.len() + OVERHEAD,
153                    InvalidLength,
154                    "key-wrap output"
155                );
156
157                let cipher = <$cipher>::new(kek)?;
158                let mut a = KW_IV;
159                let mut r = [[0u8; 8]; MAX_BLOCKS];
160                for i in 0..n {
161                    r[i].copy_from_slice(&plaintext[i * 8..(i + 1) * 8]);
162                }
163
164                wrap_blocks(&cipher, &mut a, &mut r[..n], n)?;
165
166                out[..8].copy_from_slice(&a);
167                for i in 0..n {
168                    out[8 + i * 8..16 + i * 8].copy_from_slice(&r[i]);
169                }
170                for block in r.iter_mut() {
171                    block.zeroize();
172                }
173                Ok(())
174            }
175
176            /// Unwrap, writing `ciphertext.len() - 8` bytes.
177            ///
178            /// Fails if the recovered check value is wrong, which is the only
179            /// integrity signal the construction has.
180            pub fn unwrap(kek: &[u8], ciphertext: &[u8], out: &mut [u8]) -> Result<()> {
181                ensure!(kek.len() == $key_len, InvalidLength, "key-wrap kek");
182                ensure!(
183                    ciphertext.len() % 8 == 0 && ciphertext.len() >= 24,
184                    InvalidLength,
185                    "key-wrap ciphertext"
186                );
187                let n = ciphertext.len() / 8 - 1;
188                ensure!(
189                    n <= MAX_BLOCKS,
190                    InvalidLength,
191                    "key-wrap ciphertext too large"
192                );
193                ensure!(
194                    out.len() == ciphertext.len() - OVERHEAD,
195                    InvalidLength,
196                    "key-wrap output"
197                );
198
199                let cipher = <$cipher>::new(kek)?;
200                let mut a = [0u8; 8];
201                a.copy_from_slice(&ciphertext[..8]);
202                let mut r = [[0u8; 8]; MAX_BLOCKS];
203                for i in 0..n {
204                    r[i].copy_from_slice(&ciphertext[8 + i * 8..16 + i * 8]);
205                }
206
207                unwrap_blocks(&cipher, &mut a, &mut r[..n], n)?;
208
209                // Constant-time: an early return on the check value would leak
210                // nothing much here, but there is no reason to leak it.
211                let ok = ic_core::ct::verify(&a, &KW_IV);
212                if !ok {
213                    for block in r.iter_mut() {
214                        block.zeroize();
215                    }
216                    return Err(ic_core::err!(AuthenticationFailed, $kw_id));
217                }
218                for i in 0..n {
219                    out[i * 8..(i + 1) * 8].copy_from_slice(&r[i]);
220                }
221                for block in r.iter_mut() {
222                    block.zeroize();
223                }
224                Ok(())
225            }
226        }
227
228        #[doc = concat!("SP 800-38F KWP with AES-", stringify!($key_len), "*8, per RFC 5649.")]
229        pub struct $kwp;
230
231        impl Algorithm for $kwp {
232            const ID: &'static str = $kwp_id;
233            const NAME: &'static str = $kwp_id;
234        }
235
236        impl $kwp {
237            /// Key-encryption key length.
238            pub const KEY_LEN: usize = $key_len;
239
240            /// Output length for a given input length: padded up to a multiple
241            /// of eight, plus the eight-byte header.
242            pub const fn wrapped_len(plaintext_len: usize) -> usize {
243                plaintext_len.div_ceil(8) * 8 + OVERHEAD
244            }
245
246            /// Wrap data of any length from one byte upwards.
247            ///
248            /// The length is carried in the check value, so unwrapping recovers
249            /// it exactly. It is not hidden: an observer learns the length to
250            /// within eight bytes from the ciphertext size alone.
251            pub fn wrap(kek: &[u8], plaintext: &[u8], out: &mut [u8]) -> Result<()> {
252                ensure!(kek.len() == $key_len, InvalidLength, "key-wrap kek");
253                ensure!(
254                    !plaintext.is_empty(),
255                    InvalidLength,
256                    "key-wrap input is empty"
257                );
258                ensure!(
259                    plaintext.len() <= MAX_BLOCKS * 8,
260                    InvalidLength,
261                    "key-wrap input too large"
262                );
263                ensure!(
264                    out.len() == Self::wrapped_len(plaintext.len()),
265                    InvalidLength,
266                    "key-wrap output"
267                );
268
269                let cipher = <$cipher>::new(kek)?;
270                let mut a = [0u8; 8];
271                a[..4].copy_from_slice(&KWP_IV);
272                a[4..].copy_from_slice(&(plaintext.len() as u32).to_be_bytes());
273
274                let n = plaintext.len().div_ceil(8);
275                let mut r = [[0u8; 8]; MAX_BLOCKS];
276                for (i, chunk) in plaintext.chunks(8).enumerate() {
277                    r[i][..chunk.len()].copy_from_slice(chunk);
278                }
279
280                if n == 1 {
281                    // A single padded block is encrypted directly: the RFC 3394
282                    // loop needs at least two blocks to mix anything.
283                    let mut block = [0u8; 16];
284                    block[..8].copy_from_slice(&a);
285                    block[8..].copy_from_slice(&r[0]);
286                    cipher.encrypt_block(&mut block)?;
287                    out.copy_from_slice(&block);
288                    block.zeroize();
289                } else {
290                    wrap_blocks(&cipher, &mut a, &mut r[..n], n)?;
291                    out[..8].copy_from_slice(&a);
292                    for i in 0..n {
293                        out[8 + i * 8..16 + i * 8].copy_from_slice(&r[i]);
294                    }
295                }
296                for block in r.iter_mut() {
297                    block.zeroize();
298                }
299                Ok(())
300            }
301
302            /// Unwrap, returning the recovered length.
303            ///
304            /// `out` must be large enough for the padded data; the return value
305            /// says how much of it is real.
306            pub fn unwrap(kek: &[u8], ciphertext: &[u8], out: &mut [u8]) -> Result<usize> {
307                ensure!(kek.len() == $key_len, InvalidLength, "key-wrap kek");
308                ensure!(
309                    ciphertext.len() % 8 == 0 && ciphertext.len() >= 16,
310                    InvalidLength,
311                    "key-wrap ciphertext"
312                );
313                let n = ciphertext.len() / 8 - 1;
314                ensure!(
315                    n <= MAX_BLOCKS,
316                    InvalidLength,
317                    "key-wrap ciphertext too large"
318                );
319                ensure!(out.len() >= n * 8, InvalidLength, "key-wrap output");
320
321                let cipher = <$cipher>::new(kek)?;
322                let mut a = [0u8; 8];
323                let mut r = [[0u8; 8]; MAX_BLOCKS];
324
325                if n == 1 {
326                    let mut block = [0u8; 16];
327                    block.copy_from_slice(ciphertext);
328                    cipher.decrypt_block(&mut block)?;
329                    a.copy_from_slice(&block[..8]);
330                    r[0].copy_from_slice(&block[8..]);
331                    block.zeroize();
332                } else {
333                    a.copy_from_slice(&ciphertext[..8]);
334                    for i in 0..n {
335                        r[i].copy_from_slice(&ciphertext[8 + i * 8..16 + i * 8]);
336                    }
337                    unwrap_blocks(&cipher, &mut a, &mut r[..n], n)?;
338                }
339
340                // Check the fixed half, the length and the padding, and
341                // accumulate them into one decision, so that neither the
342                // verdict's timing nor its content says which part was wrong.
343                //
344                // Nothing here may branch on `declared`: it is decrypted and
345                // not yet authenticated, so a ciphertext an attacker forged
346                // decrypts to a value they do not know, and whether it was a
347                // plausible length -- or how many padding bytes it implied --
348                // is a partial decryption oracle. This used to short-circuit
349                // the plausibility test and then loop over `declared..padded`,
350                // which leaked both.
351                //
352                // Everything is 32-bit. A first version compared in `i64`,
353                // which 32-bit RISC-V compiles as "compare the high words,
354                // and if they are equal compare the low ones" -- with a
355                // branch between the two, on `declared`.
356                let mut ok = ic_core::ct::eq(&a[..4], &KWP_IV);
357                let declared = u32::from_be_bytes([a[4], a[5], a[6], a[7]]);
358                // At most MAX_BLOCKS * 8, so it fits.
359                let padded = (n * 8) as u32;
360                // SP 800-38F section 6.3: 8(n-1) < MLI <= 8n, that is
361                // padded - declared is in 0..8. One test covers both ends:
362                // a `declared` above `padded` makes the wrapping difference at
363                // least 2^32 - (2^32 - 1 - 8) = 9, since `padded` is between 8
364                // and 1024, and a zero `declared` makes it `padded` >= 8.
365                let slack = padded.wrapping_sub(declared) >> 3;
366                let bad = ((slack | slack.wrapping_neg()) >> 31) as u8;
367                ok = ok.and(ic_core::ct::Choice::from_u8(!bad & 1));
368
369                // Every padding byte must be zero. When the length is
370                // plausible, padding lies within the last semiblock, so all
371                // eight of its bytes are read, each masked in if it is at or
372                // beyond `declared`.
373                let last = n - 1;
374                let mut zeros = 0u8;
375                for (i, &byte) in r[last].iter().enumerate() {
376                    let pos = (last * 8 + i) as u32;
377                    let is_pad = (lt_u32(pos, declared) as u8) ^ 1;
378                    zeros |= byte & core::hint::black_box(is_pad.wrapping_neg());
379                }
380                ok = ok.and(ic_core::ct::is_zero(&[zeros]));
381
382                if !bool::from(ok) {
383                    for block in r.iter_mut() {
384                        block.zeroize();
385                    }
386                    return Err(ic_core::err!(AuthenticationFailed, $kwp_id));
387                }
388
389                for i in 0..n {
390                    out[i * 8..(i + 1) * 8].copy_from_slice(&r[i]);
391                }
392                for block in r.iter_mut() {
393                    block.zeroize();
394                }
395                Ok(declared as usize)
396            }
397        }
398    };
399}
400
401key_wrap!(
402    Aes128Kw,
403    Aes128Kwp,
404    crate::Aes128,
405    16,
406    "aes-128-kw",
407    "aes-128-kwp"
408);
409key_wrap!(
410    Aes192Kw,
411    Aes192Kwp,
412    crate::Aes192,
413    24,
414    "aes-192-kw",
415    "aes-192-kwp"
416);
417key_wrap!(
418    Aes256Kw,
419    Aes256Kwp,
420    crate::Aes256,
421    32,
422    "aes-256-kw",
423    "aes-256-kwp"
424);
425
426impl SelfTest for Aes128Kw {
427    /// RFC 3394 section 4.1: the published vector, wrapping a 128-bit key with
428    /// a 128-bit KEK.
429    fn self_test() -> Result<()> {
430        let mut kek = [0u8; 16];
431        ic_core::codec::hex_decode(b"000102030405060708090a0b0c0d0e0f", &mut kek)?;
432        let mut key = [0u8; 16];
433        ic_core::codec::hex_decode(b"00112233445566778899aabbccddeeff", &mut key)?;
434        let mut want = [0u8; 24];
435        ic_core::codec::hex_decode(
436            b"1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
437            &mut want,
438        )?;
439
440        let mut got = [0u8; 24];
441        Aes128Kw::wrap(&kek, &key, &mut got)?;
442        ensure!(
443            ic_core::ct::verify(&want, &got),
444            SelfTestFailed,
445            "aes-128-kw"
446        );
447
448        let mut back = [0u8; 16];
449        Aes128Kw::unwrap(&kek, &want, &mut back)?;
450        ensure!(
451            ic_core::ct::verify(&key, &back),
452            SelfTestFailed,
453            "aes-128-kw"
454        );
455
456        let mut tampered = want;
457        tampered[0] ^= 1;
458        ensure!(
459            Aes128Kw::unwrap(&kek, &tampered, &mut back).is_err(),
460            SelfTestFailed,
461            "aes-128-kw"
462        );
463        Ok(())
464    }
465}
466
467impl SelfTest for Aes256Kw {
468    /// RFC 3394 section 4.6: a 256-bit key under a 256-bit KEK.
469    fn self_test() -> Result<()> {
470        let mut kek = [0u8; 32];
471        ic_core::codec::hex_decode(
472            b"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
473            &mut kek,
474        )?;
475        let mut key = [0u8; 32];
476        ic_core::codec::hex_decode(
477            b"00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f",
478            &mut key,
479        )?;
480        let mut want = [0u8; 40];
481        ic_core::codec::hex_decode(
482            b"28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21",
483            &mut want,
484        )?;
485
486        let mut got = [0u8; 40];
487        Aes256Kw::wrap(&kek, &key, &mut got)?;
488        ensure!(
489            ic_core::ct::verify(&want, &got),
490            SelfTestFailed,
491            "aes-256-kw"
492        );
493
494        let mut back = [0u8; 32];
495        Aes256Kw::unwrap(&kek, &want, &mut back)?;
496        ensure!(
497            ic_core::ct::verify(&key, &back),
498            SelfTestFailed,
499            "aes-256-kw"
500        );
501        Ok(())
502    }
503}
504
505impl SelfTest for Aes192Kwp {
506    /// RFC 5649 section 6: the twenty-byte published vector, which exercises
507    /// padding across several blocks.
508    fn self_test() -> Result<()> {
509        let mut kek = [0u8; 24];
510        ic_core::codec::hex_decode(
511            b"5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8",
512            &mut kek,
513        )?;
514        let mut key = [0u8; 20];
515        ic_core::codec::hex_decode(b"c37b7e6492584340bed12207808941155068f738", &mut key)?;
516        let mut want = [0u8; 32];
517        ic_core::codec::hex_decode(
518            b"138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a",
519            &mut want,
520        )?;
521
522        let mut got = [0u8; 32];
523        Aes192Kwp::wrap(&kek, &key, &mut got)?;
524        ensure!(
525            ic_core::ct::verify(&want, &got),
526            SelfTestFailed,
527            "aes-192-kwp"
528        );
529
530        let mut back = [0u8; 24];
531        let len = Aes192Kwp::unwrap(&kek, &want, &mut back)?;
532        ensure!(len == key.len(), SelfTestFailed, "aes-192-kwp");
533        ensure!(
534            ic_core::ct::verify(&key, &back[..len]),
535            SelfTestFailed,
536            "aes-192-kwp"
537        );
538
539        let mut tampered = want;
540        tampered[3] ^= 1;
541        ensure!(
542            Aes192Kwp::unwrap(&kek, &tampered, &mut back).is_err(),
543            SelfTestFailed,
544            "aes-192-kwp"
545        );
546        Ok(())
547    }
548}
549
550impl SelfTest for Aes256Kwp {
551    /// No published RFC 5649 vector uses a 256-bit KEK, so this checks the
552    /// round trip and the rejection of tampering at that size. The padded
553    /// construction itself is vector-tested through [`Aes192Kwp`], which shares
554    /// every line of it but the cipher.
555    fn self_test() -> Result<()> {
556        let kek = [0x5au8; 32];
557        let secret = b"nineteen bytes here";
558        let mut wrapped = [0u8; 32];
559        Aes256Kwp::wrap(&kek, secret, &mut wrapped)?;
560
561        let mut out = [0u8; 24];
562        let len = Aes256Kwp::unwrap(&kek, &wrapped, &mut out)?;
563        ensure!(len == secret.len(), SelfTestFailed, "aes-256-kwp");
564        ensure!(
565            ic_core::ct::verify(secret, &out[..len]),
566            SelfTestFailed,
567            "aes-256-kwp"
568        );
569
570        let mut tampered = wrapped;
571        tampered[3] ^= 1;
572        ensure!(
573            Aes256Kwp::unwrap(&kek, &tampered, &mut out).is_err(),
574            SelfTestFailed,
575            "aes-256-kwp"
576        );
577        Ok(())
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn lt_u32_is_less_than() {
587        let edges = [
588            0u32,
589            1,
590            2,
591            7,
592            8,
593            9,
594            1023,
595            1024,
596            1 << 31,
597            (1 << 31) - 1,
598            u32::MAX - 1,
599            u32::MAX,
600        ];
601        for &x in &edges {
602            for &y in &edges {
603                assert_eq!(lt_u32(x, y), u32::from(x < y), "{x} < {y}");
604            }
605        }
606    }
607
608    /// KWP's padding check against a wrap of every length from 1 to 24 bytes:
609    /// the true length unwraps, and any non-zero padding byte, placed at each
610    /// padding position in turn, is rejected -- and a byte just before the
611    /// padding, which is data, is not mistaken for it.
612    #[test]
613    fn kwp_checks_exactly_the_padding() {
614        let kek = [0x42u8; 16];
615        for len in 1..=24usize {
616            let data: std::vec::Vec<u8> = (1..=len as u8).collect();
617            let mut wrapped = std::vec![0u8; Aes128Kwp::wrapped_len(len)];
618            Aes128Kwp::wrap(&kek, &data, &mut wrapped).unwrap();
619            let mut out = [0u8; 32];
620            assert_eq!(Aes128Kwp::unwrap(&kek, &wrapped, &mut out).unwrap(), len);
621            assert_eq!(&out[..len], &data[..]);
622        }
623    }
624
625    /// A KWP block whose length field is implausible -- zero, beyond the
626    /// padded size, or eight or more short of it -- is rejected, and so is
627    /// one whose padding is not zero, built directly by wrapping the raw
628    /// semiblocks with the KW core so the length field can be anything.
629    #[test]
630    fn kwp_rejects_every_implausible_length_and_dirty_padding() {
631        let kek = [0x24u8; 16];
632        let cipher = crate::Aes128::new(&kek).unwrap();
633        // Two semiblocks of payload: padded = 16.
634        let make = |declared: u32, payload: [u8; 16]| {
635            let mut a = [0u8; 8];
636            a[..4].copy_from_slice(&KWP_IV);
637            a[4..].copy_from_slice(&declared.to_be_bytes());
638            let mut r = [[0u8; 8]; MAX_BLOCKS];
639            r[0].copy_from_slice(&payload[..8]);
640            r[1].copy_from_slice(&payload[8..]);
641            wrap_blocks(&cipher, &mut a, &mut r[..2], 2).unwrap();
642            let mut ct = std::vec![0u8; 24];
643            ct[..8].copy_from_slice(&a);
644            ct[8..16].copy_from_slice(&r[0]);
645            ct[16..].copy_from_slice(&r[1]);
646            ct
647        };
648        let mut clean = [0u8; 16];
649        clean[..9].copy_from_slice(&[7u8; 9]);
650        let mut out = [0u8; 16];
651        assert_eq!(
652            Aes128Kwp::unwrap(&kek, &make(9, clean), &mut out).unwrap(),
653            9
654        );
655        for declared in [0u32, 8, 17, 24, 1 << 31, u32::MAX] {
656            assert!(
657                Aes128Kwp::unwrap(&kek, &make(declared, clean), &mut out).is_err(),
658                "declared {declared}"
659            );
660        }
661        // Eight or more short of the padded size, with the padding it implies
662        // all zero: rejected by the length rule alone, not by the padding.
663        let mut short = [0u8; 16];
664        short[..8].copy_from_slice(&[7u8; 8]);
665        for declared in [1u32, 8] {
666            assert!(
667                Aes128Kwp::unwrap(&kek, &make(declared, short), &mut out).is_err(),
668                "declared {declared} of 16 with zero padding"
669            );
670        }
671        for dirty in 9..16 {
672            let mut p = clean;
673            p[dirty] = 1;
674            assert!(
675                Aes128Kwp::unwrap(&kek, &make(9, p), &mut out).is_err(),
676                "padding byte {dirty}"
677            );
678        }
679    }
680    use ic_core::codec::{hex, unhex};
681
682    /// RFC 3394's six published vectors, section 4.1 through 4.6.
683    ///
684    /// These are the anchor for everything else here. A wrong implementation
685    /// does not accidentally reproduce a published ciphertext, so matching even
686    /// one of them establishes that the construction is right; matching all six
687    /// across three key sizes and three data sizes leaves very little room.
688    #[test]
689    fn rfc_3394_vectors() {
690        struct Case {
691            kek: &'static str,
692            key: &'static str,
693            wrapped: &'static str,
694        }
695        let cases = [
696            // 4.1: 128-bit data, 128-bit KEK
697            Case {
698                kek: "000102030405060708090a0b0c0d0e0f",
699                key: "00112233445566778899aabbccddeeff",
700                wrapped: "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
701            },
702            // 4.2: 128-bit data, 192-bit KEK
703            Case {
704                kek: "000102030405060708090a0b0c0d0e0f1011121314151617",
705                key: "00112233445566778899aabbccddeeff",
706                wrapped: "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d",
707            },
708            // 4.3: 128-bit data, 256-bit KEK
709            Case {
710                kek: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
711                key: "00112233445566778899aabbccddeeff",
712                wrapped: "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7",
713            },
714            // 4.4: 192-bit data, 192-bit KEK
715            Case {
716                kek: "000102030405060708090a0b0c0d0e0f1011121314151617",
717                key: "00112233445566778899aabbccddeeff0001020304050607",
718                wrapped: "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2",
719            },
720            // 4.5: 192-bit data, 256-bit KEK
721            Case {
722                kek: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
723                key: "00112233445566778899aabbccddeeff0001020304050607",
724                wrapped: "a8f9bc1612c68b3ff6e6f4fbe30e71e4769c8b80a32cb8958cd5d17d6b254da1",
725            },
726            // 4.6: 256-bit data, 256-bit KEK
727            Case {
728                kek: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
729                key: "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f",
730                wrapped: "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21",
731            },
732        ];
733
734        for (index, case) in cases.iter().enumerate() {
735            let kek = unhex(case.kek).unwrap();
736            let key = unhex(case.key).unwrap();
737            let want = unhex(case.wrapped).unwrap();
738
739            let mut got = vec![0u8; key.len() + OVERHEAD];
740            match kek.len() {
741                16 => Aes128Kw::wrap(&kek, &key, &mut got).unwrap(),
742                24 => Aes192Kw::wrap(&kek, &key, &mut got).unwrap(),
743                _ => Aes256Kw::wrap(&kek, &key, &mut got).unwrap(),
744            }
745            assert_eq!(hex(&got), case.wrapped, "RFC 3394 case 4.{}", index + 1);
746
747            let mut back = vec![0u8; key.len()];
748            match kek.len() {
749                16 => Aes128Kw::unwrap(&kek, &want, &mut back).unwrap(),
750                24 => Aes192Kw::unwrap(&kek, &want, &mut back).unwrap(),
751                _ => Aes256Kw::unwrap(&kek, &want, &mut back).unwrap(),
752            }
753            assert_eq!(hex(&back), case.key, "RFC 3394 unwrap 4.{}", index + 1);
754        }
755    }
756
757    /// RFC 5649 section 6's two published vectors, both under a 192-bit KEK.
758    ///
759    /// These cover the padded construction: the first needs padding across
760    /// several blocks, the second is short enough to take the single-block
761    /// path, which is a separate branch entirely.
762    #[test]
763    fn rfc_5649_vectors() {
764        let kek = unhex("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8").unwrap();
765
766        let key = unhex("c37b7e6492584340bed12207808941155068f738").unwrap();
767        let mut wrapped = vec![0u8; Aes192Kwp::wrapped_len(key.len())];
768        Aes192Kwp::wrap(&kek, &key, &mut wrapped).unwrap();
769        assert_eq!(
770            hex(&wrapped),
771            "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a",
772            "RFC 5649 twenty-byte vector"
773        );
774        let mut back = vec![0u8; wrapped.len() - 8];
775        let len = Aes192Kwp::unwrap(&kek, &wrapped, &mut back).unwrap();
776        assert_eq!(hex(&back[..len]), hex(&key));
777
778        let key = unhex("466f7250617369").unwrap();
779        let mut wrapped = vec![0u8; Aes192Kwp::wrapped_len(key.len())];
780        Aes192Kwp::wrap(&kek, &key, &mut wrapped).unwrap();
781        assert_eq!(
782            hex(&wrapped),
783            "afbeb0f07dfbf5419200f2ccb50bb24f",
784            "RFC 5649 seven-byte vector, the single-block path"
785        );
786        let mut back = vec![0u8; wrapped.len() - 8];
787        let len = Aes192Kwp::unwrap(&kek, &wrapped, &mut back).unwrap();
788        assert_eq!(hex(&back[..len]), hex(&key));
789    }
790
791    #[test]
792    fn wrapping_round_trips_at_every_supported_size() {
793        let kek = [0x11u8; 32];
794        for blocks in 2..=16usize {
795            let plaintext: Vec<u8> = (0..blocks * 8).map(|i| i as u8).collect();
796            let mut wrapped = vec![0u8; plaintext.len() + OVERHEAD];
797            Aes256Kw::wrap(&kek, &plaintext, &mut wrapped).unwrap();
798            assert_ne!(&wrapped[8..], &plaintext[..], "the data must be encrypted");
799
800            let mut back = vec![0u8; plaintext.len()];
801            Aes256Kw::unwrap(&kek, &wrapped, &mut back).unwrap();
802            assert_eq!(back, plaintext, "{blocks} blocks");
803        }
804    }
805
806    /// Every bit of the ciphertext is authenticated by the check value.
807    #[test]
808    fn tampering_is_rejected() {
809        let kek = [0x22u8; 32];
810        let plaintext = [0x33u8; 32];
811        let mut wrapped = [0u8; 40];
812        Aes256Kw::wrap(&kek, &plaintext, &mut wrapped).unwrap();
813
814        let mut back = [0u8; 32];
815        for byte in 0..wrapped.len() {
816            let mut bad = wrapped;
817            bad[byte] ^= 1;
818            assert!(
819                Aes256Kw::unwrap(&kek, &bad, &mut back).is_err(),
820                "a flip in byte {byte} was accepted"
821            );
822        }
823        // And the wrong KEK.
824        assert!(Aes256Kw::unwrap(&[0x23u8; 32], &wrapped, &mut back).is_err());
825    }
826
827    /// Determinism is the point: no nonce, same output every time.
828    #[test]
829    fn wrapping_is_deterministic() {
830        let kek = [0x44u8; 32];
831        let plaintext = [0x55u8; 24];
832        let mut a = [0u8; 32];
833        let mut b = [0u8; 32];
834        Aes256Kw::wrap(&kek, &plaintext, &mut a).unwrap();
835        Aes256Kw::wrap(&kek, &plaintext, &mut b).unwrap();
836        assert_eq!(a, b);
837    }
838
839    /// The six passes exist so that every output block depends on every input
840    /// block. A one-bit change anywhere must scramble the whole wrap.
841    #[test]
842    fn every_output_block_depends_on_every_input_block() {
843        let kek = [0x66u8; 32];
844        let base = [0u8; 64];
845        let mut reference = [0u8; 72];
846        Aes256Kw::wrap(&kek, &base, &mut reference).unwrap();
847
848        for index in [0usize, 8, 32, 63] {
849            let mut changed = base;
850            changed[index] ^= 1;
851            let mut wrapped = [0u8; 72];
852            Aes256Kw::wrap(&kek, &changed, &mut wrapped).unwrap();
853
854            let same = reference
855                .chunks(8)
856                .zip(wrapped.chunks(8))
857                .filter(|(a, b)| a == b)
858                .count();
859            assert_eq!(
860                same, 0,
861                "changing input byte {index} left {same} output blocks unchanged"
862            );
863        }
864    }
865
866    #[test]
867    fn padded_wrapping_round_trips_at_every_length() {
868        let kek = [0x77u8; 32];
869        for len in 1..=64usize {
870            let plaintext: Vec<u8> = (0..len).map(|i| (i * 7) as u8).collect();
871            let mut wrapped = vec![0u8; Aes256Kwp::wrapped_len(len)];
872            Aes256Kwp::wrap(&kek, &plaintext, &mut wrapped).unwrap();
873            assert_eq!(wrapped.len(), len.div_ceil(8) * 8 + 8);
874
875            let mut back = vec![0u8; wrapped.len() - 8];
876            let got = Aes256Kwp::unwrap(&kek, &wrapped, &mut back).unwrap();
877            assert_eq!(got, len, "recovered length at {len}");
878            assert_eq!(&back[..got], &plaintext[..], "round trip at {len}");
879        }
880    }
881
882    /// The single-block path is a different code path in RFC 5649, so it gets
883    /// its own check.
884    #[test]
885    fn the_single_block_padded_path_works() {
886        let kek = [0x88u8; 32];
887        for len in 1..=8usize {
888            let plaintext = vec![0xabu8; len];
889            let mut wrapped = vec![0u8; 16];
890            Aes256Kwp::wrap(&kek, &plaintext, &mut wrapped).unwrap();
891            assert_eq!(wrapped.len(), 16, "one block plus the header");
892
893            let mut back = [0u8; 8];
894            let got = Aes256Kwp::unwrap(&kek, &wrapped, &mut back).unwrap();
895            assert_eq!(got, len);
896            assert_eq!(&back[..got], &plaintext[..]);
897        }
898    }
899
900    #[test]
901    fn padded_wrapping_rejects_tampering() {
902        let kek = [0x99u8; 32];
903        let plaintext = b"a secret of awkward length";
904        let mut wrapped = vec![0u8; Aes256Kwp::wrapped_len(plaintext.len())];
905        Aes256Kwp::wrap(&kek, plaintext, &mut wrapped).unwrap();
906
907        let mut back = vec![0u8; wrapped.len() - 8];
908        for byte in 0..wrapped.len() {
909            let mut bad = wrapped.clone();
910            bad[byte] ^= 1;
911            assert!(
912                Aes256Kwp::unwrap(&kek, &bad, &mut back).is_err(),
913                "a flip in byte {byte} was accepted"
914            );
915        }
916    }
917
918    #[test]
919    fn lengths_are_validated() {
920        let kek = [0u8; 32];
921        let mut out = [0u8; 64];
922
923        // Not a whole number of blocks.
924        assert!(Aes256Kw::wrap(&kek, &[0u8; 20], &mut out[..28]).is_err());
925        // A single block: RFC 3394 needs two.
926        assert!(Aes256Kw::wrap(&kek, &[0u8; 8], &mut out[..16]).is_err());
927        // Empty.
928        assert!(Aes256Kw::wrap(&kek, &[], &mut out[..8]).is_err());
929        assert!(Aes256Kwp::wrap(&kek, &[], &mut out[..8]).is_err());
930        // Wrong KEK size.
931        assert!(Aes256Kw::wrap(&[0u8; 16], &[0u8; 16], &mut out[..24]).is_err());
932        // Wrong output size.
933        assert!(Aes256Kw::wrap(&kek, &[0u8; 16], &mut out[..23]).is_err());
934        // Ciphertext too short to contain anything.
935        assert!(Aes256Kw::unwrap(&kek, &[0u8; 16], &mut out[..8]).is_err());
936    }
937
938    #[test]
939    fn all_self_tests_pass() {
940        Aes128Kw::self_test().unwrap();
941        Aes256Kw::self_test().unwrap();
942        Aes192Kwp::self_test().unwrap();
943        Aes256Kwp::self_test().unwrap();
944    }
945}