1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
//! HPKE DHKEM dispatcher: runtime selection of `DHKEM(X25519, HKDF-SHA-256)`,
//! `DHKEM(P-256, HKDF-SHA-256)`, `DHKEM(P-384, HKDF-SHA-384)`, and
//! `DHKEM(P-521, HKDF-SHA-512)` (RFC 9180 §7.1).
//!
//! The four KEMs share the same DHKEM construction (RFC 9180 §4.1); the
//! only differences are the curve, the encoded public-key length, the
//! private-scalar length, the bitmask used in `DeriveKeyPair`, and the
//! associated HKDF hash. Encoded public keys (`enc`) and raw private
//! scalars (`sk`) cross the API as opaque byte strings; this module
//! handles all curve-specific framing.
use super::labeled::{labeled_expand, labeled_extract};
use super::suite::kem_suite_id;
use super::{Error, HpkeKdf};
use crate::ec::boxed::BoxedEcdhPrivateKey;
use crate::ec::{BoxedEcdsaPublicKey, CurveId};
use crate::rng::RngCore;
use alloc::vec::Vec;
/// HPKE KEM identifiers (RFC 9180 §7.1).
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum HpkeKem {
/// `0x0010` — DHKEM(P-256, HKDF-SHA-256).
DhkemP256HkdfSha256,
/// `0x0011` — DHKEM(P-384, HKDF-SHA-384).
DhkemP384HkdfSha384,
/// `0x0012` — DHKEM(P-521, HKDF-SHA-512).
DhkemP521HkdfSha512,
/// `0x0020` — DHKEM(X25519, HKDF-SHA-256).
DhkemX25519HkdfSha256,
}
impl HpkeKem {
/// The IANA-assigned KEM id.
pub const fn id(self) -> u16 {
match self {
HpkeKem::DhkemP256HkdfSha256 => 0x0010,
HpkeKem::DhkemP384HkdfSha384 => 0x0011,
HpkeKem::DhkemP521HkdfSha512 => 0x0012,
HpkeKem::DhkemX25519HkdfSha256 => 0x0020,
}
}
/// The HKDF function used internally by this DHKEM (independent of
/// the suite's KDF choice).
pub const fn kdf(self) -> HpkeKdf {
match self {
HpkeKem::DhkemP256HkdfSha256 => HpkeKdf::HkdfSha256,
HpkeKem::DhkemP384HkdfSha384 => HpkeKdf::HkdfSha384,
HpkeKem::DhkemP521HkdfSha512 => HpkeKdf::HkdfSha512,
HpkeKem::DhkemX25519HkdfSha256 => HpkeKdf::HkdfSha256,
}
}
/// `Nsecret`: the KEM shared-secret length in bytes — equal to the
/// HKDF output length here.
pub const fn n_secret(self) -> usize {
self.kdf().output_len()
}
/// `Nenc`: the encoded encapsulated-key length in bytes.
///
/// For NIST curves this is the SEC1 uncompressed form
/// `0x04 || X || Y` (`1 + 2·field_len`). For X25519 it is the 32-byte
/// u-coordinate.
pub const fn n_enc(self) -> usize {
match self {
HpkeKem::DhkemP256HkdfSha256 => 65,
HpkeKem::DhkemP384HkdfSha384 => 97,
HpkeKem::DhkemP521HkdfSha512 => 133,
HpkeKem::DhkemX25519HkdfSha256 => 32,
}
}
/// `Npk`: the encoded recipient-public-key length. Identical to
/// `Nenc` for DHKEM.
pub const fn n_pk(self) -> usize {
self.n_enc()
}
/// `Nsk`: the raw private-scalar length in bytes.
pub const fn n_sk(self) -> usize {
match self {
HpkeKem::DhkemP256HkdfSha256 => 32,
HpkeKem::DhkemP384HkdfSha384 => 48,
HpkeKem::DhkemP521HkdfSha512 => 66,
HpkeKem::DhkemX25519HkdfSha256 => 32,
}
}
/// `bitmask` used by `DeriveKeyPair` for NIST curves. `0x01` on
/// P-521 (whose order is 521 bits — the top byte carries one bit);
/// `0xFF` elsewhere. Unused for X25519.
const fn bitmask(self) -> u8 {
match self {
HpkeKem::DhkemP521HkdfSha512 => 0x01,
_ => 0xFF,
}
}
/// Returns the NIST curve identifier for this KEM, or `None` for
/// X25519.
fn nist_curve(self) -> Option<CurveId> {
match self {
HpkeKem::DhkemP256HkdfSha256 => Some(CurveId::P256),
HpkeKem::DhkemP384HkdfSha384 => Some(CurveId::P384),
HpkeKem::DhkemP521HkdfSha512 => Some(CurveId::P521),
HpkeKem::DhkemX25519HkdfSha256 => None,
}
}
/// Validates an encoded public key without computing anything else
/// with it. For NIST curves this enforces SEC1 framing, in-range
/// coordinates, and on-curve membership (the underlying group is
/// prime-order, so a co-factor check is unnecessary). For X25519
/// every 32-byte string is a valid encoding; small-order rejection
/// happens later, in `dh`.
pub(crate) fn validate_public_key(self, pk: &[u8]) -> Result<(), Error> {
match self.nist_curve() {
Some(curve) => {
if pk.len() != self.n_pk() {
return Err(Error::InvalidKey);
}
BoxedEcdsaPublicKey::from_sec1(curve, pk).map_err(|_| Error::InvalidKey)?;
Ok(())
}
None => {
if pk.len() != 32 {
return Err(Error::InvalidKey);
}
Ok(())
}
}
}
/// `SerializePublicKey(pk(sk))`: derive the encoded public key from
/// a private scalar `sk`.
fn pk_from_sk(self, sk: &[u8]) -> Result<Vec<u8>, Error> {
match self.nist_curve() {
Some(curve) => {
if sk.len() != self.n_sk() {
return Err(Error::InvalidKey);
}
let pk =
BoxedEcdhPrivateKey::from_bytes(curve, sk).map_err(|_| Error::InvalidKey)?;
Ok(pk.public_key().to_sec1())
}
None => {
if sk.len() != 32 {
return Err(Error::InvalidKey);
}
let mut s = [0u8; 32];
s.copy_from_slice(sk);
let pk = crate::ec::x25519::X25519PrivateKey::from_bytes(s);
Ok(pk.public_key().to_vec())
}
}
}
/// `DH(sk, pk)`: the curve's Diffie-Hellman primitive. Returns the
/// raw shared field-element bytes (`field_len` for NIST,
/// 32 for X25519). All-zero / identity outputs map to
/// [`Error::InvalidDhOutput`] per RFC 9180 §7.1.3-§7.1.4.
fn dh(self, sk: &[u8], pk: &[u8]) -> Result<Vec<u8>, Error> {
match self.nist_curve() {
Some(curve) => {
if sk.len() != self.n_sk() || pk.len() != self.n_pk() {
return Err(Error::InvalidKey);
}
let sk =
BoxedEcdhPrivateKey::from_bytes(curve, sk).map_err(|_| Error::InvalidKey)?;
let pk =
BoxedEcdsaPublicKey::from_sec1(curve, pk).map_err(|_| Error::InvalidKey)?;
sk.diffie_hellman(&pk).map_err(|_| Error::InvalidDhOutput)
}
None => {
if sk.len() != 32 || pk.len() != 32 {
return Err(Error::InvalidKey);
}
let mut s = [0u8; 32];
s.copy_from_slice(sk);
let mut p = [0u8; 32];
p.copy_from_slice(pk);
crate::ec::x25519::X25519PrivateKey::from_bytes(s)
.diffie_hellman(&p)
.map(|out| out.to_vec())
.map_err(|_| Error::InvalidDhOutput)
}
}
}
/// `DeriveKeyPair(ikm)` (RFC 9180 §7.1.3/§7.1.4): deterministically
/// derives `(sk, enc_pk)` from `ikm`. For NIST curves this is a
/// rejection-sample loop bounded at 256 candidates; for X25519 it
/// is a single HKDF expansion.
pub(crate) fn derive_key_pair(self, ikm: &[u8]) -> Result<(Vec<u8>, Vec<u8>), Error> {
let suite_id = kem_suite_id(self.id());
let kdf = self.kdf();
let dkp_prk = labeled_extract(kdf, b"", &suite_id, b"dkp_prk", ikm);
match self.nist_curve() {
Some(curve) => {
let n_sk = self.n_sk();
let bitmask = self.bitmask();
for counter in 0u16..=255 {
let mut bytes = alloc::vec![0u8; n_sk];
labeled_expand(
kdf,
&dkp_prk,
&suite_id,
b"candidate",
&[counter as u8],
&mut bytes,
);
bytes[0] &= bitmask;
// BoxedEcdhPrivateKey::from_bytes enforces `1 <= sk < n`;
// anything outside the valid scalar range is rejected
// here and the loop retries with the next counter.
if let Ok(sk) = BoxedEcdhPrivateKey::from_bytes(curve, &bytes) {
let pk = sk.public_key().to_sec1();
return Ok((bytes, pk));
}
}
Err(Error::DeriveKeyPair)
}
None => {
let mut sk = alloc::vec![0u8; 32];
labeled_expand(kdf, &dkp_prk, &suite_id, b"sk", b"", &mut sk);
let pk = self.pk_from_sk(&sk)?;
Ok((sk, pk))
}
}
}
/// `GenerateKeyPair`: draws `ikm = Nsk` random bytes from `rng` and
/// runs the same `DeriveKeyPair` chain (RFC 9180 §7.1.3/§7.1.4) used
/// for deterministic key derivation. Returns `(sk, encoded_pk)`.
pub fn generate_key_pair<R: RngCore>(self, rng: &mut R) -> Result<(Vec<u8>, Vec<u8>), Error> {
let mut ikm = alloc::vec![0u8; self.n_sk()];
rng.fill_bytes(&mut ikm);
self.derive_key_pair(&ikm)
}
/// `ExtractAndExpand(dh, kem_context)` (RFC 9180 §4.1): the DHKEM
/// internal KDF chain.
fn extract_and_expand(self, dh: &[u8], kem_context: &[u8]) -> Vec<u8> {
let suite_id = kem_suite_id(self.id());
let kdf = self.kdf();
let mut eae_prk = labeled_extract(kdf, b"", &suite_id, b"eae_prk", dh);
let mut shared = alloc::vec![0u8; self.n_secret()];
labeled_expand(
kdf,
&eae_prk,
&suite_id,
b"shared_secret",
kem_context,
&mut shared,
);
super::wipe(&mut eae_prk);
shared
}
/// `Encap(pkR)`: generates an ephemeral DH key, derives the shared
/// secret, and returns `(shared_secret, enc)`.
pub(crate) fn encap<R: RngCore>(
self,
rng: &mut R,
pk_r: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), Error> {
self.validate_public_key(pk_r)?;
let (mut sk_e, pk_e) = self.generate_key_pair(rng)?;
// Wipe the ephemeral scalar as soon as the DH is done — before
// propagating any DH failure.
let dh = self.dh(&sk_e, pk_r);
super::wipe(&mut sk_e);
let mut dh = dh?;
let mut kem_context = Vec::with_capacity(pk_e.len() + pk_r.len());
kem_context.extend_from_slice(&pk_e);
kem_context.extend_from_slice(pk_r);
let shared = self.extract_and_expand(&dh, &kem_context);
super::wipe(&mut dh);
Ok((shared, pk_e))
}
/// `Decap(enc, skR)`.
pub(crate) fn decap(self, enc: &[u8], sk_r: &[u8]) -> Result<Vec<u8>, Error> {
if enc.len() != self.n_enc() {
return Err(Error::InvalidEnc);
}
self.validate_public_key(enc)?;
// Derive the public key first so no fallible step sits between the
// DH output's creation and its wipe below.
let pk_r = self.pk_from_sk(sk_r)?;
let mut dh = self.dh(sk_r, enc)?;
let mut kem_context = Vec::with_capacity(enc.len() + pk_r.len());
kem_context.extend_from_slice(enc);
kem_context.extend_from_slice(&pk_r);
let shared = self.extract_and_expand(&dh, &kem_context);
super::wipe(&mut dh);
Ok(shared)
}
/// `AuthEncap(pkR, skS)`: like [`encap`](Self::encap) but also
/// binds the sender's static identity into the shared secret.
pub(crate) fn auth_encap<R: RngCore>(
self,
rng: &mut R,
pk_r: &[u8],
sk_s: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), Error> {
self.validate_public_key(pk_r)?;
// Derive the sender's public key first so no fallible step sits
// between the DH outputs' creation and their wipes below.
let pk_s = self.pk_from_sk(sk_s)?;
let (mut sk_e, pk_e) = self.generate_key_pair(rng)?;
// Wipe the ephemeral scalar as soon as the DH is done — before
// propagating any DH failure.
let dh1 = self.dh(&sk_e, pk_r);
super::wipe(&mut sk_e);
let mut dh1 = dh1?;
let mut dh2 = match self.dh(sk_s, pk_r) {
Ok(dh2) => dh2,
Err(e) => {
super::wipe(&mut dh1);
return Err(e);
}
};
let mut dh = Vec::with_capacity(dh1.len() + dh2.len());
dh.extend_from_slice(&dh1);
dh.extend_from_slice(&dh2);
super::wipe(&mut dh1);
super::wipe(&mut dh2);
let mut kem_context = Vec::with_capacity(pk_e.len() + pk_r.len() + pk_s.len());
kem_context.extend_from_slice(&pk_e);
kem_context.extend_from_slice(pk_r);
kem_context.extend_from_slice(&pk_s);
let shared = self.extract_and_expand(&dh, &kem_context);
super::wipe(&mut dh);
Ok((shared, pk_e))
}
/// `AuthDecap(enc, skR, pkS)`.
pub(crate) fn auth_decap(self, enc: &[u8], sk_r: &[u8], pk_s: &[u8]) -> Result<Vec<u8>, Error> {
if enc.len() != self.n_enc() {
return Err(Error::InvalidEnc);
}
self.validate_public_key(enc)?;
self.validate_public_key(pk_s)?;
// Derive the public key first so no fallible step sits between the
// DH outputs' creation and their wipes below.
let pk_r = self.pk_from_sk(sk_r)?;
let mut dh1 = self.dh(sk_r, enc)?;
let mut dh2 = match self.dh(sk_r, pk_s) {
Ok(dh2) => dh2,
Err(e) => {
super::wipe(&mut dh1);
return Err(e);
}
};
let mut dh = Vec::with_capacity(dh1.len() + dh2.len());
dh.extend_from_slice(&dh1);
dh.extend_from_slice(&dh2);
super::wipe(&mut dh1);
super::wipe(&mut dh2);
let mut kem_context = Vec::with_capacity(enc.len() + pk_r.len() + pk_s.len());
kem_context.extend_from_slice(enc);
kem_context.extend_from_slice(&pk_r);
kem_context.extend_from_slice(pk_s);
let shared = self.extract_and_expand(&dh, &kem_context);
super::wipe(&mut dh);
Ok(shared)
}
}