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