Skip to main content

crypto/
hkdf.rs

1//! HKDF (HMAC-based Extract-and-Expand Key Derivation Function) key derivation function.
2
3use crate::{Hash, Hasher, HkdfError, MAX_HASH_OUTPUT_SIZE, hmac::Hmac};
4
5const DEFAULT_SALT: [u8; MAX_HASH_OUTPUT_SIZE] = [0u8; MAX_HASH_OUTPUT_SIZE];
6
7/// HKDF extract step: `PRK = HMAC-Hash(salt, IKM)`.
8///
9/// If `salt` is `None`, a string of `H::OUTPUT_SIZE` zero bytes is used.
10///
11/// # Example
12///
13/// ```ignore
14/// use crypto::hkdf;
15/// use crypto::sha2::Sha256;
16///
17/// let prk = hkdf::extract::<Sha256>(b"input key material", Some(b"salt"));
18/// ```
19pub fn extract<H: Hasher>(ikm: &[u8], salt: Option<&[u8]>) -> Hash {
20    const { assert!(H::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE) };
21
22    let salt = salt.unwrap_or(&DEFAULT_SALT[..H::OUTPUT_SIZE]);
23    let mut mac = Hmac::<H>::new(salt);
24    mac.update(ikm);
25    return mac.finalize();
26}
27
28/// HKDF expand step: `OKM = T(1) || T(2) || ...`, where
29/// `T(i) = HMAC-Hash(PRK, T(i-1) || info || i)`.
30///
31/// The output is written into `okm`. The length of the output is determined by
32/// `okm.len()`.
33///
34/// # Example
35///
36/// ```ignore
37/// use crypto::hkdf;
38/// use crypto::sha2::Sha256;
39///
40/// let prk = hkdf::extract::<Sha256>(b"input key material", Some(b"salt"));
41/// let mut okm = [0u8; 32];
42/// hkdf::expand::<Sha256>(&mut okm, &prk, b"context info").unwrap();
43/// ```
44///
45/// # Error
46///
47/// Returns an error if `okm.len() > 255 * H::OUTPUT_SIZE` or if `prk.len() < H::OUTPUT_SIZE`.
48pub fn expand<H: Hasher>(okm: &mut [u8], prk: &[u8], info: &[u8]) -> Result<(), HkdfError> {
49    const { assert!(H::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE) };
50
51    let n = okm.len();
52
53    if prk.len() < H::OUTPUT_SIZE {
54        return Err(HkdfError::PrkIsTooShort(H::OUTPUT_SIZE));
55    }
56
57    if n > 255 * H::OUTPUT_SIZE {
58        return Err(HkdfError::OutputIsTooLong);
59    }
60
61    if n == 0 {
62        return Ok(());
63    }
64
65    let mut t = [0u8; MAX_HASH_OUTPUT_SIZE];
66    let mut t_len = 0usize;
67    let mut offset = 0usize;
68    let mut counter = 1u8;
69
70    while offset < n {
71        let mut mac = Hmac::<H>::new(&prk[..H::OUTPUT_SIZE]);
72        mac.update(&t[..t_len]);
73        mac.update(info);
74        mac.update(&[counter]);
75        let block = mac.finalize();
76        let block_bytes = block.as_ref();
77        let chunk_len = (n - offset).min(H::OUTPUT_SIZE);
78        okm[offset..offset + chunk_len].copy_from_slice(&block_bytes[..chunk_len]);
79        t[..H::OUTPUT_SIZE].copy_from_slice(block_bytes);
80        t_len = H::OUTPUT_SIZE;
81        offset += chunk_len;
82        counter = counter.wrapping_add(1);
83    }
84
85    Ok(())
86}
87
88/// One-shot HKDF: extract-then-expand in a single call.
89///
90/// `H::OUTPUT_SIZE` must be smaller than or equal to
91/// [`MAX_HASH_OUTPUT_SIZE`](crate::MAX_HASH_OUTPUT_SIZE); this bound is checked
92/// at compile time.
93///
94/// # Example
95///
96/// ```ignore
97/// use crypto::hkdf;
98/// use crypto::sha2::Sha256;
99///
100/// let okm: [u8; 32] = hkdf::derive_key::<Sha256, 32>(
101///     b"input key material",
102///     b"context info",
103///     Some(b"salt"),
104/// ).unwrap();
105/// ```
106///
107/// # Error
108///
109/// Returns an error if `N > 255 * H::OUTPUT_SIZE`.
110pub fn derive_key<H: Hasher, const N: usize>(
111    ikm: &[u8],
112    info: &[u8],
113    salt: Option<&[u8]>,
114) -> Result<[u8; N], HkdfError> {
115    let prk = extract::<H>(ikm, salt);
116    let mut okm = [0u8; N];
117    expand::<H>(&mut okm, prk.as_ref(), info)?;
118    Ok(okm)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::sha2::{Sha256, Sha384, Sha512};
125
126    struct TestVector {
127        ikm: &'static str,
128        salt: Option<&'static str>,
129        info: &'static str,
130        expected_prk: &'static str,
131        expected_okm: &'static str,
132    }
133
134    fn decode_hex(input: &str) -> Vec<u8> {
135        let input = input.replace(|c: char| c.is_whitespace(), "");
136        (0..input.len())
137            .step_by(2)
138            .map(|i| u8::from_str_radix(&input[i..i + 2], 16).unwrap())
139            .collect()
140    }
141
142    const SHA256_VECTORS: [TestVector; 4] = [
143        TestVector {
144            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
145            salt: Some("000102030405060708090a0b0c"),
146            info: "f0f1f2f3f4f5f6f7f8f9",
147            expected_prk: "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5",
148            expected_okm: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
149        },
150        TestVector {
151            ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
152                  202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\
153                  404142434445464748494a4b4c4d4e4f",
154            salt: Some(
155                "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\
156                 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\
157                 a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
158            ),
159            info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
160                  d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\
161                  f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
162            expected_prk: "06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244",
163            expected_okm: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87",
164        },
165        TestVector {
166            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
167            salt: Some(""),
168            info: "",
169            expected_prk: "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04",
170            expected_okm: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
171        },
172        TestVector {
173            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
174            salt: None,
175            info: "",
176            expected_prk: "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04",
177            expected_okm: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
178        },
179    ];
180
181    const SHA512_VECTORS: [TestVector; 4] = [
182        TestVector {
183            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
184            salt: Some("000102030405060708090a0b0c"),
185            info: "f0f1f2f3f4f5f6f7f8f9",
186            expected_prk: "665799823737ded04a88e47e54a5890bb2c3d247c7a4254a8e61350723590a26c36238127d8661b88cf80ef802d57e2f7cebcf1e00e083848be19929c61b4237",
187            expected_okm: "832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb",
188        },
189        TestVector {
190            ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
191                  202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\
192                  404142434445464748494a4b4c4d4e4f",
193            salt: Some(
194                "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\
195                 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\
196                 a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
197            ),
198            info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
199                  d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\
200                  f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
201            expected_prk: "35672542907d4e142c00e84499e74e1de08be86535f924e022804ad775dde27ec86cd1e5b7d178c74489bdbeb30712beb82d4f97416c5a94ea81ebdf3e629e4a",
202            expected_okm: "ce6c97192805b346e6161e821ed165673b84f400a2b514b2fe23d84cd189ddf1b695b48cbd1c8388441137b3ce28f16aa64ba33ba466b24df6cfcb021ecff235f6a2056ce3af1de44d572097a8505d9e7a93",
203        },
204        TestVector {
205            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
206            salt: Some(""),
207            info: "",
208            expected_prk: "fd200c4987ac491313bd4a2a13287121247239e11c9ef82802044b66ef357e5b194498d0682611382348572a7b1611de54764094286320578a863f36562b0df6",
209            expected_okm: "f5fa02b18298a72a8c23898a8703472c6eb179dc204c03425c970e3b164bf90fff22d04836d0e2343bac",
210        },
211        TestVector {
212            ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
213            salt: None,
214            info: "",
215            expected_prk: "fd200c4987ac491313bd4a2a13287121247239e11c9ef82802044b66ef357e5b194498d0682611382348572a7b1611de54764094286320578a863f36562b0df6",
216            expected_okm: "f5fa02b18298a72a8c23898a8703472c6eb179dc204c03425c970e3b164bf90fff22d04836d0e2343bac",
217        },
218    ];
219
220    #[test]
221    fn hkdf_sha256_vectors() {
222        for (i, vector) in SHA256_VECTORS.iter().enumerate() {
223            let ikm = decode_hex(vector.ikm);
224            let salt = vector.salt.map(decode_hex);
225            let info = decode_hex(vector.info);
226            let expected_prk = decode_hex(vector.expected_prk);
227            let expected_okm = decode_hex(vector.expected_okm);
228
229            let prk = extract::<Sha256>(&ikm, salt.as_deref());
230            assert_eq!(prk.as_ref(), expected_prk.as_slice(), "vector {} PRK", i);
231
232            let mut buf = [0u8; 82];
233            match expected_okm.len() {
234                42 => expand::<Sha256>(&mut buf[..42], prk.as_ref(), &info).unwrap(),
235                82 => expand::<Sha256>(&mut buf[..82], prk.as_ref(), &info).unwrap(),
236                _ => unreachable!(),
237            };
238            assert_eq!(&buf[..expected_okm.len()], expected_okm, "vector {} OKM", i);
239
240            let derived = match expected_okm.len() {
241                42 => derive_key::<Sha256, 42>(&ikm, &info, salt.as_deref()).unwrap().to_vec(),
242                82 => derive_key::<Sha256, 82>(&ikm, &info, salt.as_deref()).unwrap().to_vec(),
243                _ => unreachable!(),
244            };
245            assert_eq!(derived, expected_okm, "vector {} derive_key OKM", i);
246        }
247    }
248
249    #[test]
250    fn hkdf_sha512_vectors() {
251        for (i, vector) in SHA512_VECTORS.iter().enumerate() {
252            let ikm = decode_hex(vector.ikm);
253            let salt = vector.salt.map(decode_hex);
254            let info = decode_hex(vector.info);
255            let expected_prk = decode_hex(vector.expected_prk);
256            let expected_okm = decode_hex(vector.expected_okm);
257
258            let prk = extract::<Sha512>(&ikm, salt.as_deref());
259            assert_eq!(prk.as_ref(), expected_prk.as_slice(), "vector {} PRK", i);
260
261            let mut buf = [0u8; 82];
262            match expected_okm.len() {
263                42 => expand::<Sha512>(&mut buf[..42], prk.as_ref(), &info).unwrap(),
264                82 => expand::<Sha512>(&mut buf[..82], prk.as_ref(), &info).unwrap(),
265                _ => unreachable!(),
266            };
267            assert_eq!(&buf[..expected_okm.len()], expected_okm, "vector {} OKM", i);
268
269            let derived = match expected_okm.len() {
270                42 => derive_key::<Sha512, 42>(&ikm, &info, salt.as_deref()).unwrap().to_vec(),
271                82 => derive_key::<Sha512, 82>(&ikm, &info, salt.as_deref()).unwrap().to_vec(),
272                _ => unreachable!(),
273            };
274            assert_eq!(derived, expected_okm, "vector {} derive_key OKM", i);
275        }
276    }
277
278    #[test]
279    fn hkdf_zero_length_output() {
280        let prk = [0u8; 32];
281        let mut buf = [0u8; 0];
282        assert_eq!(expand::<Sha256>(&mut buf, &prk, b""), Ok(()));
283        assert_eq!(derive_key::<Sha256, 0>(b"ikm", b"info", None).unwrap(), [] as [u8; 0]);
284    }
285
286    #[test]
287    fn hkdf_expand_panics_when_output_is_too_large() {
288        let prk = [0u8; 32];
289        let mut buf = vec![0u8; Sha256::BLOCK_SIZE * 300];
290        assert_eq!(expand::<Sha256>(&mut buf, &prk, b""), Err(HkdfError::OutputIsTooLong));
291    }
292
293    #[test]
294    fn hkdf_expand_panics_when_prk_is_too_short() {
295        let mut buf = [0u8; 32];
296        assert_eq!(
297            expand::<Sha256>(&mut buf, &[0u8; 31], b""),
298            Err(HkdfError::PrkIsTooShort(Sha256::OUTPUT_SIZE))
299        );
300    }
301
302    // --- Wycheproof test vectors ---
303
304    #[test]
305    fn hkdf_sha256_wycheproof() {
306        // Maximum valid HKDF-SHA-256 output: 255 * 32 = 8160 bytes.
307        const MAX_OKM: usize = 8160;
308        const SIZE_TOO_LARGE: usize = 8161;
309
310        let data: serde_json::Value =
311            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hkdf_sha256_test.json")).unwrap();
312        let mut valid_tested = 0u64;
313        let mut invalid_tested = 0u64;
314        for group in data["testGroups"].as_array().unwrap() {
315            for test in group["tests"].as_array().unwrap() {
316                let ikm_hex = test["ikm"].as_str().unwrap();
317                let salt_hex = test["salt"].as_str().unwrap();
318                let info_hex = test["info"].as_str().unwrap();
319                let size = test["size"].as_u64().unwrap() as usize;
320                let expected_okm_hex = test["okm"].as_str().unwrap();
321                let result = test["result"].as_str().unwrap();
322
323                let ikm = hex::decode(ikm_hex).unwrap();
324                let info = hex::decode(info_hex).unwrap();
325                let salt: Option<Vec<u8>> = if salt_hex.is_empty() {
326                    None
327                } else {
328                    Some(hex::decode(salt_hex).unwrap())
329                };
330
331                if result == "valid" {
332                    let okm = derive_key::<Sha256, MAX_OKM>(&ikm, &info, salt.as_deref()).unwrap();
333                    let okm_hex = hex::encode(&okm[..size]);
334                    assert_eq!(
335                        okm_hex, expected_okm_hex,
336                        "wycheproof HKDF-SHA-256 tcId={} size={}",
337                        test["tcId"], size
338                    );
339                    valid_tested += 1;
340                } else {
341                    assert_eq!(
342                        derive_key::<Sha256, SIZE_TOO_LARGE>(&ikm, &info, salt.as_deref()),
343                        Err(HkdfError::OutputIsTooLong),
344                        "wycheproof HKDF-SHA-256 tcId={} size={} should reject",
345                        test["tcId"],
346                        size
347                    );
348                    invalid_tested += 1;
349                }
350            }
351        }
352        assert!(valid_tested > 0, "no valid HKDF-SHA-256 wycheproof tests were run");
353        assert!(invalid_tested > 0, "no invalid HKDF-SHA-256 wycheproof tests were run");
354    }
355
356    #[test]
357    fn hkdf_sha512_wycheproof() {
358        // Maximum valid HKDF-SHA-512 output: 255 * 64 = 16320 bytes.
359        const MAX_OKM: usize = 16320;
360        const SIZE_TOO_LARGE: usize = 16321;
361
362        let data: serde_json::Value =
363            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hkdf_sha512_test.json")).unwrap();
364        let mut valid_tested = 0u64;
365        let mut invalid_tested = 0u64;
366        for group in data["testGroups"].as_array().unwrap() {
367            for test in group["tests"].as_array().unwrap() {
368                let ikm_hex = test["ikm"].as_str().unwrap();
369                let salt_hex = test["salt"].as_str().unwrap();
370                let info_hex = test["info"].as_str().unwrap();
371                let size = test["size"].as_u64().unwrap() as usize;
372                let expected_okm_hex = test["okm"].as_str().unwrap();
373                let result = test["result"].as_str().unwrap();
374
375                let ikm = hex::decode(ikm_hex).unwrap();
376                let info = hex::decode(info_hex).unwrap();
377                let salt: Option<Vec<u8>> = if salt_hex.is_empty() {
378                    None
379                } else {
380                    Some(hex::decode(salt_hex).unwrap())
381                };
382
383                if result == "valid" {
384                    let okm = derive_key::<Sha512, MAX_OKM>(&ikm, &info, salt.as_deref()).unwrap();
385                    let okm_hex = hex::encode(&okm[..size]);
386                    assert_eq!(
387                        okm_hex, expected_okm_hex,
388                        "wycheproof HKDF-SHA-512 tcId={} size={}",
389                        test["tcId"], size
390                    );
391                    valid_tested += 1;
392                } else {
393                    assert_eq!(
394                        derive_key::<Sha512, SIZE_TOO_LARGE>(&ikm, &info, salt.as_deref()),
395                        Err(HkdfError::OutputIsTooLong),
396                        "wycheproof HKDF-SHA-512 tcId={} size={} should reject",
397                        test["tcId"],
398                        size
399                    );
400                    invalid_tested += 1;
401                }
402            }
403        }
404        assert!(valid_tested > 0, "no valid HKDF-SHA-512 wycheproof tests were run");
405        assert!(invalid_tested > 0, "no invalid HKDF-SHA-512 wycheproof tests were run");
406    }
407
408    #[test]
409    fn hkdf_sha384_wycheproof() {
410        // Maximum valid HKDF-SHA-384 output: 255 * 48 = 12240 bytes.
411        const MAX_OKM: usize = 12240;
412        const SIZE_TOO_LARGE: usize = 12241;
413
414        let data: serde_json::Value =
415            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hkdf_sha384_test.json")).unwrap();
416        let mut valid_tested = 0u64;
417        let mut invalid_tested = 0u64;
418        for group in data["testGroups"].as_array().unwrap() {
419            for test in group["tests"].as_array().unwrap() {
420                let ikm_hex = test["ikm"].as_str().unwrap();
421                let salt_hex = test["salt"].as_str().unwrap();
422                let info_hex = test["info"].as_str().unwrap();
423                let size = test["size"].as_u64().unwrap() as usize;
424                let expected_okm_hex = test["okm"].as_str().unwrap();
425                let result = test["result"].as_str().unwrap();
426
427                let ikm = hex::decode(ikm_hex).unwrap();
428                let info = hex::decode(info_hex).unwrap();
429                let salt: Option<Vec<u8>> = if salt_hex.is_empty() {
430                    None
431                } else {
432                    Some(hex::decode(salt_hex).unwrap())
433                };
434
435                if result == "valid" {
436                    let okm = derive_key::<Sha384, MAX_OKM>(&ikm, &info, salt.as_deref()).unwrap();
437                    let okm_hex = hex::encode(&okm[..size]);
438                    assert_eq!(
439                        okm_hex, expected_okm_hex,
440                        "wycheproof HKDF-SHA-384 tcId={} size={}",
441                        test["tcId"], size
442                    );
443                    valid_tested += 1;
444                } else {
445                    assert_eq!(
446                        derive_key::<Sha384, SIZE_TOO_LARGE>(&ikm, &info, salt.as_deref()),
447                        Err(HkdfError::OutputIsTooLong),
448                        "wycheproof HKDF-SHA-384 tcId={} size={} should reject",
449                        test["tcId"],
450                        size
451                    );
452                    invalid_tested += 1;
453                }
454            }
455        }
456        assert!(valid_tested > 0, "no valid HKDF-SHA-384 wycheproof tests were run");
457        assert!(invalid_tested > 0, "no invalid HKDF-SHA-384 wycheproof tests were run");
458    }
459}