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}