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