Skip to main content

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}