aws_lc_rs/hkdf.rs
1// Copyright 2015 Brian Smith.
2// SPDX-License-Identifier: ISC
3// Modifications copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4// SPDX-License-Identifier: Apache-2.0 OR ISC
5
6//! HMAC-based Extract-and-Expand Key Derivation Function.
7//!
8//! HKDF is specified in [RFC 5869].
9//!
10//! [RFC 5869]: https://tools.ietf.org/html/rfc5869
11//!
12//! # Encoding `info`
13//!
14//! [`Prk::expand`] concatenates the `info` slices with no separator or length
15//! prefix. Callers must encode structured, variable-length context
16//! unambiguously to avoid deriving the same key from different inputs; see
17//! [`Prk::expand`].
18//!
19//! # Example
20//! ```
21//! use aws_lc_rs::{aead, hkdf, hmac, rand};
22//!
23//! // Generate a (non-secret) salt value
24//! let mut salt_bytes = [0u8; 32];
25//! rand::fill(&mut salt_bytes).unwrap();
26//!
27//! // Extract pseudo-random key from secret keying materials
28//! let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, &salt_bytes);
29//! let pseudo_random_key = salt.extract(b"secret input keying material");
30//!
31//! // Derive HMAC key
32//! let hmac_key_material = pseudo_random_key
33//! .expand(
34//! &[b"hmac contextual info"],
35//! hkdf::HKDF_SHA256.hmac_algorithm(),
36//! )
37//! .unwrap();
38//! let hmac_key = hmac::Key::from(hmac_key_material);
39//!
40//! // Derive UnboundKey for AES-128-GCM
41//! let aes_keying_material = pseudo_random_key
42//! .expand(&[b"aes contextual info"], &aead::AES_128_GCM)
43//! .unwrap();
44//! let aead_unbound_key = aead::UnboundKey::from(aes_keying_material);
45//! ```
46
47use crate::aws_lc::{HKDF_expand, HKDF};
48use crate::error::Unspecified;
49use crate::fips::indicator_check;
50use crate::{digest, hmac};
51use alloc::sync::Arc;
52use core::fmt;
53use zeroize::Zeroize;
54
55/// An HKDF algorithm.
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub struct Algorithm(hmac::Algorithm);
58
59impl Algorithm {
60 /// The underlying HMAC algorithm.
61 #[inline]
62 #[must_use]
63 pub fn hmac_algorithm(&self) -> hmac::Algorithm {
64 self.0
65 }
66}
67
68/// HKDF using HMAC-SHA-1. Obsolete.
69pub const HKDF_SHA1_FOR_LEGACY_USE_ONLY: Algorithm = Algorithm(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY);
70
71/// HKDF using HMAC-SHA-256.
72pub const HKDF_SHA256: Algorithm = Algorithm(hmac::HMAC_SHA256);
73
74/// HKDF using HMAC-SHA-384.
75pub const HKDF_SHA384: Algorithm = Algorithm(hmac::HMAC_SHA384);
76
77/// HKDF using HMAC-SHA-512.
78pub const HKDF_SHA512: Algorithm = Algorithm(hmac::HMAC_SHA512);
79
80/// General Info length's for HKDF don't normally exceed 256 bits.
81/// We set the default capacity to a value larger than should be needed
82/// so that the value passed to |`HKDF_expand`| is only allocated once.
83const HKDF_INFO_DEFAULT_CAPACITY_LEN: usize = 80;
84
85/// The maximum output size of a PRK computed by |`HKDF_extract`| is the maximum digest
86/// size that can be outputted by *AWS-LC*.
87const MAX_HKDF_PRK_LEN: usize = digest::MAX_OUTPUT_LEN;
88
89impl KeyType for Algorithm {
90 fn len(&self) -> usize {
91 self.0.digest_algorithm().output_len
92 }
93}
94
95/// A salt for HKDF operations.
96pub struct Salt {
97 algorithm: Algorithm,
98 bytes: Arc<[u8]>,
99}
100
101#[allow(clippy::missing_fields_in_debug)]
102impl fmt::Debug for Salt {
103 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104 f.debug_struct("hkdf::Salt")
105 .field("algorithm", &self.algorithm.0)
106 .finish()
107 }
108}
109
110impl Salt {
111 /// Constructs a new `Salt` with the given value based on the given digest
112 /// algorithm.
113 ///
114 /// Constructing a `Salt` is relatively expensive so it is good to reuse a
115 /// `Salt` object instead of re-constructing `Salt`s with the same value.
116 ///
117 // # FIPS
118 // The following conditions must be met:
119 // * Algorithm is one of the following:
120 // * `HKDF_SHA1_FOR_LEGACY_USE_ONLY`
121 // * `HKDF_SHA256`
122 // * `HKDF_SHA384`
123 // * `HKDF_SHA512`
124 // * `value.len() > 0` is true
125 //
126 /// # Panics
127 /// `new` panics if salt creation fails
128 #[must_use]
129 pub fn new(algorithm: Algorithm, value: &[u8]) -> Self {
130 Self {
131 algorithm,
132 bytes: Arc::from(value),
133 }
134 }
135
136 /// Constructs a `Salt` with no salt value.
137 ///
138 /// This is equivalent to a `salt` argument of "a string of HashLen
139 /// zeros" as described in [RFC 5869 Section 2.2], and avoids the
140 /// awkward [`Salt::new(alg, b"")`](Self::new) idiom.
141 ///
142 /// # Use a salt when you can
143 ///
144 /// HKDF's extraction step is strengthened by a non-secret, ideally
145 /// random salt; [RFC 5869 Section 3.1] recommends supplying one
146 /// whenever the application can, and [NIST SP 800-56C Rev. 2 §5.1]
147 /// likewise recommends a non-empty salt for the key-derivation key.
148 /// In particular, when the input keying material is not uniformly
149 /// random or when an attacker has any control over it, a salt is
150 /// what gives the extract step a reduction to HMAC's PRF security.
151 ///
152 /// Use this constructor only when no salt material is available to
153 /// the application. Otherwise prefer [`Salt::new`] with a salt that
154 /// is at least as long as the underlying digest's output and is
155 /// either random or chosen with care to avoid collisions across
156 /// uses of the same key.
157 ///
158 /// [RFC 5869 Section 2.2]: https://tools.ietf.org/html/rfc5869#section-2.2
159 /// [RFC 5869 Section 3.1]: https://tools.ietf.org/html/rfc5869#section-3.1
160 /// [NIST SP 800-56C Rev. 2 §5.1]: https://doi.org/10.6028/NIST.SP.800-56Cr2
161 //
162 // # FIPS
163 // Not allowed in FIPS mode: HKDF in FIPS mode requires a non-empty salt.
164 #[must_use]
165 pub fn none(algorithm: Algorithm) -> Self {
166 Self::new(algorithm, &[])
167 }
168
169 /// The [HKDF-Extract] operation.
170 ///
171 /// [HKDF-Extract]: https://tools.ietf.org/html/rfc5869#section-2.2
172 ///
173 /// # Panics
174 /// Panics if the extract operation is unable to be performed
175 #[inline]
176 #[must_use]
177 pub fn extract(&self, secret: &[u8]) -> Prk {
178 Prk {
179 algorithm: self.algorithm,
180 mode: PrkMode::ExtractExpand {
181 secret: Arc::new(ZeroizeBoxSlice::from(secret)),
182 salt: Arc::clone(&self.bytes),
183 },
184 }
185 }
186
187 /// The algorithm used to derive this salt.
188 #[inline]
189 #[must_use]
190 pub fn algorithm(&self) -> Algorithm {
191 Algorithm(self.algorithm.hmac_algorithm())
192 }
193}
194
195impl From<Okm<'_, Algorithm>> for Salt {
196 fn from(okm: Okm<'_, Algorithm>) -> Self {
197 let algorithm = okm.prk.algorithm;
198 let salt_len = okm.len().len();
199 let mut salt_bytes = vec![0u8; salt_len];
200 okm.fill(&mut salt_bytes).unwrap();
201 Self {
202 algorithm,
203 bytes: Arc::from(salt_bytes.as_slice()),
204 }
205 }
206}
207
208/// The length of the OKM (Output Keying Material) for a `Prk::expand()` call.
209#[allow(clippy::len_without_is_empty)]
210pub trait KeyType {
211 /// The length that `Prk::expand()` should expand its input to.
212 fn len(&self) -> usize;
213}
214
215#[derive(Clone)]
216enum PrkMode {
217 Expand {
218 key_bytes: [u8; MAX_HKDF_PRK_LEN],
219 key_len: usize,
220 },
221 ExtractExpand {
222 secret: Arc<ZeroizeBoxSlice<u8>>,
223 salt: Arc<[u8]>,
224 },
225}
226
227impl PrkMode {
228 fn fill(&self, algorithm: Algorithm, out: &mut [u8], info: &[u8]) -> Result<(), Unspecified> {
229 let digest = digest::match_digest_type(&algorithm.0.digest_algorithm().id).as_const_ptr();
230
231 match &self {
232 PrkMode::Expand { key_bytes, key_len } => unsafe {
233 if 1 != indicator_check!(HKDF_expand(
234 out.as_mut_ptr(),
235 out.len(),
236 digest,
237 key_bytes.as_ptr(),
238 *key_len,
239 info.as_ptr(),
240 info.len(),
241 )) {
242 return Err(Unspecified);
243 }
244 },
245 PrkMode::ExtractExpand { secret, salt } => {
246 if 1 != indicator_check!(unsafe {
247 HKDF(
248 out.as_mut_ptr(),
249 out.len(),
250 digest,
251 secret.as_ptr(),
252 secret.len(),
253 salt.as_ptr(),
254 salt.len(),
255 info.as_ptr(),
256 info.len(),
257 )
258 }) {
259 return Err(Unspecified);
260 }
261 }
262 }
263
264 Ok(())
265 }
266}
267
268impl fmt::Debug for PrkMode {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 match self {
271 Self::Expand { .. } => f.debug_struct("Expand").finish_non_exhaustive(),
272 Self::ExtractExpand { .. } => f.debug_struct("ExtractExpand").finish_non_exhaustive(),
273 }
274 }
275}
276
277struct ZeroizeBoxSlice<T: Zeroize>(Box<[T]>);
278
279impl<T: Zeroize> core::ops::Deref for ZeroizeBoxSlice<T> {
280 type Target = [T];
281
282 fn deref(&self) -> &Self::Target {
283 &self.0
284 }
285}
286
287impl<T: Clone + Zeroize> From<&[T]> for ZeroizeBoxSlice<T> {
288 fn from(value: &[T]) -> Self {
289 Self(Vec::from(value).into_boxed_slice())
290 }
291}
292
293impl<T: Zeroize> Drop for ZeroizeBoxSlice<T> {
294 fn drop(&mut self) {
295 self.0.zeroize();
296 }
297}
298
299/// A HKDF PRK (pseudorandom key).
300#[derive(Clone)]
301pub struct Prk {
302 algorithm: Algorithm,
303 mode: PrkMode,
304}
305
306impl Drop for Prk {
307 fn drop(&mut self) {
308 if let PrkMode::Expand {
309 ref mut key_bytes, ..
310 } = self.mode
311 {
312 key_bytes.zeroize();
313 }
314 }
315}
316
317#[allow(clippy::missing_fields_in_debug)]
318impl fmt::Debug for Prk {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 f.debug_struct("hkdf::Prk")
321 .field("algorithm", &self.algorithm.0)
322 .field("mode", &self.mode)
323 .finish()
324 }
325}
326
327impl Prk {
328 /// Construct a new `Prk` directly with the given value.
329 ///
330 /// Usually one can avoid using this. It is useful when the application
331 /// intentionally wants to leak the PRK secret, e.g. to implement
332 /// `SSLKEYLOGFILE` functionality.
333 ///
334 // # FIPS
335 // The following conditions must be met:
336 // * Algorithm is one of the following:
337 // * `HKDF_SHA1_FOR_LEGACY_USE_ONLY`
338 // * `HKDF_SHA256`
339 // * `HKDF_SHA384`
340 // * `HKDF_SHA512`
341 // * The `info_len` from [`Prk::expand`] is non-zero.
342 //
343 /// # Panics
344 /// Panics if the given Prk length exceeds the limit
345 #[must_use]
346 pub fn new_less_safe(algorithm: Algorithm, value: &[u8]) -> Self {
347 Prk::try_new_less_safe(algorithm, value).expect("Prk length limit exceeded.")
348 }
349
350 fn try_new_less_safe(algorithm: Algorithm, value: &[u8]) -> Result<Prk, Unspecified> {
351 let key_len = value.len();
352 if key_len > MAX_HKDF_PRK_LEN {
353 return Err(Unspecified);
354 }
355 let mut key_bytes = [0u8; MAX_HKDF_PRK_LEN];
356 key_bytes[0..key_len].copy_from_slice(value);
357 Ok(Self {
358 algorithm,
359 mode: PrkMode::Expand { key_bytes, key_len },
360 })
361 }
362
363 /// The [HKDF-Expand] operation.
364 ///
365 /// [HKDF-Expand]: https://tools.ietf.org/html/rfc5869#section-2.3
366 ///
367 /// The `info` slices are concatenated with no separator or length prefix to
368 /// form the single `info` octet string of [RFC 5869]; passing multiple
369 /// slices is equivalent to passing their concatenation.
370 ///
371 /// Callers are therefore responsible for unambiguously encoding structured
372 /// context. Concatenating variable-length fields directly lets distinct
373 /// inputs collide into the same `info` and derive the same key material
374 /// (e.g. `&[b"AB", b"CD"]` and `&[b"ABC", b"D"]`); use a fixed-width or
375 /// length-prefixed encoding instead. This matches the requirement for AEAD
376 /// associated data ([`crate::aead::Aad`]).
377 ///
378 /// [RFC 5869]: https://tools.ietf.org/html/rfc5869
379 ///
380 /// # Errors
381 /// Returns `error::Unspecified` if:
382 /// * `len` is more than 255 times the digest algorithm's output length.
383 // # FIPS
384 // The following conditions must be met:
385 // * `Prk` must be constructed using `Salt::extract` prior to calling
386 // this method.
387 // * After concatination of the `info` slices the resulting `[u8].len() > 0` is true.
388 #[inline]
389 pub fn expand<'a, L: KeyType>(
390 &'a self,
391 info: &'a [&'a [u8]],
392 len: L,
393 ) -> Result<Okm<'a, L>, Unspecified> {
394 let len_cached = len.len();
395 if len_cached > 255 * self.algorithm.0.digest_algorithm().output_len {
396 return Err(Unspecified);
397 }
398 Ok(Okm {
399 prk: self,
400 info,
401 len,
402 })
403 }
404}
405
406impl From<Okm<'_, Algorithm>> for Prk {
407 fn from(okm: Okm<Algorithm>) -> Self {
408 let algorithm = okm.len;
409 let key_len = okm.len.len();
410 let mut key_bytes = [0u8; MAX_HKDF_PRK_LEN];
411 okm.fill(&mut key_bytes[0..key_len]).unwrap();
412
413 Self {
414 algorithm,
415 mode: PrkMode::Expand { key_bytes, key_len },
416 }
417 }
418}
419
420/// An HKDF OKM (Output Keying Material)
421///
422/// Intentionally not `Clone` or `Copy` as an OKM is generally only safe to
423/// use once.
424pub struct Okm<'a, L: KeyType> {
425 prk: &'a Prk,
426 info: &'a [&'a [u8]],
427 len: L,
428}
429
430impl<L: KeyType> fmt::Debug for Okm<'_, L> {
431 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
432 f.debug_struct("hkdf::Okm").field("prk", &self.prk).finish()
433 }
434}
435
436/// Concatenates info slices into a contiguous buffer for HKDF operations.
437/// Uses stack allocation for typical cases, heap allocation for large info.
438/// Info is public context data per RFC 5869, so no zeroization is needed.
439#[inline]
440fn concatenate_info<F, R>(info: &[&[u8]], f: F) -> R
441where
442 F: FnOnce(&[u8]) -> R,
443{
444 let info_len: usize = info.iter().map(|s| s.len()).sum();
445
446 // Info is public; no need to zeroize.
447 if info_len <= HKDF_INFO_DEFAULT_CAPACITY_LEN {
448 // Use stack buffer for typical case (avoids heap allocation)
449 let mut stack_buf = [0u8; HKDF_INFO_DEFAULT_CAPACITY_LEN];
450 let mut pos = 0;
451 for &slice in info {
452 stack_buf[pos..pos + slice.len()].copy_from_slice(slice);
453 pos += slice.len();
454 }
455
456 f(&stack_buf[..info_len])
457 } else {
458 // Heap allocation for rare large info case
459 let mut heap_buf = Vec::with_capacity(info_len);
460 for &slice in info {
461 heap_buf.extend_from_slice(slice);
462 }
463
464 f(&heap_buf)
465 }
466}
467
468impl<L: KeyType> Okm<'_, L> {
469 /// The `OkmLength` given to `Prk::expand()`.
470 #[inline]
471 pub fn len(&self) -> &L {
472 &self.len
473 }
474
475 /// Fills `out` with the output of the HKDF-Expand operation for the given
476 /// inputs.
477 ///
478 // # FIPS
479 // The following conditions must be met:
480 // * Algorithm is one of the following:
481 // * `HKDF_SHA1_FOR_LEGACY_USE_ONLY`
482 // * `HKDF_SHA256`
483 // * `HKDF_SHA384`
484 // * `HKDF_SHA512`
485 // * The [`Okm`] was constructed from a [`Prk`] created with [`Salt::extract`] and:
486 // * The `value.len()` passed to [`Salt::new`] was non-zero.
487 // * The `info_len` from [`Prk::expand`] was non-zero.
488 //
489 /// # Errors
490 /// `error::Unspecified` if the requested output length differs from the length specified by
491 /// `L: KeyType`.
492 #[inline]
493 pub fn fill(self, out: &mut [u8]) -> Result<(), Unspecified> {
494 if out.len() != self.len.len() {
495 return Err(Unspecified);
496 }
497
498 concatenate_info(self.info, |info_bytes| {
499 self.prk.mode.fill(self.prk.algorithm, out, info_bytes)
500 })
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use crate::hkdf::{Salt, HKDF_SHA256, HKDF_SHA384};
507
508 #[cfg(feature = "fips")]
509 mod fips;
510
511 #[test]
512 fn hkdf_coverage() {
513 // Something would have gone horribly wrong for this to not pass, but we test this so our
514 // coverage reports will look better.
515 assert_ne!(HKDF_SHA256, HKDF_SHA384);
516 assert_eq!("Algorithm(Algorithm(SHA256))", format!("{HKDF_SHA256:?}"));
517 }
518
519 #[test]
520 fn test_debug() {
521 const SALT: &[u8; 32] = &[
522 29, 113, 120, 243, 11, 202, 39, 222, 206, 81, 163, 184, 122, 153, 52, 192, 98, 195,
523 240, 32, 34, 19, 160, 128, 178, 111, 97, 232, 113, 101, 221, 143,
524 ];
525 const SECRET1: &[u8; 32] = &[
526 157, 191, 36, 107, 110, 131, 193, 6, 175, 226, 193, 3, 168, 133, 165, 181, 65, 120,
527 194, 152, 31, 92, 37, 191, 73, 222, 41, 112, 207, 236, 196, 174,
528 ];
529
530 const INFO1: &[&[u8]] = &[
531 &[
532 2, 130, 61, 83, 192, 248, 63, 60, 211, 73, 169, 66, 101, 160, 196, 212, 250, 113,
533 ],
534 &[
535 80, 46, 248, 123, 78, 204, 171, 178, 67, 204, 96, 27, 131, 24,
536 ],
537 ];
538
539 let alg = HKDF_SHA256;
540 let salt = Salt::new(alg, SALT);
541 let prk = salt.extract(SECRET1);
542 let okm = prk.expand(INFO1, alg).unwrap();
543
544 assert_eq!(
545 "hkdf::Salt { algorithm: Algorithm(SHA256) }",
546 format!("{salt:?}")
547 );
548 assert_eq!(
549 "hkdf::Prk { algorithm: Algorithm(SHA256), mode: ExtractExpand { .. } }",
550 format!("{prk:?}")
551 );
552 assert_eq!(
553 "hkdf::Okm { prk: hkdf::Prk { algorithm: Algorithm(SHA256), mode: ExtractExpand { .. } } }",
554 format!("{okm:?}")
555 );
556 }
557
558 #[test]
559 fn test_salt_none_matches_empty_salt() {
560 let none = Salt::none(HKDF_SHA256);
561 let empty = Salt::new(HKDF_SHA256, &[]);
562 let secret = b"input keying material";
563 let info = [b"context".as_slice()];
564
565 let prk_none = none.extract(secret);
566 let prk_empty = empty.extract(secret);
567
568 let mut out_none = [0u8; 32];
569 let mut out_empty = [0u8; 32];
570 prk_none
571 .expand(&info, HKDF_SHA256)
572 .unwrap()
573 .fill(&mut out_none)
574 .unwrap();
575 prk_empty
576 .expand(&info, HKDF_SHA256)
577 .unwrap()
578 .fill(&mut out_empty)
579 .unwrap();
580
581 // RFC 5869: a missing/empty salt is equivalent to HashLen zero bytes;
582 // the two constructors must produce identical PRKs (and thus OKMs).
583 assert_eq!(out_none, out_empty);
584 }
585
586 #[test]
587 fn test_long_salt() {
588 // Test with a salt longer than the previous 80-byte limit
589 let long_salt = vec![0x42u8; 100];
590
591 // This should work now that we removed the MAX_HKDF_SALT_LEN restriction
592 let salt = Salt::new(HKDF_SHA256, &long_salt);
593
594 // Test the extract operation still works
595 let secret = b"test secret key material";
596 let prk = salt.extract(secret);
597
598 // Test expand operation
599 let info_data = b"test context info";
600 let info = [info_data.as_slice()];
601 let okm = prk.expand(&info, HKDF_SHA256).unwrap();
602
603 // Fill output buffer
604 let mut output = [0u8; 32];
605 okm.fill(&mut output).unwrap();
606
607 // Test with an even longer salt to demonstrate flexibility
608 let very_long_salt = vec![0x55u8; 500];
609 let very_long_salt_obj = Salt::new(HKDF_SHA256, &very_long_salt);
610 let prk2 = very_long_salt_obj.extract(secret);
611 let okm2 = prk2.expand(&info, HKDF_SHA256).unwrap();
612 let mut output2 = [0u8; 32];
613 okm2.fill(&mut output2).unwrap();
614
615 // Verify outputs are different (they should be due to different salts)
616 assert_ne!(output, output2);
617 }
618}