Skip to main content

ic_kdf/
kbkdf.rs

1//! SP 800-108 KDF in counter mode.
2//!
3//! This expands a key-derivation key into multiple application keys, and is the
4//! mechanism behind the key hierarchies in TLS exporters, IKEv2, and most
5//! HSM-backed designs.
6
7use ic_core::traits::Mac;
8use ic_core::{ensure, Result, Zeroize};
9
10const MAX_TAG_LEN: usize = 64;
11
12/// SP 800-108 counter-mode KDF with a 32-bit big-endian counter.
13///
14/// Each block is `PRF(key, [i]_32 || label || 0x00 || context || [L]_32)`,
15/// where `L` is the total output length in bits. The `0x00` separator makes
16/// `label` and `context` unambiguously parseable, which is what stops two
17/// different `(label, context)` pairs from deriving the same key.
18pub fn kbkdf_counter<M: Mac>(
19    key: &[u8],
20    label: &[u8],
21    context: &[u8],
22    out: &mut [u8],
23) -> Result<()> {
24    ic_core::module::operational()?;
25    ensure!(!out.is_empty(), InvalidLength, "kbkdf output");
26    ensure!(
27        M::TAG_LEN <= MAX_TAG_LEN,
28        InvalidParameter,
29        "mac tag too wide"
30    );
31    ensure!(
32        !label.contains(&0),
33        InvalidParameter,
34        "kbkdf label must not contain a zero byte"
35    );
36
37    let total_bits = (out.len() as u64)
38        .checked_mul(8)
39        .and_then(|b| u32::try_from(b).ok())
40        .ok_or(ic_core::err!(InvalidLength, "kbkdf output too long"))?;
41
42    let blocks = out.len().div_ceil(M::TAG_LEN);
43    ensure!(
44        blocks <= u32::MAX as usize,
45        CounterExhausted,
46        "kbkdf counter"
47    );
48
49    let mut scratch = [0u8; MAX_TAG_LEN];
50    for (i, chunk) in out.chunks_mut(M::TAG_LEN).enumerate() {
51        let counter = (i as u32) + 1;
52        let mut m = M::new(key)?;
53        m.update(&counter.to_be_bytes());
54        m.update(label);
55        m.update(&[0u8]);
56        m.update(context);
57        m.update(&total_bits.to_be_bytes());
58        let t = m.finalize();
59        scratch[..M::TAG_LEN].copy_from_slice(t.as_ref());
60        chunk.copy_from_slice(&scratch[..chunk.len()]);
61    }
62    scratch.zeroize();
63    Ok(())
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use ic_mac::HmacSha256;
70
71    #[test]
72    fn is_deterministic() {
73        let mut a = [0u8; 48];
74        let mut b = [0u8; 48];
75        kbkdf_counter::<HmacSha256>(b"kdk", b"label", b"ctx", &mut a).unwrap();
76        kbkdf_counter::<HmacSha256>(b"kdk", b"label", b"ctx", &mut b).unwrap();
77        assert_eq!(a, b);
78    }
79
80    #[test]
81    fn label_and_context_are_domain_separated() {
82        let mut a = [0u8; 32];
83        let mut b = [0u8; 32];
84        let mut c = [0u8; 32];
85        kbkdf_counter::<HmacSha256>(b"kdk", b"ab", b"c", &mut a).unwrap();
86        kbkdf_counter::<HmacSha256>(b"kdk", b"a", b"bc", &mut b).unwrap();
87        kbkdf_counter::<HmacSha256>(b"kdk", b"ab", b"d", &mut c).unwrap();
88        // Without the 0x00 separator the first two would collide.
89        assert_ne!(a, b);
90        assert_ne!(a, c);
91    }
92
93    /// Output length is bound into every block, so a 32-byte request is not a
94    /// prefix of a 64-byte one. That is what blocks a truncation attack.
95    #[test]
96    fn output_length_is_bound_into_the_derivation() {
97        let mut short = [0u8; 32];
98        let mut long = [0u8; 64];
99        kbkdf_counter::<HmacSha256>(b"kdk", b"l", b"c", &mut short).unwrap();
100        kbkdf_counter::<HmacSha256>(b"kdk", b"l", b"c", &mut long).unwrap();
101        assert_ne!(&long[..32], &short[..]);
102    }
103
104    #[test]
105    fn blocks_are_distinct() {
106        let mut out = [0u8; 96];
107        kbkdf_counter::<HmacSha256>(b"kdk", b"l", b"c", &mut out).unwrap();
108        assert_ne!(&out[..32], &out[32..64]);
109        assert_ne!(&out[32..64], &out[64..]);
110    }
111
112    #[test]
113    fn rejects_ambiguous_label() {
114        let mut out = [0u8; 32];
115        assert!(kbkdf_counter::<HmacSha256>(b"kdk", b"a\0b", b"c", &mut out).is_err());
116        assert!(kbkdf_counter::<HmacSha256>(b"kdk", b"l", b"c", &mut []).is_err());
117    }
118}