From 4670b108a662d4554c5a919f5813959a97936b39 Mon Sep 17 00:00:00 2001 From: Timo Keller Date: Tue, 7 Jul 2026 16:37:42 +0200 Subject: [PATCH] pv: Implement HostKey::V2 and Keyslot::V2 Introduce HybridPublicKey combining ECDH and ML-KEM public keys. Use it for HostKey::V2 and add Keyslot::V2 for hybrid keyslots. Add tests for the new functionality. Assisted-by: IBM Bob:1.0.5 Signed-off-by: Timo Keller Reviewed-by: Marc Hartmayer Reviewed-by: Steffen Eiden Signed-off-by: Steffen Eiden --- rust/pv/src/req/hostkey.rs | 314 +++++++++++++++++++++++++++++++++++- rust/pv/src/req/keyslot.rs | 317 ++++++++++++++++++++++++++++++++++++- rust/pv/src/req/mod.rs | 5 +- 3 files changed, 628 insertions(+), 8 deletions(-) diff --git a/rust/pv/src/req/hostkey.rs b/rust/pv/src/req/hostkey.rs index d50cb3f3..2896c264 100644 --- a/rust/pv/src/req/hostkey.rs +++ b/rust/pv/src/req/hostkey.rs @@ -4,7 +4,56 @@ //! Host key types for UV requests -use openssl::pkey::{PKey, PKeyRef, Public}; +use openssl::nid::Nid; +use openssl::pkey::{KeyType, PKey, PKeyRef, Public}; + +use crate::crypto::{validate_ec_key, validate_key_type}; +pub use crate::error::Result; + +/// Hybrid public key (ECDH and ML-KEM) +#[derive(Clone, Debug)] +pub struct HybridPKey { + /// ECDH public key + pub(super) ec_key: PKey, + + /// ML-KEM public key + pub(super) mlkem_key: PKey, +} + +impl HybridPKey { + /// Creates a new hybrid public key with validation. + /// + /// # Parameters + /// - `ec_key`: ECDH public key (must be SECP521R1) + /// - `mlkem_key`: ML-KEM public key (must be ML-KEM-1024) + /// + /// # Errors + /// Returns an error if: + /// - EC key is not SECP521R1 curve + /// - ML-KEM key is not ML-KEM-1024 + pub fn new(ec_key: PKey, mlkem_key: PKey) -> Result { + validate_ec_key(&ec_key, "ECDH key", Nid::SECP521R1)?; + validate_key_type(&mlkem_key, "ML-KEM key", KeyType::ML_KEM_1024)?; + + Ok(Self { ec_key, mlkem_key }) + } + + /// Returns a reference to the EC key + pub fn ec_key(&self) -> &PKeyRef { + &self.ec_key + } + + /// Returns a reference to the ML-KEM key + pub fn mlkem_key(&self) -> &PKeyRef { + &self.mlkem_key + } +} + +impl AsRef for HybridPKey { + fn as_ref(&self) -> &HybridPKey { + self + } +} /// Versioned host keys container #[non_exhaustive] @@ -12,6 +61,9 @@ use openssl::pkey::{PKey, PKeyRef, Public}; pub enum HostKey { /// ECDH public key V1(PKey), + + /// Hybrid public key (ECDH and ML-KEM) + V2(HybridPKey), } impl HostKey { @@ -19,8 +71,23 @@ impl HostKey { pub fn ec_key(&self) -> Option<&PKeyRef> { Some(match self { HostKey::V1(ec_key) => ec_key, + HostKey::V2(hybrid) => hybrid.ec_key(), }) } + + /// Return the ML-KEM public key + pub fn mlkem_key(&self) -> Option<&PKeyRef> { + match self { + HostKey::V1(_) => None, + HostKey::V2(hybrid) => Some(hybrid.mlkem_key()), + } + } + + /// Test if the hostkey is hybrid + #[must_use] + pub fn is_hybrid(&self) -> bool { + matches!(self, HostKey::V2(_)) + } } impl AsRef for HostKey { @@ -28,3 +95,248 @@ impl AsRef for HostKey { self } } + +#[cfg(test)] +mod tests { + use openssl::ec::{EcGroup, EcKey}; + use openssl::pkey::Private; + + use super::*; + use crate::openssl_extensions::generate_ml_kem; + use crate::test_utils::get_test_key_and_cert_hybrid; + use crate::Error; + + fn to_public_key(key: &PKey) -> Result> { + let der = key.public_key_to_der()?; + Ok(PKey::public_key_from_der(&der)?) + } + + #[test] + fn test_hostkey_v1_variant() { + let (_, ec_key) = crate::test_utils::get_test_key_and_cert(); + let hostkey = HostKey::V1(ec_key.public_key().unwrap()); + + assert!(!hostkey.is_hybrid(), "V1 HostKey should not be hybrid"); + assert!(matches!(hostkey, HostKey::V1(_))); + } + + #[test] + fn test_hostkey_v2_variant() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + let hostkey = HostKey::V2(hybrid); + + assert!(hostkey.is_hybrid(), "V2 HostKey should be hybrid"); + assert!(matches!(hostkey, HostKey::V2(_))); + } + + #[test] + fn test_hostkey_ec_key_access() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let v1_key = HostKey::V1(ec_key.public_key().unwrap()); + let v2_key = HostKey::V2(hybrid); + + assert!(v1_key.ec_key().unwrap().public_key_to_der().is_ok()); + assert!(v2_key.ec_key().unwrap().public_key_to_der().is_ok()); + } + + #[test] + fn test_hostkey_v2_mlkem_key_access() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + let hostkey = HostKey::V2(hybrid); + + assert!(hostkey.mlkem_key().unwrap().public_key_to_der().is_ok()); + } + + #[test] + fn test_hostkey_v1_has_no_mlkem_key() { + let (_, ec_key) = crate::test_utils::get_test_key_and_cert(); + let hostkey = HostKey::V1(ec_key.public_key().unwrap()); + + assert!(hostkey.mlkem_key().is_none()); + } + + #[test] + fn test_hybrid_public_key_structure() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + // Verify both components are present and valid + assert!(!hybrid.ec_key().public_key_to_der().unwrap().is_empty()); + assert!(!hybrid.mlkem_key().public_key_to_der().unwrap().is_empty()); + } + + #[test] + fn test_hybrid_pkey_invalid_ec_curve() { + // Generate a P-256 key instead of P-521 + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let wrong_ec_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap(); + + let (_, _, mlkem_key) = get_test_key_and_cert_hybrid(); + + let result = HybridPKey::new( + to_public_key(&wrong_ec_key).unwrap(), + mlkem_key.public_key().unwrap(), + ); + assert!(result.is_err(), "Should reject EC key with wrong curve"); + + if let Err(Error::RetrInvKey { + what, + kind, + value, + exp, + }) = result + { + assert_eq!(what, "curve"); + assert_eq!(kind, "ECDH key"); + assert_eq!(value, "prime256v1"); + assert_eq!(exp, "secp521r1"); + } else { + panic!("Expected RetrInvKey error for wrong curve"); + } + } + + #[test] + fn test_hybrid_pkey_invalid_mlkem_type() { + let (_, ec_key, _) = get_test_key_and_cert_hybrid(); + + // Generate ML-KEM-512 instead of ML-KEM-1024 + let wrong_mlkem_key = generate_ml_kem(KeyType::ML_KEM_512).unwrap(); + + let result = HybridPKey::new( + ec_key.public_key().unwrap(), + to_public_key(&wrong_mlkem_key).unwrap(), + ); + assert!(result.is_err(), "Should reject ML-KEM key with wrong type"); + + if let Err(Error::RetrInvKey { + what, + kind, + value, + exp, + }) = result + { + assert_eq!(what, "key type"); + assert_eq!(kind, "ML-KEM key"); + assert_eq!(value, "ML-KEM-512"); + assert_eq!(exp, "ML-KEM-1024"); + } else { + panic!("Expected RetrInvKey error for wrong ML-KEM type"); + } + } + + #[test] + fn test_hybrid_pkey_clone() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let cloned = hybrid.clone(); + + // Verify both original and clone have valid keys + assert!(!hybrid.ec_key().public_key_to_der().unwrap().is_empty()); + assert!(!hybrid.mlkem_key().public_key_to_der().unwrap().is_empty()); + assert!(!cloned.ec_key().public_key_to_der().unwrap().is_empty()); + assert!(!cloned.mlkem_key().public_key_to_der().unwrap().is_empty()); + + // Verify the keys are equivalent + assert_eq!( + hybrid.ec_key().public_key_to_der().unwrap(), + cloned.ec_key().public_key_to_der().unwrap() + ); + assert_eq!( + hybrid.mlkem_key().public_key_to_der().unwrap(), + cloned.mlkem_key().public_key_to_der().unwrap() + ); + } + + #[test] + fn test_hostkey_clone() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + // Test cloning V1 + let v1_key = HostKey::V1(ec_key.public_key().unwrap()); + let v1_cloned = v1_key.clone(); + assert!(!v1_cloned.is_hybrid()); + assert_eq!( + v1_key.ec_key().unwrap().public_key_to_der().unwrap(), + v1_cloned.ec_key().unwrap().public_key_to_der().unwrap() + ); + + // Test cloning V2 + let v2_key = HostKey::V2(hybrid); + let v2_cloned = v2_key.clone(); + assert!(v2_cloned.is_hybrid()); + assert_eq!( + v2_key.ec_key().unwrap().public_key_to_der().unwrap(), + v2_cloned.ec_key().unwrap().public_key_to_der().unwrap() + ); + assert_eq!( + v2_key.mlkem_key().unwrap().public_key_to_der().unwrap(), + v2_cloned.mlkem_key().unwrap().public_key_to_der().unwrap() + ); + } + + #[test] + fn test_hybrid_pkey_as_ref() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let hybrid_ref: &HybridPKey = hybrid.as_ref(); + assert!(!hybrid_ref.ec_key().public_key_to_der().unwrap().is_empty()); + assert!(!hybrid_ref + .mlkem_key() + .public_key_to_der() + .unwrap() + .is_empty()); + } + + #[test] + fn test_hostkey_as_ref() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let v1_key = HostKey::V1(ec_key.public_key().unwrap()); + let v1_ref: &HostKey = v1_key.as_ref(); + assert!(!v1_ref.is_hybrid()); + + let v2_key = HostKey::V2(hybrid); + let v2_ref: &HostKey = v2_key.as_ref(); + assert!(v2_ref.is_hybrid()); + } +} diff --git a/rust/pv/src/req/keyslot.rs b/rust/pv/src/req/keyslot.rs index c4a52ee2..0d925f00 100644 --- a/rust/pv/src/req/keyslot.rs +++ b/rust/pv/src/req/keyslot.rs @@ -7,11 +7,8 @@ use openssl::hash::MessageDigest; use openssl::pkey::{PKey, PKeyRef, Private, Public}; -use super::ec_coord::EcPubKeyCoord; -use super::encrypt::Encrypt; -#[expect(unused)] use crate::crypto::{derive_aes256_gcm_key, derive_aes256_gcm_key_hybrid, encrypt_aead, hash}; -use crate::request::HostKey; +use crate::req::{EcPubKeyCoord, Encrypt, HostKey, HybridPKey}; use crate::Result; /// IBM Z Host key-slot @@ -72,12 +69,100 @@ impl Encrypt for KeyslotV1 { } } +/// IBM Z hybrid (V2) Host key-slot +/// +/// Layout in binary format: +/// ```none +/// _______________________________________________________________ +/// | Public Host Key Hash (64) | +/// | Wrapped(=Encrypted) Request Protection Key(32) | +/// | Key Slot Tag (16) | +/// | ML-KEM Ciphertext (1568) | +/// |_____________________________________________________________| +/// ``` +#[derive(Debug, Clone)] +pub struct KeyslotV2 { + ec_hostkey: PKey, + mlkem_hostkey: PKey, +} + +impl KeyslotV2 { + /// Size of a hybrid host-key hash + pub const PHKH_SIZE: u32 = 0x40; + /// Size of a complete V2 keyslot in bytes + pub const SIZE: usize = 1680; + + /// Creates a new HybridKeyslot from the provided hybrid public key + pub fn new(hostkey: HybridPKey) -> Self { + let HybridPKey { ec_key, mlkem_key } = hostkey; + Self { + ec_hostkey: ec_key, + mlkem_hostkey: mlkem_key, + } + } + + /// Encrypts `secret` using `self` and `priv_key` the encryption. + /// + /// # Returns + /// the encrypted data. + /// + /// # Errors + /// + /// This function will return an error if OpenSSL could not encrypt the secret. + pub fn encrypt(&self, secret: &[u8], priv_key: &PKeyRef) -> Result> { + let mut res = Vec::with_capacity(1680); + self.encrypt_to(secret, priv_key, &mut res)?; + Ok(res) + } + + /// Encrypts the given request protection key `prot_key`. + /// + /// The AES256 encryption key is derived from `self` as public key, and `priv_key` as private + /// key. + /// + /// # Returns + /// The encrypted HybridKeyslot. + /// + /// # Errors + /// + /// This function will return an error if OpenSSL could not encrypt the secret. + fn encrypt_to( + &self, + prot_key: &[u8], + priv_key: &PKeyRef, + to: &mut Vec, + ) -> Result<()> { + let mut phk_buf = Vec::::with_capacity(160 + 1568); + let ec_phk: EcPubKeyCoord = self.ec_hostkey.as_ref().try_into()?; + phk_buf.extend_from_slice(ec_phk.as_ref()); + phk_buf.extend_from_slice(&self.mlkem_hostkey.raw_public_key()?); + assert_eq!(phk_buf.len(), 160 + 1568); + + let (derived_key, ciphertext) = + derive_aes256_gcm_key_hybrid(priv_key, &self.ec_hostkey, &self.mlkem_hostkey)?; + let mut wrpk_and_kst = + encrypt_aead(&derived_key.into(), &[0; 12], &[], prot_key)?.into_buf(); + assert_eq!(wrpk_and_kst.len(), 48); + + to.reserve(1680); + let hash = hash(MessageDigest::sha512(), &phk_buf)?; + assert_eq!(hash.len(), 64); + to.extend_from_slice(&hash); + to.append(&mut wrpk_and_kst); + assert_eq!(ciphertext.len(), 1568); + to.extend_from_slice(&ciphertext); + Ok(()) + } +} + /// Versioned keyslot container #[non_exhaustive] #[derive(Debug, Clone)] pub enum Keyslot { /// V1 key-slots with ECDH keys V1(KeyslotV1), + /// V2 key-slots with hybrid ECDH/ML-KEM keys + V2(KeyslotV2), } impl Keyslot { @@ -85,6 +170,7 @@ impl Keyslot { pub fn new(hostkey: HostKey) -> Self { match hostkey { HostKey::V1(key) => Keyslot::V1(KeyslotV1::new(key)), + HostKey::V2(key) => Keyslot::V2(KeyslotV2::new(key)), } } @@ -92,6 +178,7 @@ impl Keyslot { pub fn phkh_size(&self) -> u32 { match self { Keyslot::V1(_) => KeyslotV1::PHKH_SIZE, + Keyslot::V2(_) => KeyslotV2::PHKH_SIZE, } } @@ -99,13 +186,16 @@ impl Keyslot { pub fn size(&self) -> usize { match self { Keyslot::V1(_) => KeyslotV1::SIZE, + Keyslot::V2(_) => KeyslotV2::SIZE, } } /// Return whether the key-slot uses hybrid keys + #[must_use] pub fn is_hybrid(&self) -> bool { match self { Keyslot::V1(_) => false, + Keyslot::V2(_) => true, } } } @@ -119,6 +209,7 @@ impl Encrypt for Keyslot { ) -> Result<()> { match self { Keyslot::V1(ks) => ks.encrypt_to(secret, priv_key, to), + Keyslot::V2(ks) => ks.encrypt_to(secret, priv_key, to), } } } @@ -129,11 +220,17 @@ impl From> for Keyslot { } } +impl From for Keyslot { + fn from(key: HybridPKey) -> Self { + Keyslot::V2(KeyslotV2::new(key)) + } +} + #[cfg(test)] mod tests { use super::*; use crate::get_test_asset; - use crate::test_utils::*; + use crate::test_utils::{DeterministicTestRandGuard, *}; #[test] fn keyslot() { @@ -148,4 +245,214 @@ mod tests { let encr_ks = keyslot.encrypt(&[0x16u8; 32], &cust_key).unwrap(); assert_ne!(exp_keyslot, encr_ks); } + + #[test] + fn keyslot_v2() { + // Install deterministic RNG for reproducible encryption + let _guard = DeterministicTestRandGuard::install(&[0x42; 64], &[0x17; 16]).unwrap(); + + let (cust_key, host_key1, host_key2) = get_test_keys_hybrid(); + let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap()); + let exp_keyslot = vec![ + 255, 94, 191, 53, 220, 196, 47, 37, 93, 227, 234, 101, 1, 174, 171, 68, 42, 136, 92, + 238, 72, 6, 17, 77, 231, 225, 174, 22, 222, 188, 212, 15, 248, 145, 72, 126, 139, 17, + 233, 225, 156, 46, 233, 151, 54, 2, 175, 88, 215, 254, 243, 222, 37, 81, 50, 110, 18, + 76, 252, 12, 210, 146, 66, 23, 123, 246, 141, 14, 70, 157, 73, 124, 205, 112, 192, 82, + 160, 243, 2, 154, 134, 145, 176, 147, 30, 217, 221, 93, 170, 239, 165, 37, 30, 192, 47, + 94, 129, 165, 12, 137, 239, 12, 180, 199, 240, 160, 180, 205, 117, 84, 16, 148, 51, 48, + 243, 216, 50, 132, 187, 115, 137, 3, 109, 145, 172, 159, 224, 25, 63, 96, 241, 105, 89, + 35, 42, 180, 248, 35, 166, 81, 116, 175, 179, 252, 81, 188, 189, 42, 17, 60, 231, 132, + 32, 111, 90, 138, 201, 16, 3, 97, 246, 130, 173, 85, 218, 187, 232, 92, 52, 120, 56, + 142, 137, 101, 31, 21, 194, 186, 198, 38, 240, 58, 150, 93, 86, 18, 186, 5, 12, 41, 37, + 94, 162, 119, 16, 237, 82, 252, 178, 159, 57, 58, 162, 20, 173, 140, 10, 172, 203, 241, + 94, 110, 117, 32, 36, 217, 7, 168, 50, 105, 43, 33, 174, 140, 55, 88, 26, 233, 81, 253, + 98, 121, 190, 70, 146, 84, 186, 6, 51, 181, 2, 59, 216, 45, 78, 4, 82, 90, 21, 180, 99, + 239, 195, 254, 33, 171, 124, 97, 36, 110, 119, 142, 98, 125, 70, 236, 51, 192, 32, 8, + 214, 224, 101, 169, 45, 173, 137, 194, 78, 114, 224, 76, 174, 188, 133, 227, 167, 169, + 47, 241, 190, 15, 145, 217, 87, 254, 15, 150, 52, 138, 192, 230, 52, 129, 36, 8, 40, + 37, 126, 103, 85, 86, 25, 78, 53, 243, 107, 193, 18, 102, 3, 83, 212, 5, 252, 224, 241, + 7, 205, 220, 168, 183, 15, 37, 52, 190, 12, 9, 152, 124, 210, 65, 152, 210, 199, 81, + 95, 240, 141, 196, 236, 8, 249, 221, 116, 130, 42, 52, 208, 218, 180, 27, 249, 59, 245, + 21, 165, 142, 234, 140, 75, 89, 136, 250, 155, 157, 12, 154, 152, 188, 178, 227, 237, + 124, 111, 176, 131, 227, 238, 164, 22, 88, 80, 217, 42, 217, 80, 148, 118, 12, 136, 95, + 53, 69, 209, 131, 74, 91, 193, 253, 229, 146, 11, 58, 125, 19, 8, 229, 60, 177, 244, + 74, 41, 92, 91, 62, 64, 221, 20, 213, 227, 4, 40, 22, 177, 155, 236, 147, 165, 27, 4, + 99, 4, 151, 155, 82, 72, 151, 22, 102, 183, 192, 242, 18, 104, 146, 205, 107, 21, 254, + 88, 23, 69, 246, 57, 217, 249, 124, 246, 54, 163, 244, 38, 74, 215, 144, 50, 45, 142, + 36, 216, 88, 39, 70, 67, 88, 130, 1, 8, 205, 240, 159, 210, 205, 233, 237, 60, 81, 176, + 112, 172, 187, 121, 239, 198, 43, 17, 49, 55, 170, 228, 243, 255, 76, 72, 121, 125, + 157, 250, 93, 251, 55, 25, 4, 129, 67, 195, 30, 37, 6, 76, 10, 240, 178, 255, 151, 138, + 36, 255, 32, 237, 133, 162, 130, 91, 9, 238, 67, 134, 4, 86, 225, 179, 166, 219, 4, + 144, 7, 125, 248, 75, 132, 54, 56, 51, 237, 206, 252, 96, 9, 208, 140, 127, 143, 180, + 32, 179, 254, 18, 233, 187, 122, 159, 172, 77, 204, 9, 179, 242, 153, 18, 138, 82, 13, + 210, 140, 207, 9, 217, 216, 241, 106, 205, 109, 195, 223, 82, 172, 181, 48, 91, 124, + 235, 85, 17, 18, 166, 216, 86, 120, 185, 54, 147, 156, 84, 106, 30, 235, 234, 114, 27, + 23, 121, 210, 251, 136, 117, 53, 177, 28, 153, 144, 17, 70, 190, 206, 27, 135, 42, 227, + 39, 243, 86, 170, 11, 63, 191, 102, 89, 92, 137, 103, 59, 142, 236, 203, 156, 231, 255, + 30, 5, 86, 53, 59, 76, 181, 218, 184, 244, 21, 50, 100, 72, 81, 98, 193, 34, 58, 67, + 150, 15, 17, 162, 216, 119, 213, 203, 50, 27, 158, 61, 105, 134, 151, 200, 68, 103, 74, + 207, 12, 5, 163, 30, 198, 28, 41, 41, 97, 93, 251, 213, 133, 41, 225, 178, 67, 160, 85, + 132, 146, 1, 201, 99, 49, 185, 27, 150, 213, 165, 134, 45, 248, 204, 67, 145, 49, 81, + 35, 246, 25, 215, 209, 159, 106, 212, 14, 149, 193, 163, 90, 24, 83, 230, 178, 216, + 194, 130, 118, 169, 81, 49, 49, 145, 96, 206, 216, 15, 134, 43, 130, 25, 110, 84, 39, + 175, 223, 183, 209, 123, 5, 166, 244, 19, 89, 2, 47, 226, 3, 93, 156, 163, 67, 156, + 237, 17, 59, 69, 99, 94, 32, 180, 64, 64, 115, 75, 44, 241, 150, 43, 169, 17, 36, 125, + 216, 132, 92, 3, 244, 16, 83, 179, 192, 65, 133, 208, 19, 156, 17, 60, 54, 29, 253, + 237, 18, 158, 145, 142, 232, 147, 2, 91, 21, 145, 125, 204, 243, 161, 245, 110, 140, + 219, 206, 70, 235, 211, 167, 138, 104, 132, 248, 157, 65, 153, 216, 47, 125, 205, 237, + 137, 220, 25, 228, 146, 194, 10, 169, 201, 224, 88, 119, 38, 140, 120, 125, 140, 46, + 184, 221, 30, 10, 47, 34, 140, 173, 64, 38, 48, 77, 236, 206, 163, 111, 80, 46, 40, + 232, 63, 247, 222, 25, 20, 246, 143, 9, 107, 172, 180, 84, 188, 234, 102, 87, 181, 173, + 83, 14, 163, 170, 91, 29, 209, 93, 52, 158, 213, 6, 91, 71, 54, 244, 189, 198, 60, 21, + 131, 210, 35, 18, 36, 164, 188, 87, 54, 73, 208, 115, 11, 248, 57, 107, 93, 23, 49, + 129, 221, 61, 12, 172, 31, 199, 129, 196, 5, 184, 78, 226, 210, 83, 232, 153, 64, 17, + 119, 243, 45, 73, 50, 129, 35, 94, 243, 146, 86, 136, 202, 86, 87, 97, 193, 59, 160, + 181, 95, 150, 148, 117, 96, 31, 151, 97, 159, 53, 72, 171, 239, 210, 67, 207, 201, 109, + 160, 230, 235, 176, 35, 235, 98, 128, 166, 195, 144, 200, 156, 7, 72, 242, 15, 95, 99, + 69, 41, 219, 254, 244, 80, 158, 177, 64, 83, 11, 235, 98, 201, 130, 176, 119, 22, 214, + 135, 215, 37, 65, 108, 91, 34, 94, 0, 153, 161, 87, 144, 177, 12, 122, 205, 11, 72, + 233, 213, 63, 37, 12, 255, 235, 188, 194, 189, 65, 169, 185, 207, 131, 182, 243, 233, + 30, 245, 39, 42, 188, 157, 28, 121, 231, 194, 121, 250, 121, 11, 28, 252, 98, 151, 238, + 124, 56, 138, 106, 116, 187, 203, 21, 75, 239, 132, 19, 62, 43, 42, 12, 214, 87, 225, + 217, 169, 134, 183, 193, 241, 175, 140, 36, 147, 248, 86, 6, 58, 212, 72, 112, 0, 56, + 168, 128, 68, 253, 173, 225, 152, 167, 138, 254, 97, 123, 227, 163, 135, 198, 49, 49, + 138, 249, 234, 245, 78, 150, 140, 170, 199, 41, 206, 246, 19, 117, 241, 27, 112, 102, + 4, 94, 237, 30, 0, 68, 252, 163, 205, 10, 63, 146, 147, 37, 153, 197, 52, 162, 50, 250, + 219, 130, 197, 3, 60, 246, 133, 83, 140, 103, 227, 50, 212, 121, 165, 114, 139, 225, + 195, 145, 107, 249, 127, 194, 23, 112, 141, 242, 14, 218, 42, 131, 19, 245, 143, 73, + 79, 194, 135, 224, 171, 249, 169, 129, 160, 153, 75, 66, 26, 12, 254, 180, 212, 229, + 172, 134, 159, 122, 212, 219, 21, 29, 33, 11, 176, 149, 73, 169, 26, 150, 96, 133, 90, + 217, 18, 37, 244, 48, 249, 4, 180, 129, 9, 45, 219, 106, 215, 28, 81, 118, 48, 98, 109, + 167, 72, 107, 187, 78, 127, 251, 184, 170, 74, 57, 188, 91, 196, 229, 251, 70, 163, 68, + 227, 238, 71, 115, 155, 246, 146, 97, 208, 21, 245, 62, 127, 47, 79, 131, 217, 41, 153, + 52, 237, 159, 60, 98, 18, 169, 149, 6, 84, 135, 2, 45, 170, 165, 213, 201, 127, 23, + 209, 2, 158, 235, 240, 195, 255, 76, 54, 189, 113, 50, 105, 230, 191, 217, 29, 46, 181, + 80, 197, 60, 177, 243, 61, 52, 24, 140, 134, 147, 176, 198, 22, 92, 156, 217, 189, 134, + 17, 122, 53, 49, 14, 87, 128, 99, 207, 123, 113, 169, 195, 206, 127, 211, 21, 216, 166, + 18, 137, 110, 148, 70, 26, 54, 52, 113, 189, 45, 89, 254, 218, 247, 193, 71, 19, 153, + 35, 179, 49, 237, 199, 176, 251, 143, 95, 115, 195, 3, 122, 161, 243, 220, 39, 102, + 147, 134, 25, 172, 164, 167, 110, 123, 221, 177, 28, 236, 100, 165, 186, 179, 45, 189, + 183, 76, 171, 127, 209, 108, 220, 83, 207, 136, 98, 54, 76, 37, 188, 57, 157, 204, 109, + 150, 181, 110, 57, 5, 26, 168, 34, 11, 117, 3, 184, 147, 155, 122, 244, 251, 215, 1, + 211, 226, 185, 214, 120, 206, 212, 75, 203, 174, 140, 20, 93, 93, 207, 28, 15, 122, 9, + 83, 98, 107, 51, 202, 151, 220, 42, 95, 17, 141, 141, 201, 149, 253, 55, 169, 170, 237, + 166, 92, 92, 20, 89, 124, 167, 102, 161, 87, 97, 88, 20, 245, 175, 32, 111, 0, 2, 192, + 25, 63, 182, 10, 226, 165, 162, 223, 35, 243, 198, 189, 167, 137, 134, 207, 84, 240, 8, + 95, 137, 31, 159, 58, 211, 161, 94, 150, 54, 188, 145, 253, 206, 156, 130, 121, 192, + 221, 73, 249, 92, 91, 184, 211, 131, 206, 205, 183, 159, 195, 170, 47, 13, 131, 6, 132, + 103, 121, 40, 252, 250, 251, 85, 253, 68, 66, 211, 24, 104, 48, 150, 176, 62, 201, 161, + 93, 204, 120, 196, 159, 208, 199, 96, 228, 239, 29, 239, 128, 156, 14, 131, 25, 27, + 157, 249, 253, 250, 193, 148, 133, 59, 138, 202, 239, + ]; + + let keyslot = Keyslot::new(host_key); + let encr_keyslot = keyslot.encrypt(&[0x17u8; 32], &cust_key).unwrap(); + + assert_eq!(encr_keyslot, exp_keyslot); + } + + #[test] + fn test_keyslot_v2_constants() { + // Test V2 keyslot constants + assert_eq!( + KeyslotV2::SIZE, + 1680, + "V2 keyslot size should be 1680 bytes" + ); + assert_eq!( + KeyslotV2::PHKH_SIZE, + 0x40, + "V2 PHKH size should be 64 bytes (SHA-512)" + ); + } + + #[test] + fn test_keyslot_v2_creation() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let keyslot = KeyslotV2::new(hybrid); + + // Verify keyslot was created successfully + assert!(keyslot.ec_hostkey.public_key_to_der().is_ok()); + assert!(keyslot.mlkem_hostkey.public_key_to_der().is_ok()); + } + + #[test] + fn test_keyslot_enum_v1_variant() { + let (_, host_key) = get_test_keys(); + let keyslot = Keyslot::V1(KeyslotV1(host_key)); + + assert_eq!(keyslot.phkh_size(), KeyslotV1::PHKH_SIZE); + assert_eq!(keyslot.size(), KeyslotV1::SIZE); + assert!(matches!(keyslot, Keyslot::V1(_))); + } + + #[test] + fn test_keyslot_enum_v2_variant() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + let keyslot = Keyslot::V2(KeyslotV2::new(hybrid)); + + assert_eq!(keyslot.phkh_size(), KeyslotV2::PHKH_SIZE); + assert_eq!(keyslot.size(), KeyslotV2::SIZE); + assert!(matches!(keyslot, Keyslot::V2(_))); + } + + #[test] + fn test_keyslot_from_hostkey_v1() { + let (_, ec_key) = get_test_keys(); + let hostkey = HostKey::V1(ec_key); + let keyslot = Keyslot::new(hostkey); + + assert!(matches!(keyslot, Keyslot::V1(_))); + assert_eq!(keyslot.size(), KeyslotV1::SIZE); + } + + #[test] + fn test_keyslot_from_hostkey_v2() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + let hostkey = HostKey::V2(hybrid); + let keyslot = Keyslot::new(hostkey); + + assert!(matches!(keyslot, Keyslot::V2(_))); + assert_eq!(keyslot.size(), KeyslotV2::SIZE); + } + + #[test] + fn test_keyslot_from_hybrid_public_key() { + let (_, ec_key, mlkem_key) = get_test_key_and_cert_hybrid(); + let hybrid = HybridPKey::new( + ec_key.public_key().unwrap(), + mlkem_key.public_key().unwrap(), + ) + .unwrap(); + + let keyslot: Keyslot = hybrid.into(); + assert!(matches!(keyslot, Keyslot::V2(_))); + } + + #[test] + fn test_keyslot_sizes() { + // Document the sizes for V1 and V2 + assert_eq!(KeyslotV1::SIZE, 80, "V1 keyslot size"); + assert_eq!( + KeyslotV2::SIZE, + 1680, + "V2 keyslot size (includes ML-KEM ciphertext)" + ); + } } diff --git a/rust/pv/src/req/mod.rs b/rust/pv/src/req/mod.rs index acc632e7..3869dd15 100644 --- a/rust/pv/src/req/mod.rs +++ b/rust/pv/src/req/mod.rs @@ -21,6 +21,7 @@ pub(crate) use ec_coord::get_pub_ecdh_points; pub use ec_coord::EcPubKeyCoord; pub use encrypt::{Aad, Encrypt}; pub use header::RequestHdr; -pub use hostkey::HostKey; -pub use keyslot::{Keyslot, KeyslotV1}; +pub use hostkey::{HostKey, HybridPKey}; +#[expect(unused)] +pub use keyslot::{Keyslot, KeyslotV1, KeyslotV2}; pub use request::{BinReqValues, Request};