Skip to main content

ic_kdf/
hkdf.rs

1//! RFC 5869 HKDF: extract-then-expand.
2
3use core::marker::PhantomData;
4use ic_core::traits::{Algorithm, Kdf, Mac, SelfTest};
5use ic_core::{ensure, Result, Zeroize};
6
7/// The largest PRK any supported HMAC produces (SHA-512).
8const MAX_PRK_LEN: usize = 64;
9
10/// HKDF instantiated with the MAC `M`.
11pub struct Hkdf<M: Mac>(PhantomData<M>);
12
13impl<M: Mac> Hkdf<M> {
14    /// HKDF-Extract: compress arbitrary input keying material into a PRK.
15    ///
16    /// An empty salt is replaced by `HashLen` zero bytes, as RFC 5869 requires.
17    pub fn extract(salt: &[u8], ikm: &[u8], prk: &mut [u8]) -> Result<()> {
18        ensure!(prk.len() == M::TAG_LEN, InvalidLength, "hkdf prk buffer");
19        let zeros = [0u8; MAX_PRK_LEN];
20        let salt = if salt.is_empty() {
21            &zeros[..M::TAG_LEN]
22        } else {
23            salt
24        };
25        let tag = M::mac(salt, ikm)?;
26        prk.copy_from_slice(tag.as_ref());
27        Ok(())
28    }
29
30    /// HKDF-Expand: stretch a PRK to `out.len()` bytes bound to `info`.
31    ///
32    /// Keys a MAC with `prk` for every block of output. Deriving several
33    /// outputs from one PRK -- TLS 1.3 takes a key, an IV and a finished key
34    /// from each traffic secret -- is cheaper through [`Self::expand_from`],
35    /// which keys once.
36    pub fn expand(prk: &[u8], info: &[u8], out: &mut [u8]) -> Result<()> {
37        ensure!(prk.len() >= M::TAG_LEN, InvalidLength, "hkdf prk too short");
38        Self::expand_with(|| M::new(prk), info, out)
39    }
40
41    /// HKDF-Expand from a MAC already keyed with the PRK.
42    ///
43    /// `keyed` is `M::new(prk)`, made once and kept. Each block of output
44    /// starts from a clone of it rather than from the key, so the key setup
45    /// -- for HMAC, two compressions of the padded key -- is paid when `keyed`
46    /// is made and not again per call or per block. The output is exactly
47    /// [`Self::expand`]'s for the same PRK.
48    ///
49    /// RFC 5869 requires the PRK to be at least `HashLen` bytes. `expand`
50    /// checks that; here the key is already inside `keyed`, so making it from
51    /// a PRK of the right length is the caller's part.
52    pub fn expand_from(keyed: &M, info: &[u8], out: &mut [u8]) -> Result<()>
53    where
54        M: Clone,
55    {
56        Self::expand_with(|| Ok(keyed.clone()), info, out)
57    }
58
59    /// The expansion loop both of those share; `keyed` supplies a MAC keyed
60    /// with the PRK for each block.
61    fn expand_with(
62        mut keyed: impl FnMut() -> Result<M>,
63        info: &[u8],
64        out: &mut [u8],
65    ) -> Result<()> {
66        let n = M::TAG_LEN;
67        // RFC 5869 caps output at 255 * HashLen because the counter is a byte.
68        ensure!(
69            out.len() <= 255 * n,
70            InvalidLength,
71            "hkdf output exceeds 255*HashLen"
72        );
73
74        let mut previous = [0u8; MAX_PRK_LEN];
75        let mut previous_len = 0usize;
76        let mut counter: u8 = 1;
77
78        for chunk in out.chunks_mut(n) {
79            let mut m = keyed()?;
80            m.update(&previous[..previous_len]);
81            m.update(info);
82            m.update(&[counter]);
83            let t = m.finalize();
84            chunk.copy_from_slice(&t.as_ref()[..chunk.len()]);
85            previous[..n].copy_from_slice(t.as_ref());
86            previous_len = n;
87            counter = counter.wrapping_add(1);
88        }
89        previous.zeroize();
90        Ok(())
91    }
92}
93
94impl<M: Mac> Algorithm for Hkdf<M> {
95    const ID: &'static str = M::ID;
96    const NAME: &'static str = "HKDF";
97}
98
99impl<M: Mac> Kdf for Hkdf<M> {
100    /// One-shot extract-then-expand.
101    fn derive(secret: &[u8], salt: &[u8], info: &[u8], out: &mut [u8]) -> Result<()> {
102        let mut prk = [0u8; MAX_PRK_LEN];
103        Self::extract(salt, secret, &mut prk[..M::TAG_LEN])?;
104        let r = Self::expand(&prk[..M::TAG_LEN], info, out);
105        prk.zeroize();
106        r
107    }
108}
109
110impl SelfTest for Hkdf<ic_mac::HmacSha256> {
111    fn self_test() -> Result<()> {
112        // RFC 5869 test case 1.
113        let ikm = [0x0bu8; 22];
114        let mut salt = [0u8; 13];
115        ic_core::codec::hex_decode(b"000102030405060708090a0b0c", &mut salt)?;
116        let mut info = [0u8; 10];
117        ic_core::codec::hex_decode(b"f0f1f2f3f4f5f6f7f8f9", &mut info)?;
118
119        let mut okm = [0u8; 42];
120        <Self as Kdf>::derive(&ikm, &salt, &info, &mut okm)?;
121
122        let mut want = [0u8; 42];
123        ic_core::codec::hex_decode(
124            b"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
125            &mut want,
126        )?;
127        ensure!(
128            ic_core::ct::verify(&want, &okm),
129            SelfTestFailed,
130            "hkdf-sha2-256"
131        );
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use ic_core::codec::{hex, unhex};
140    use ic_mac::{HmacSha256, HmacSha512};
141
142    /// RFC 5869 test case 1 (SHA-256, with salt and info).
143    #[test]
144    fn rfc5869_case_1() {
145        let ikm = [0x0bu8; 22];
146        let salt = unhex("000102030405060708090a0b0c").unwrap();
147        let info = unhex("f0f1f2f3f4f5f6f7f8f9").unwrap();
148
149        let mut prk = [0u8; 32];
150        Hkdf::<HmacSha256>::extract(&salt, &ikm, &mut prk).unwrap();
151        assert_eq!(
152            hex(&prk),
153            "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"
154        );
155
156        let mut okm = [0u8; 42];
157        Hkdf::<HmacSha256>::expand(&prk, &info, &mut okm).unwrap();
158        assert_eq!(
159            hex(&okm),
160            "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
161        );
162    }
163
164    /// RFC 5869 test case 3: empty salt and empty info.
165    #[test]
166    fn rfc5869_case_3_empty_salt_and_info() {
167        let ikm = [0x0bu8; 22];
168        let mut prk = [0u8; 32];
169        Hkdf::<HmacSha256>::extract(b"", &ikm, &mut prk).unwrap();
170        assert_eq!(
171            hex(&prk),
172            "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04"
173        );
174
175        let mut okm = [0u8; 42];
176        Hkdf::<HmacSha256>::expand(&prk, b"", &mut okm).unwrap();
177        assert_eq!(
178            hex(&okm),
179            "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"
180        );
181    }
182
183    /// RFC 5869 test case 2: inputs longer than one hash block.
184    #[test]
185    fn rfc5869_case_2_long_inputs() {
186        let ikm: Vec<u8> = (0..80u8).collect();
187        let salt: Vec<u8> = (0x60..0xb0u8).collect();
188        let info: Vec<u8> = (0xb0..=0xffu8).collect();
189
190        let mut okm = [0u8; 82];
191        Hkdf::<HmacSha256>::derive(&ikm, &salt, &info, &mut okm).unwrap();
192        assert_eq!(
193            hex(&okm),
194            "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"
195        );
196    }
197
198    #[test]
199    fn derive_matches_extract_then_expand() {
200        let mut a = [0u8; 40];
201        Hkdf::<HmacSha512>::derive(b"ikm", b"salt", b"info", &mut a).unwrap();
202
203        let mut prk = [0u8; 64];
204        Hkdf::<HmacSha512>::extract(b"salt", b"ikm", &mut prk).unwrap();
205        let mut b = [0u8; 40];
206        Hkdf::<HmacSha512>::expand(&prk, b"info", &mut b).unwrap();
207        assert_eq!(a, b);
208    }
209
210    #[test]
211    fn info_separates_derived_keys() {
212        let mut a = [0u8; 32];
213        let mut b = [0u8; 32];
214        Hkdf::<HmacSha256>::derive(b"ikm", b"salt", b"context-a", &mut a).unwrap();
215        Hkdf::<HmacSha256>::derive(b"ikm", b"salt", b"context-b", &mut b).unwrap();
216        assert_ne!(a, b, "distinct info must yield independent keys");
217    }
218
219    /// `expand_from` against `expand`, over output lengths that end on a
220    /// block boundary, one byte either side of it, and many blocks in: the
221    /// clone per block is exactly what those exercise and a single-block
222    /// vector would not.
223    #[test]
224    fn expand_from_a_keyed_mac_matches_expand() {
225        let mut checked = 0;
226        for prk in [[0x0bu8; 32], [0xa5; 32]] {
227            let keyed = HmacSha256::new(&prk).unwrap();
228            for len in [1usize, 31, 32, 33, 64, 65, 200, 255 * 32] {
229                let mut a = std::vec![0u8; len];
230                let mut b = std::vec![0u8; len];
231                Hkdf::<HmacSha256>::expand(&prk, b"tls13 key", &mut a).unwrap();
232                Hkdf::<HmacSha256>::expand_from(&keyed, b"tls13 key", &mut b).unwrap();
233                assert_eq!(a, b, "length {len}");
234                checked += 1;
235            }
236        }
237        let prk = [0x5cu8; 64];
238        let keyed = HmacSha512::new(&prk).unwrap();
239        let (mut a, mut b) = ([0u8; 200], [0u8; 200]);
240        Hkdf::<HmacSha512>::expand(&prk, b"info", &mut a).unwrap();
241        Hkdf::<HmacSha512>::expand_from(&keyed, b"info", &mut b).unwrap();
242        assert_eq!(a, b);
243        assert!(checked >= 16);
244    }
245
246    /// The RFC 5869 case 1 output, reached through a keyed MAC.
247    #[test]
248    fn expand_from_reproduces_rfc5869_case_1() {
249        let ikm = [0x0bu8; 22];
250        let salt: std::vec::Vec<u8> = (0x00..=0x0c).collect();
251        let info: std::vec::Vec<u8> = (0xf0..=0xf9).collect();
252        let mut prk = [0u8; 32];
253        Hkdf::<HmacSha256>::extract(&salt, &ikm, &mut prk).unwrap();
254        let keyed = HmacSha256::new(&prk).unwrap();
255        let mut okm = [0u8; 42];
256        Hkdf::<HmacSha256>::expand_from(&keyed, &info, &mut okm).unwrap();
257        let mut expected = [0u8; 42];
258        Hkdf::<HmacSha256>::expand(&prk, &info, &mut expected).unwrap();
259        assert_eq!(okm, expected);
260        // The RFC's own bytes, which the `expand` test above also checks.
261        assert_eq!(&okm[..4], &[0x3c, 0xb2, 0x5f, 0x25]);
262    }
263
264    #[test]
265    fn rejects_output_beyond_255_blocks() {
266        let mut too_long = vec![0u8; 255 * 32 + 1];
267        assert!(Hkdf::<HmacSha256>::expand(&[0u8; 32], b"", &mut too_long).is_err());
268        let mut at_limit = vec![0u8; 255 * 32];
269        assert!(Hkdf::<HmacSha256>::expand(&[0u8; 32], b"", &mut at_limit).is_ok());
270    }
271
272    #[test]
273    fn self_test_passes() {
274        Hkdf::<HmacSha256>::self_test().unwrap();
275    }
276}