Skip to content

Commit 4764d8b

Browse files
committed
Extend nss-rs with a KEM API
1 parent 78715d7 commit 4764d8b

3 files changed

Lines changed: 338 additions & 0 deletions

File tree

bindings/bindings.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ functions = [
231231
"PK11_SignWithMechanism",
232232
"PK11_VerifyWithMechanism",
233233
"PK11_WrapPrivKey",
234+
"PK11_Encapsulate",
235+
"PK11_Decapsulate",
234236
"SECITEM_AllocItem",
235237
"SECITEM_ReallocItemV2",
236238
"SECKEY_CopyPrivateKey",
@@ -296,6 +298,14 @@ variables = [
296298
"CKM_SHA512_HMAC",
297299
"CKM_ECDSA",
298300
"CKM_EDDSA",
301+
"CKM_ML_KEM_KEY_PAIR_GEN",
302+
"CKM_ML_KEM",
303+
"CKP_ML_KEM_512",
304+
"CKP_ML_KEM_768",
305+
"CKP_ML_KEM_1024",
306+
"PK11_ATTR_SESSION",
307+
"PK11_ATTR_INSENSITIVE",
308+
"PK11_ATTR_SENSITIVE",
299309
]
300310

301311
[nspr_err]

src/kem.rs

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4+
// option. This file may not be copied, modified, or distributed
5+
// except according to those terms.
6+
7+
//! Key Encapsulation Mechanism (KEM) support for ML-KEM (FIPS 203).
8+
//!
9+
//! This module provides Rust bindings for NSS's ML-KEM implementation,
10+
//! supporting ML-KEM-768 and ML-KEM-1024 parameter sets.
11+
12+
use std::ptr::null_mut;
13+
14+
use crate::{
15+
err::{secstatus_to_res, IntoResult as _, Res},
16+
init,
17+
p11::{self, PrivateKey, PublicKey, Slot, SymKey},
18+
prtypes::PRUint32,
19+
util::SECItemBorrowed,
20+
ScopedSECItem, SECItem,
21+
};
22+
23+
/// ML-KEM parameter sets as defined in FIPS 203.
24+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25+
pub enum MlKemParameterSet {
26+
/// ML-KEM-768: 192-bit security level
27+
MlKem768,
28+
/// ML-KEM-1024: 256-bit security level
29+
MlKem1024,
30+
}
31+
32+
impl MlKemParameterSet {
33+
/// Returns the PKCS#11 parameter set constant.
34+
#[must_use]
35+
pub const fn to_ck_param(self) -> u64 {
36+
match self {
37+
Self::MlKem768 => p11::CKP_ML_KEM_768 as u64,
38+
Self::MlKem1024 => p11::CKP_ML_KEM_1024 as u64,
39+
}
40+
}
41+
42+
/// Returns the public key size in bytes.
43+
#[must_use]
44+
pub const fn public_key_bytes(self) -> usize {
45+
match self {
46+
Self::MlKem768 => 1184,
47+
Self::MlKem1024 => 1568,
48+
}
49+
}
50+
51+
/// Returns the private key size in bytes.
52+
#[must_use]
53+
pub const fn private_key_bytes(self) -> usize {
54+
match self {
55+
Self::MlKem768 => 2400,
56+
Self::MlKem1024 => 3168,
57+
}
58+
}
59+
60+
/// Returns the ciphertext size in bytes.
61+
#[must_use]
62+
pub const fn ciphertext_bytes(self) -> usize {
63+
match self {
64+
Self::MlKem768 => 1088,
65+
Self::MlKem1024 => 1568,
66+
}
67+
}
68+
69+
/// Returns the shared secret size in bytes (always 32 for ML-KEM).
70+
#[must_use]
71+
pub const fn shared_secret_bytes(self) -> usize {
72+
32
73+
}
74+
}
75+
76+
/// An ML-KEM key pair.
77+
pub struct MlKemKeypair {
78+
pub public: PublicKey,
79+
pub private: PrivateKey,
80+
}
81+
82+
/// Type alias for PK11 attribute flags.
83+
type Pk11AttrFlags = PRUint32;
84+
85+
/// Generate an ML-KEM key pair.
86+
///
87+
/// # Arguments
88+
///
89+
/// * `params` - The ML-KEM parameter set to use.
90+
///
91+
/// # Errors
92+
///
93+
/// Returns an error if NSS initialization fails or key generation fails.
94+
pub fn generate_keypair(params: MlKemParameterSet) -> Res<MlKemKeypair> {
95+
init()?;
96+
97+
let slot = Slot::internal()?;
98+
99+
// Create the parameter for key generation (the parameter set as CK_ULONG)
100+
// The parameter is passed as a pointer to the CK_ULONG value
101+
let mut ck_param: p11::CK_ULONG = match params {
102+
MlKemParameterSet::MlKem768 => p11::CKP_ML_KEM_768.into(),
103+
MlKemParameterSet::MlKem1024 => p11::CKP_ML_KEM_1024.into(),
104+
};
105+
106+
let mut public_ptr: *mut p11::SECKEYPublicKey = null_mut();
107+
108+
// Generate the key pair using the standard ML-KEM mechanism
109+
let private_ptr = unsafe {
110+
p11::PK11_GenerateKeyPair(
111+
*slot,
112+
p11::CK_MECHANISM_TYPE::from(p11::CKM_ML_KEM_KEY_PAIR_GEN),
113+
std::ptr::addr_of_mut!(ck_param).cast(),
114+
&mut public_ptr,
115+
pkcs11_bindings::CK_FALSE.into(),
116+
pkcs11_bindings::CK_FALSE.into(),
117+
null_mut(),
118+
)
119+
};
120+
121+
let private = unsafe { PrivateKey::from_ptr(private_ptr)? };
122+
let public = unsafe { PublicKey::from_ptr(public_ptr)? };
123+
124+
Ok(MlKemKeypair { public, private })
125+
}
126+
127+
/// Encapsulate a shared secret using an ML-KEM public key.
128+
///
129+
/// This function generates a random shared secret and encapsulates it using
130+
/// the provided public key. The encapsulation (ciphertext) can be sent to
131+
/// the holder of the corresponding private key, who can decapsulate it to
132+
/// recover the same shared secret.
133+
///
134+
/// # Arguments
135+
///
136+
/// * `public_key` - The recipient's ML-KEM public key.
137+
/// * `target` - The target mechanism for the derived symmetric key (e.g., `CKM_HKDF_DERIVE`).
138+
///
139+
/// # Returns
140+
///
141+
/// A tuple of `(shared_secret, ciphertext)` where:
142+
/// - `shared_secret` is a symmetric key that can be used for further key derivation.
143+
/// - `ciphertext` is the encapsulation that should be sent to the private key holder.
144+
///
145+
/// # Errors
146+
///
147+
/// Returns an error if NSS initialization fails or encapsulation fails.
148+
pub fn encapsulate(
149+
public_key: &PublicKey,
150+
target: p11::CK_MECHANISM_TYPE,
151+
) -> Res<(SymKey, Vec<u8>)> {
152+
init()?;
153+
154+
let attr_flags: Pk11AttrFlags = p11::PK11_ATTR_SESSION | p11::PK11_ATTR_INSENSITIVE;
155+
let op_flags: p11::CK_FLAGS = p11::CKF_DERIVE.into();
156+
157+
let mut shared_secret_ptr: *mut p11::PK11SymKey = null_mut();
158+
let mut ciphertext_ptr: *mut SECItem = null_mut();
159+
160+
secstatus_to_res(unsafe {
161+
p11::PK11_Encapsulate(
162+
**public_key,
163+
target,
164+
attr_flags,
165+
op_flags,
166+
&mut shared_secret_ptr,
167+
&mut ciphertext_ptr,
168+
)
169+
})?;
170+
171+
let shared_secret = unsafe { SymKey::from_ptr(shared_secret_ptr)? };
172+
let ciphertext_item: ScopedSECItem = unsafe { ciphertext_ptr.into_result()? };
173+
let ciphertext = unsafe { ciphertext_item.into_vec() };
174+
175+
Ok((shared_secret, ciphertext))
176+
}
177+
178+
/// Decapsulate a ciphertext using an ML-KEM private key.
179+
///
180+
/// This function recovers the shared secret from an encapsulation (ciphertext)
181+
/// using the corresponding private key.
182+
///
183+
/// # Arguments
184+
///
185+
/// * `private_key` - The ML-KEM private key.
186+
/// * `ciphertext` - The encapsulation received from the sender.
187+
/// * `target` - The target mechanism for the derived symmetric key (e.g., `CKM_HKDF_DERIVE`).
188+
///
189+
/// # Returns
190+
///
191+
/// The shared secret as a symmetric key.
192+
///
193+
/// # Errors
194+
///
195+
/// Returns an error if NSS initialization fails or decapsulation fails.
196+
pub fn decapsulate(
197+
private_key: &PrivateKey,
198+
ciphertext: &[u8],
199+
target: p11::CK_MECHANISM_TYPE,
200+
) -> Res<SymKey> {
201+
init()?;
202+
203+
let attr_flags: Pk11AttrFlags = p11::PK11_ATTR_SESSION | p11::PK11_ATTR_INSENSITIVE;
204+
let op_flags: p11::CK_FLAGS = p11::CKF_DERIVE.into();
205+
206+
let mut ciphertext_item = SECItemBorrowed::wrap(ciphertext)?;
207+
let ciphertext_ptr: *mut SECItem = ciphertext_item.as_mut();
208+
209+
let mut shared_secret_ptr: *mut p11::PK11SymKey = null_mut();
210+
211+
secstatus_to_res(unsafe {
212+
p11::PK11_Decapsulate(
213+
**private_key,
214+
ciphertext_ptr,
215+
target,
216+
attr_flags,
217+
op_flags,
218+
&mut shared_secret_ptr,
219+
)
220+
})?;
221+
222+
let shared_secret = unsafe { SymKey::from_ptr(shared_secret_ptr)? };
223+
224+
Ok(shared_secret)
225+
}
226+
227+
#[cfg(test)]
228+
#[cfg_attr(coverage_nightly, coverage(off))]
229+
mod tests {
230+
use test_fixture::fixture_init;
231+
232+
use super::*;
233+
234+
#[test]
235+
fn generate_mlkem768_keypair() {
236+
fixture_init();
237+
let keypair = generate_keypair(MlKemParameterSet::MlKem768).unwrap();
238+
// Keypair was successfully created (from_ptr would have failed if null)
239+
assert!(!(*keypair.public).is_null());
240+
assert!(!(*keypair.private).is_null());
241+
}
242+
243+
#[test]
244+
fn generate_mlkem1024_keypair() {
245+
fixture_init();
246+
let keypair = generate_keypair(MlKemParameterSet::MlKem1024).unwrap();
247+
// Keypair was successfully created (from_ptr would have failed if null)
248+
assert!(!(*keypair.public).is_null());
249+
assert!(!(*keypair.private).is_null());
250+
}
251+
252+
#[test]
253+
fn encapsulate_decapsulate_mlkem768() {
254+
fixture_init();
255+
256+
// Generate a key pair
257+
let keypair = generate_keypair(MlKemParameterSet::MlKem768).unwrap();
258+
259+
// Encapsulate using the public key
260+
let target = p11::CKM_HKDF_DERIVE.into();
261+
let (shared_secret1, ciphertext) = encapsulate(&keypair.public, target).unwrap();
262+
263+
// Verify ciphertext size
264+
assert_eq!(
265+
ciphertext.len(),
266+
MlKemParameterSet::MlKem768.ciphertext_bytes()
267+
);
268+
269+
// Decapsulate using the private key
270+
let shared_secret2 = decapsulate(&keypair.private, &ciphertext, target).unwrap();
271+
272+
// Verify that both shared secrets are the same
273+
let ss1 = shared_secret1.key_data().unwrap();
274+
let ss2 = shared_secret2.key_data().unwrap();
275+
assert_eq!(ss1, ss2);
276+
assert_eq!(ss1.len(), MlKemParameterSet::MlKem768.shared_secret_bytes());
277+
}
278+
279+
#[test]
280+
fn encapsulate_decapsulate_mlkem1024() {
281+
fixture_init();
282+
283+
// Generate a key pair
284+
let keypair = generate_keypair(MlKemParameterSet::MlKem1024).unwrap();
285+
286+
// Encapsulate using the public key
287+
let target = p11::CKM_HKDF_DERIVE.into();
288+
let (shared_secret1, ciphertext) = encapsulate(&keypair.public, target).unwrap();
289+
290+
// Verify ciphertext size
291+
assert_eq!(
292+
ciphertext.len(),
293+
MlKemParameterSet::MlKem1024.ciphertext_bytes()
294+
);
295+
296+
// Decapsulate using the private key
297+
let shared_secret2 = decapsulate(&keypair.private, &ciphertext, target).unwrap();
298+
299+
// Verify that both shared secrets are the same
300+
let ss1 = shared_secret1.key_data().unwrap();
301+
let ss2 = shared_secret2.key_data().unwrap();
302+
assert_eq!(ss1, ss2);
303+
assert_eq!(
304+
ss1.len(),
305+
MlKemParameterSet::MlKem1024.shared_secret_bytes()
306+
);
307+
}
308+
309+
#[test]
310+
fn parameter_set_sizes() {
311+
// ML-KEM-768 sizes
312+
assert_eq!(MlKemParameterSet::MlKem768.public_key_bytes(), 1184);
313+
assert_eq!(MlKemParameterSet::MlKem768.private_key_bytes(), 2400);
314+
assert_eq!(MlKemParameterSet::MlKem768.ciphertext_bytes(), 1088);
315+
assert_eq!(MlKemParameterSet::MlKem768.shared_secret_bytes(), 32);
316+
317+
// ML-KEM-1024 sizes
318+
assert_eq!(MlKemParameterSet::MlKem1024.public_key_bytes(), 1568);
319+
assert_eq!(MlKemParameterSet::MlKem1024.private_key_bytes(), 3168);
320+
assert_eq!(MlKemParameterSet::MlKem1024.ciphertext_bytes(), 1568);
321+
assert_eq!(MlKemParameterSet::MlKem1024.shared_secret_bytes(), 32);
322+
}
323+
}

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub mod der;
4040
pub mod ec;
4141
pub mod hash;
4242
pub mod hmac;
43+
pub mod kem;
4344
pub mod p11;
4445
mod prio;
4546
mod replay;
@@ -80,6 +81,10 @@ pub use self::{
8081
},
8182
err::{secstatus_to_res, Error, IntoResult, PRErrorCode, Res},
8283
ext::{ExtensionHandler, ExtensionHandlerResult, ExtensionWriterResult},
84+
kem::{
85+
decapsulate as kem_decapsulate, encapsulate as kem_encapsulate,
86+
generate_keypair as kem_generate_keypair, MlKemKeypair, MlKemParameterSet,
87+
},
8388
p11::{random, randomize, PrivateKey, PublicKey, SymKey},
8489
replay::AntiReplay,
8590
secrets::SecretDirection,

0 commit comments

Comments
 (0)