|
| 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 | +} |
0 commit comments