Skip to content

Commit d7ffd0e

Browse files
committed
Extend public_key imports for KEMs
1 parent 48915a8 commit d7ffd0e

4 files changed

Lines changed: 207 additions & 7 deletions

File tree

bindings/bindings.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ enums = [
256256
opaque = [
257257
"PK11ContextStr",
258258
"PK11SlotInfoStr",
259-
"SECKEYPublicKeyStr",
260259
]
261260
variables = [
262261
"AES_BLOCK_SIZE",

src/kem.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,60 @@ pub(crate) fn mlkem_decapsulate(
306306
Ok(shared_secret)
307307
}
308308

309+
// ============================================================================
310+
// ML-KEM Public Key Import
311+
// ============================================================================
312+
313+
/// Import an ML-KEM public key from raw bytes.
314+
///
315+
/// Constructs a `SECKEYPublicKey` with `keyType = kyberKey` and the
316+
/// appropriate `KyberParams`, then uses `SECKEY_CopyPublicKey` to create
317+
/// a properly arena-backed copy, and `PK11_ImportPublicKey` to register
318+
/// it in PKCS#11.
319+
pub fn import_mlkem_public_key(
320+
raw_key: &[u8],
321+
params: MlKemParameterSet,
322+
) -> Res<PublicKey> {
323+
init()?;
324+
325+
let expected_size = params.public_key_bytes();
326+
if raw_key.len() != expected_size {
327+
return Err(Error::InvalidInput);
328+
}
329+
330+
let kyber_params = match params {
331+
MlKemParameterSet::MlKem768 => p11::KyberParams_params_ml_kem768,
332+
MlKemParameterSet::MlKem1024 => p11::KyberParams_params_ml_kem1024,
333+
};
334+
335+
unsafe {
336+
let mut temp_key: p11::SECKEYPublicKey = std::mem::zeroed();
337+
temp_key.keyType = p11::KeyType_kyberKey;
338+
temp_key.u.kyber.as_mut().params = kyber_params;
339+
temp_key.u.kyber.as_mut().publicValue = SECItem {
340+
type_: crate::SECItemType::siBuffer,
341+
data: raw_key.as_ptr() as *mut u8,
342+
len: raw_key.len() as u32,
343+
};
344+
345+
// SECKEY_CopyPublicKey allocates a new arena and deep-copies
346+
let copied: PublicKey = p11::SECKEY_CopyPublicKey(&temp_key).into_result()?;
347+
348+
// Register in PKCS#11
349+
let slot = Slot::internal()?;
350+
let handle = p11::PK11_ImportPublicKey(
351+
*slot,
352+
*copied,
353+
crate::PR_FALSE,
354+
);
355+
if handle == pkcs11_bindings::CK_INVALID_HANDLE {
356+
return Err(Error::InvalidInput);
357+
}
358+
359+
Ok(copied)
360+
}
361+
}
362+
309363
// ============================================================================
310364
// Public API
311365
// ============================================================================
@@ -616,6 +670,69 @@ mod tests {
616670
// Legacy API tests (for backwards compatibility)
617671
// ========================================================================
618672

673+
// ========================================================================
674+
// Import tests
675+
// ========================================================================
676+
677+
#[test]
678+
fn test_mlkem768_import_public_key_roundtrip() {
679+
use crate::err::secstatus_to_res;
680+
use crate::util::SECItemMut;
681+
682+
fixture_init();
683+
684+
// Generate a keypair
685+
let keypair = generate_keypair(KemParameterSet::MlKem768).unwrap();
686+
let KemKeypair::MlKem768 {
687+
ref public,
688+
ref private,
689+
} = keypair
690+
else {
691+
panic!("Expected MlKem768 keypair");
692+
};
693+
694+
// Export public key bytes via PK11_ReadRawAttribute(CKA_VALUE)
695+
let mut key_item = SECItemMut::make_empty();
696+
secstatus_to_res(unsafe {
697+
p11::PK11_ReadRawAttribute(
698+
p11::PK11ObjectType::PK11_TypePubKey,
699+
(**public).cast(),
700+
pkcs11_bindings::CKA_VALUE,
701+
key_item.as_mut(),
702+
)
703+
})
704+
.unwrap();
705+
let pk_bytes = key_item.as_slice().to_owned();
706+
assert_eq!(pk_bytes.len(), MlKemParameterSet::MlKem768.public_key_bytes());
707+
708+
// Import from raw bytes
709+
let imported_pk =
710+
import_mlkem_public_key(&pk_bytes, MlKemParameterSet::MlKem768).unwrap();
711+
712+
// Encapsulate with imported public key
713+
let target = p11::CKM_HKDF_DERIVE.into();
714+
let (ss_key, ciphertext) = mlkem_encapsulate(&imported_pk, target).unwrap();
715+
let shared_secret = ss_key.key_data().unwrap().to_vec();
716+
717+
// Decapsulate with original private key
718+
let dec_key = mlkem_decapsulate(private, &ciphertext, target).unwrap();
719+
let decap_ss = dec_key.key_data().unwrap().to_vec();
720+
721+
assert_eq!(shared_secret, decap_ss);
722+
}
723+
724+
#[test]
725+
fn test_mlkem_import_invalid_size() {
726+
fixture_init();
727+
728+
let result = import_mlkem_public_key(&[0u8; 100], MlKemParameterSet::MlKem768);
729+
assert!(result.is_err());
730+
}
731+
732+
// ========================================================================
733+
// Legacy API tests (for backwards compatibility)
734+
// ========================================================================
735+
619736
#[test]
620737
fn test_mlkem_parameter_set_sizes() {
621738
// ML-KEM-768 sizes

src/kem_combiners.rs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ fn xwing_combiner(
154154
/// RFC 8410 OID for X25519: 1.3.101.110
155155
const RFC8410_OID_X25519: &[u8] = &[0x2b, 0x65, 0x6e];
156156

157-
/// Import a raw X25519 public key.
158-
fn import_x25519_public_key(raw_key: &[u8; 32]) -> Res<PublicKey> {
157+
/// Import a raw X25519 public key from 32 bytes.
158+
pub fn import_x25519_public_key(raw_key: &[u8; 32]) -> Res<PublicKey> {
159159
// Build SPKI structure for X25519 per RFC 8410
160160
// SEQUENCE {
161161
// SEQUENCE {
@@ -194,6 +194,31 @@ fn import_x25519_public_key(raw_key: &[u8; 32]) -> Res<PublicKey> {
194194
import_ec_public_key_from_spki(&spki)
195195
}
196196

197+
/// Import an X-Wing (ML-KEM-768 + X25519) public key from raw bytes.
198+
///
199+
/// Expects 1216 bytes: 1184 bytes ML-KEM-768 || 32 bytes X25519.
200+
/// Returns a tuple of (mlkem_public, x25519_public) `PublicKey` objects
201+
/// suitable for passing to `xwing_encapsulate()`.
202+
pub fn import_xwing_public_key(
203+
raw_key: &[u8],
204+
) -> Res<(PublicKey, PublicKey)> {
205+
use crate::kem::{import_mlkem_public_key, MlKemParameterSet};
206+
207+
if raw_key.len() != XWING_MLKEM768_X25519_PUBLIC_KEY_SIZE {
208+
return Err(Error::InvalidInput);
209+
}
210+
211+
let mlkem_pk_bytes = &raw_key[..MLKEM768_PUBLIC_KEY_SIZE];
212+
let x25519_pk_bytes: &[u8; 32] = raw_key[MLKEM768_PUBLIC_KEY_SIZE..]
213+
.try_into()
214+
.map_err(|_| Error::InvalidInput)?;
215+
216+
let mlkem_pk = import_mlkem_public_key(mlkem_pk_bytes, MlKemParameterSet::MlKem768)?;
217+
let x25519_pk = import_x25519_public_key(x25519_pk_bytes)?;
218+
219+
Ok((mlkem_pk, x25519_pk))
220+
}
221+
197222
/// Encapsulate using an X-Wing public key.
198223
///
199224
/// This function generates a random shared secret and encapsulates it using
@@ -449,4 +474,62 @@ mod tests {
449474
);
450475
assert!(result.is_err());
451476
}
477+
478+
#[test]
479+
fn test_xwing_import_public_key_roundtrip() {
480+
use crate::err::secstatus_to_res;
481+
use crate::p11;
482+
use crate::util::SECItemMut;
483+
484+
fixture_init();
485+
486+
let keypair = XWingKeyPair::generate().unwrap();
487+
488+
// Export ML-KEM public key bytes via PK11_ReadRawAttribute(CKA_VALUE)
489+
let mut key_item = SECItemMut::make_empty();
490+
secstatus_to_res(unsafe {
491+
p11::PK11_ReadRawAttribute(
492+
p11::PK11ObjectType::PK11_TypePubKey,
493+
(*keypair.mlkem_public).cast(),
494+
pkcs11_bindings::CKA_VALUE,
495+
key_item.as_mut(),
496+
)
497+
})
498+
.unwrap();
499+
let mlkem_pk_bytes = key_item.as_slice().to_owned();
500+
501+
// Export X25519 public key bytes
502+
let x25519_raw = extract_x25519_public_key_bytes(&keypair.x25519_public).unwrap();
503+
504+
// Concatenate: ML-KEM-768 pk || X25519 pk
505+
let mut combined = Vec::with_capacity(XWING_MLKEM768_X25519_PUBLIC_KEY_SIZE);
506+
combined.extend_from_slice(&mlkem_pk_bytes);
507+
combined.extend_from_slice(&x25519_raw);
508+
assert_eq!(combined.len(), XWING_MLKEM768_X25519_PUBLIC_KEY_SIZE);
509+
510+
// Import
511+
let (imported_mlkem, imported_x25519) = import_xwing_public_key(&combined).unwrap();
512+
513+
// Encapsulate with imported keys
514+
let encap = xwing_encapsulate(&imported_mlkem, &imported_x25519).unwrap();
515+
516+
// Decapsulate with original private keys
517+
let ss = xwing_decapsulate(
518+
&encap.ciphertext,
519+
&keypair.mlkem_private,
520+
&keypair.x25519_private,
521+
&keypair.x25519_public,
522+
)
523+
.unwrap();
524+
525+
assert_eq!(encap.shared_secret, ss);
526+
}
527+
528+
#[test]
529+
fn test_xwing_import_invalid_size() {
530+
fixture_init();
531+
532+
let result = import_xwing_public_key(&[0u8; 100]);
533+
assert!(result.is_err());
534+
}
452535
}

src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,13 @@ pub use self::{
8787
ext::{ExtensionHandler, ExtensionHandlerResult, ExtensionWriterResult},
8888
kem::{
8989
decapsulate as kem_decapsulate, encapsulate as kem_encapsulate,
90-
generate_keypair as kem_generate_keypair, KemEncapResult, KemKeypair, KemParameterSet,
91-
MlKemKeypair, MlKemParameterSet,
90+
generate_keypair as kem_generate_keypair, import_mlkem_public_key, KemEncapResult,
91+
KemKeypair, KemParameterSet, MlKemKeypair, MlKemParameterSet,
9292
},
9393
kem_combiners::{
94-
xwing_decapsulate, xwing_encapsulate, XWingEncapResult, XWingKeyPair,
95-
XWING_MLKEM768_X25519_CIPHERTEXT_SIZE, XWING_MLKEM768_X25519_PUBLIC_KEY_SIZE, XWING_MLKEM768_X25519_SECRET_KEY_SIZE,
94+
import_x25519_public_key, import_xwing_public_key, xwing_decapsulate, xwing_encapsulate,
95+
XWingEncapResult, XWingKeyPair, XWING_MLKEM768_X25519_CIPHERTEXT_SIZE,
96+
XWING_MLKEM768_X25519_PUBLIC_KEY_SIZE, XWING_MLKEM768_X25519_SECRET_KEY_SIZE,
9697
XWING_MLKEM768_X25519_SHARED_SECRET_SIZE,
9798
},
9899
p11::{random, randomize, PrivateKey, PublicKey, SymKey},

0 commit comments

Comments
 (0)