Skip to main content

hpke/
kdf.rs

1//! Key derivation functions for HPKE (`draft-ietf-hpke-hpke-05` Section 4 and
2//! Section 7.2).
3
4use crypto::{
5    Hash, MAX_HASH_OUTPUT_SIZE,
6    hmac::Hmac,
7    sha2::{Sha256, Sha512},
8};
9
10use super::HpkeError;
11
12/// The HPKE version label used by the labeled derivations
13/// (`draft-ietf-hpke-hpke-05` Section 4.4).
14const VERSION_LABEL: &[u8] = b"HPKE-v1";
15
16/// Key derivation function (KDF) usable within an HPKE ciphersuite.
17///
18/// The default implementations follow the `draft-ietf-hpke-hpke-05`
19/// constructions on top of HKDF (RFC 5869) with the hash [`Kdf::Hash`]. `suite_id` is the caller's domain
20/// separator: the 5-byte KEM `suite_id` (`"KEM" || I2OSP(kem_id, 2)`) inside a
21/// KEM, or the 10-byte ciphersuite `suite_id` (`"HPKE" || I2OSP(kem_id, 2) ||
22/// I2OSP(kdf_id, 2) || I2OSP(aead_id, 2)`) everywhere else. Passing the wrong
23/// one silently breaks interoperability.
24///
25/// Implementors only need to provide [`Kdf::HPKE_KDF_ID`] and the hash [`Kdf::Hash`]; for
26/// example, a KDF based on another hash is two lines.
27///
28/// # Single-stage KDFs
29///
30/// The two-stage key schedule is built from
31/// [`Kdf::labeled_extract`] and [`Kdf::labeled_expand`]. `draft-ietf-hpke-hpke-05` also defines
32/// single-stage KDFs which derive the whole key schedule from a single [`Kdf::labeled_derive`] call,
33/// such as the SHAKE256 KDF of `draft-ietf-hpke-pq-05`.
34/// A single-stage KDF sets [`Kdf::SINGLE_STAGE`] to `true` and overrides
35/// [`Kdf::labeled_derive`] (its [`Kdf::labeled_extract`] and
36/// [`Kdf::labeled_expand`] implementations are never used).
37///
38/// # Input length restrictions
39///
40/// A single-stage `LabeledDerive` prefixes each length with two bytes, so the
41/// `psk`, `psk_id`, and `info` inputs are limited to 65,535 bytes; the key
42/// schedule and single-stage export enforce this (`draft-ietf-hpke-hpke-05`
43/// Section 7.2.1). For two-stage KDFs the inputs are streamed into the
44/// underlying KDF, so `info` values of at least 16,384 bytes are supported, as
45/// Section 7.2.1 recommends.
46///
47/// # Example
48///
49/// ```
50/// use hpke::kdf::{HkdfSha256, Kdf};
51///
52/// let suite_id = b"HPKE\x00\x20\x00\x01\x00\x02";
53/// let pseudorandom_key = HkdfSha256::labeled_extract(b"salt", suite_id, b"label", b"ikm");
54/// let mut output_keying_material = [0u8; 32];
55/// HkdfSha256::labeled_expand(&mut output_keying_material, &pseudorandom_key, suite_id, b"label", b"info").unwrap();
56/// ```
57pub trait Kdf {
58    /// The HPKE KDF identifier (`draft-ietf-hpke-hpke-05` Section 7.2, IANA "HPKE KDF
59    /// Identifiers"), used to build the ciphersuite `suite_id`.
60    const HPKE_KDF_ID: u16;
61
62    /// The hash function underlying the default HKDF construction.
63    type Hash: crypto::Hasher;
64
65    /// Output size of the extract step (the hash output size).
66    ///
67    /// Must not exceed [`MAX_HASH_OUTPUT_SIZE`].
68    const OUTPUT_SIZE: usize = <Self::Hash as crypto::Hasher>::OUTPUT_SIZE;
69
70    /// Whether this is a single-stage KDF (`draft-ietf-hpke-hpke-05` Section 4).
71    ///
72    /// Two-stage KDFs (the default) use the two-stage key schedule built on
73    /// [`Kdf::labeled_extract`] and [`Kdf::labeled_expand`]. Single-stage KDFs
74    /// (e.g. [`Shake256`]) build the schedule from a single
75    /// [`Kdf::labeled_derive`] call and MUST override it.
76    const SINGLE_STAGE: bool = false;
77
78    /// `draft-ietf-hpke-hpke-05` Section 4.4
79    /// `LabeledExtract(salt, label, input_keying_material)`:
80    /// `Extract(salt, "HPKE-v1" || suite_id || label || input_keying_material)`.
81    ///
82    /// `salt` may be empty, in which case the HKDF default salt (a string of
83    /// [`Kdf::OUTPUT_SIZE`] zero bytes) is used. The prefix is fed to HMAC
84    /// incrementally, so `input_keying_material` may be arbitrarily long.
85    fn labeled_extract(salt: &[u8], suite_id: &[u8], label: &[u8], input_keying_material: &[u8]) -> Hash {
86        const { assert!(Self::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE) };
87
88        let mut mac = Hmac::<Self::Hash>::new(salt);
89        mac.update(VERSION_LABEL);
90        mac.update(suite_id);
91        mac.update(label);
92        mac.update(input_keying_material);
93        return mac.finalize();
94    }
95
96    /// `draft-ietf-hpke-hpke-05` Section 4.4
97    /// `LabeledExpand(pseudorandom_key, label, info, L)`:
98    /// `Expand(pseudorandom_key, I2OSP(L, 2) || "HPKE-v1" || suite_id || label || info, L)`,
99    /// where `L == out.len()`. The `info` string is absorbed incrementally, so
100    /// it may be arbitrarily long.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`HpkeError::InvalidPseudorandomKey`] when `pseudorandom_key` is not exactly
105    /// [`Kdf::OUTPUT_SIZE`] bytes long, and [`HpkeError::KdfOutputTooLong`]
106    /// when `out.len() > 255 * OUTPUT_SIZE`.
107    fn labeled_expand(
108        out: &mut [u8],
109        pseudorandom_key: &[u8],
110        suite_id: &[u8],
111        label: &[u8],
112        info: &[u8],
113    ) -> Result<(), HpkeError> {
114        const { assert!(Self::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE) };
115
116        if pseudorandom_key.len() != Self::OUTPUT_SIZE {
117            return Err(HpkeError::InvalidPseudorandomKey);
118        }
119
120        let n = out.len();
121        if n > 255 * Self::OUTPUT_SIZE {
122            return Err(HpkeError::KdfOutputTooLong);
123        }
124
125        if n == 0 {
126            return Ok(());
127        }
128
129        let mut t = [0u8; MAX_HASH_OUTPUT_SIZE];
130        let mut t_len = 0usize;
131        let mut offset = 0usize;
132        let mut counter = 1u8;
133
134        while offset < n {
135            let mut mac = Hmac::<Self::Hash>::new(pseudorandom_key);
136            mac.update(&t[..t_len]);
137            mac.update(&(n as u16).to_be_bytes());
138            mac.update(VERSION_LABEL);
139            mac.update(suite_id);
140            mac.update(label);
141            mac.update(info);
142            mac.update(&[counter]);
143
144            let block = mac.finalize();
145            let block_bytes = block.as_ref();
146            let chunk_len = (n - offset).min(Self::OUTPUT_SIZE);
147            out[offset..offset + chunk_len].copy_from_slice(&block_bytes[..chunk_len]);
148            t[..Self::OUTPUT_SIZE].copy_from_slice(block_bytes);
149            t_len = Self::OUTPUT_SIZE;
150            offset += chunk_len;
151            counter = counter.wrapping_add(1);
152        }
153
154        return Ok(());
155    }
156
157    /// `draft-ietf-hpke-hpke-05` Section 4.4
158    /// `LabeledDerive(input_keying_material, label, context, L)`:
159    /// `Derive(input_keying_material || "HPKE-v1" || suite_id ||
160    /// I2OSP(len(label), 2) || label || I2OSP(L, 2) || context, L)`, where
161    /// `L == out.len()` and the elements of `input_keying_material` and
162    /// `context` are absorbed in order.
163    ///
164    /// This is the single-stage counterpart of
165    /// [`Kdf::labeled_extract`]/[`Kdf::labeled_expand`]. `suite_id` has the
166    /// same meaning as for the two-stage functions: the 5-byte KEM `suite_id`
167    /// inside a KEM, or the 10-byte ciphersuite `suite_id` everywhere else.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`HpkeError::KdfOutputTooLong`] when `out` is longer than
172    /// `2^16 - 1` bytes, and [`HpkeError::NotSupported`] by default (only
173    /// single-stage KDFs implement this).
174    fn labeled_derive(
175        _out: &mut [u8],
176        _input_keying_material: &[&[u8]],
177        _suite_id: &[u8],
178        _label: &[u8],
179        _context: &[&[u8]],
180    ) -> Result<(), HpkeError> {
181        return Err(HpkeError::NotSupported);
182    }
183}
184
185/// HKDF-SHA256 (KDF id `0x0001`).
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct HkdfSha256;
188
189impl Kdf for HkdfSha256 {
190    const HPKE_KDF_ID: u16 = 0x0001;
191    type Hash = Sha256;
192}
193
194/// HKDF-SHA512 (KDF id `0x0003`).
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct HkdfSha512;
197
198impl Kdf for HkdfSha512 {
199    const HPKE_KDF_ID: u16 = 0x0003;
200    type Hash = Sha512;
201}
202
203/// SHAKE256 single-stage KDF (KDF id `0x0011`), as specified in
204/// [`draft-ietf-hpke-pq-05`](https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05)
205/// Section 5.
206///
207/// SHAKE256 is a single-stage KDF with an output size of 64 bytes. It derives
208/// the key schedule with a single [`Kdf::labeled_derive`] call instead of the
209/// two-stage `LabeledExtract`/`LabeledExpand` construction, and is a natural
210/// pairing with the hybrid post-quantum KEMs defined in the same draft.
211///
212/// # Example
213///
214/// ```
215/// use hpke::kdf::{Kdf, Shake256};
216///
217/// let suite_id = b"HPKE\x64\x7a\x00\x11\x00\x03";
218/// let mut output_keying_material = [0u8; 32];
219/// Shake256::labeled_derive(&mut output_keying_material, &[b"ikm"], suite_id, b"label", &[b"info"]).unwrap();
220/// ```
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub struct Shake256;
223
224impl Kdf for Shake256 {
225    const HPKE_KDF_ID: u16 = 0x0011;
226    const SINGLE_STAGE: bool = true;
227    type Hash = crypto::sha3::Shake256;
228
229    fn labeled_derive(
230        out: &mut [u8],
231        input_keying_material: &[&[u8]],
232        suite_id: &[u8],
233        label: &[u8],
234        context: &[&[u8]],
235    ) -> Result<(), HpkeError> {
236        if out.len() > u16::MAX as usize {
237            return Err(HpkeError::KdfOutputTooLong);
238        }
239        if label.len() > u16::MAX as usize {
240            return Err(HpkeError::ValidationError);
241        }
242
243        let label_len = (label.len() as u16).to_be_bytes();
244        let out_len = (out.len() as u16).to_be_bytes();
245
246        use crypto::Xof;
247        let mut xof = crypto::sha3::Shake256::new();
248        for part in input_keying_material {
249            xof.absorb(part);
250        }
251        xof.absorb(VERSION_LABEL);
252        xof.absorb(suite_id);
253        xof.absorb(&label_len);
254        xof.absorb(label);
255        xof.absorb(&out_len);
256        for part in context {
257            xof.absorb(part);
258        }
259        xof.squeeze(out);
260        return Ok(());
261    }
262}
263
264/// BLAKE3 single-stage KDF (unofficial KDF id `0xFF01`).
265///
266/// BLAKE3 is an extendable-output function, so it is used exactly like
267/// [`Shake256`]: the key schedule is built from a single [`Kdf::labeled_derive`]
268/// call whose `Derive` primitive is the BLAKE3 XOF. The output size is 32
269/// bytes.
270///
271/// # Interoperability
272///
273/// BLAKE3 is **not** registered in the IANA "HPKE KDF Identifiers" registry and
274/// is not specified by any HPKE standard. The identifier `0xFF01` is an
275/// stdx-private, unofficial value chosen to stay clear of the low IDs IANA is
276/// likely to assign to future official KDFs; ciphersuites using this KDF only
277/// interoperate with implementations that adopt the same convention.
278///
279/// # Example
280///
281/// ```
282/// use hpke::kdf::{Blake3, Kdf};
283///
284/// let suite_id = b"HPKE\x64\x7a\xff\x01\x00\x03";
285/// let mut output_keying_material = [0u8; 32];
286/// Blake3::labeled_derive(&mut output_keying_material, &[b"ikm"], suite_id, b"label", &[b"info"]).unwrap();
287/// ```
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub struct Blake3;
290
291impl Kdf for Blake3 {
292    const HPKE_KDF_ID: u16 = 0xFF01;
293    const SINGLE_STAGE: bool = true;
294    type Hash = crypto::blake3::Blake3;
295
296    fn labeled_derive(
297        out: &mut [u8],
298        input_keying_material: &[&[u8]],
299        suite_id: &[u8],
300        label: &[u8],
301        context: &[&[u8]],
302    ) -> Result<(), HpkeError> {
303        if out.len() > u16::MAX as usize {
304            return Err(HpkeError::KdfOutputTooLong);
305        }
306        if label.len() > u16::MAX as usize {
307            return Err(HpkeError::ValidationError);
308        }
309
310        let label_len = (label.len() as u16).to_be_bytes();
311        let out_len = (out.len() as u16).to_be_bytes();
312
313        use crypto::{Hasher, Xof};
314        let mut hasher = crypto::blake3::Blake3::new();
315        for part in input_keying_material {
316            hasher.update(part);
317        }
318        hasher.update(VERSION_LABEL);
319        hasher.update(suite_id);
320        hasher.update(&label_len);
321        hasher.update(label);
322        hasher.update(&out_len);
323        for part in context {
324            hasher.update(part);
325        }
326        hasher.finalize_xof().squeeze(out);
327        return Ok(());
328    }
329}