use p256::ecdh::diffie_hellman as p256_dh;
use p256::PublicKey as P256PublicKey;
use p256::SecretKey as P256SecretKey;
use p384::ecdh::diffie_hellman as p384_dh;
use p384::PublicKey as P384PublicKey;
use p384::SecretKey as P384SecretKey;
use zeroize::Zeroizing;
use crate::config::config::AlgorithmConfig;
use crate::crypto::backend::CryptoBackend;
use crate::error::{HsmError, HsmResult};
use crate::pkcs11_abi::constants::*;
use crate::pkcs11_abi::types::{CK_ATTRIBUTE_TYPE, CK_EC_KDF_TYPE, CK_KEY_TYPE};
use crate::store::attributes::read_ck_ulong;
use crate::store::key_material::RawKeyMaterial;
const MAX_OKM_LEN: usize = 64;
pub fn ecdh_p256(
private_key_bytes: &[u8],
peer_public_key_sec1: &[u8],
okm_len: Option<usize>,
) -> HsmResult<RawKeyMaterial> {
let derived_len = okm_len.unwrap_or(32);
validate_okm_len(derived_len)?;
let secret_key =
P256SecretKey::from_slice(private_key_bytes).map_err(|_| HsmError::KeyHandleInvalid)?;
let peer_public =
P256PublicKey::from_sec1_bytes(peer_public_key_sec1).map_err(|_| HsmError::ArgumentsBad)?;
let shared_secret = p256_dh(secret_key.to_nonzero_scalar(), peer_public.as_affine());
let raw_bytes = Zeroizing::new(shared_secret.raw_secret_bytes().to_vec());
let our_public = secret_key.public_key();
let our_pk_bytes = elliptic_curve::sec1::ToEncodedPoint::to_encoded_point(&our_public, false);
const P256_OID: &[u8] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
let mut info =
Vec::with_capacity(P256_OID.len() + 4 + our_pk_bytes.len() + peer_public_key_sec1.len());
info.extend_from_slice(P256_OID);
info.extend_from_slice(&(derived_len as u32).to_be_bytes());
info.extend_from_slice(our_pk_bytes.as_bytes());
info.extend_from_slice(peer_public_key_sec1);
let okm = apply_hkdf(&raw_bytes, &info, derived_len)?;
Ok(RawKeyMaterial::new(okm))
}
pub fn ecdh_p384(
private_key_bytes: &[u8],
peer_public_key_sec1: &[u8],
okm_len: Option<usize>,
) -> HsmResult<RawKeyMaterial> {
let derived_len = okm_len.unwrap_or(48);
validate_okm_len(derived_len)?;
let secret_key =
P384SecretKey::from_slice(private_key_bytes).map_err(|_| HsmError::KeyHandleInvalid)?;
let peer_public =
P384PublicKey::from_sec1_bytes(peer_public_key_sec1).map_err(|_| HsmError::ArgumentsBad)?;
let shared_secret = p384_dh(secret_key.to_nonzero_scalar(), peer_public.as_affine());
let raw_bytes = Zeroizing::new(shared_secret.raw_secret_bytes().to_vec());
let our_public = secret_key.public_key();
let our_pk_bytes = elliptic_curve::sec1::ToEncodedPoint::to_encoded_point(&our_public, false);
const P384_OID: &[u8] = &[0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22];
let mut info =
Vec::with_capacity(P384_OID.len() + 4 + our_pk_bytes.len() + peer_public_key_sec1.len());
info.extend_from_slice(P384_OID);
info.extend_from_slice(&(derived_len as u32).to_be_bytes());
info.extend_from_slice(our_pk_bytes.as_bytes());
info.extend_from_slice(peer_public_key_sec1);
let okm = apply_hkdf(&raw_bytes, &info, derived_len)?;
Ok(RawKeyMaterial::new(okm))
}
pub fn ecdh_p256_shared_secret(
private_key_bytes: &[u8],
peer_public_key_sec1: &[u8],
) -> HsmResult<RawKeyMaterial> {
let secret_key =
P256SecretKey::from_slice(private_key_bytes).map_err(|_| HsmError::KeyHandleInvalid)?;
let peer_public =
P256PublicKey::from_sec1_bytes(peer_public_key_sec1).map_err(|_| HsmError::ArgumentsBad)?;
let shared_secret = p256_dh(secret_key.to_nonzero_scalar(), peer_public.as_affine());
Ok(RawKeyMaterial::new(
shared_secret.raw_secret_bytes().to_vec(),
))
}
pub fn ecdh_p384_shared_secret(
private_key_bytes: &[u8],
peer_public_key_sec1: &[u8],
) -> HsmResult<RawKeyMaterial> {
let secret_key =
P384SecretKey::from_slice(private_key_bytes).map_err(|_| HsmError::KeyHandleInvalid)?;
let peer_public =
P384PublicKey::from_sec1_bytes(peer_public_key_sec1).map_err(|_| HsmError::ArgumentsBad)?;
let shared_secret = p384_dh(secret_key.to_nonzero_scalar(), peer_public.as_affine());
Ok(RawKeyMaterial::new(
shared_secret.raw_secret_bytes().to_vec(),
))
}
pub fn apply_ec_kdf(
backend: &dyn CryptoBackend,
kdf: CK_EC_KDF_TYPE,
z: &RawKeyMaterial,
shared_data: &[u8],
key_len: usize,
fips_mode: bool,
) -> HsmResult<RawKeyMaterial> {
if key_len == 0 {
return Err(HsmError::KeySizeRange);
}
let hash_mech = match kdf {
CKD_NULL => {
if fips_mode {
tracing::error!(
"ECDH with CKD_NULL is not permitted in FIPS-approved mode; use a SHA-2 KDF"
);
return Err(HsmError::MechanismParamInvalid);
}
if !shared_data.is_empty() {
return Err(HsmError::MechanismParamInvalid);
}
let z = z.as_bytes();
if key_len > z.len() {
return Err(HsmError::KeySizeRange);
}
return Ok(RawKeyMaterial::new(z[z.len() - key_len..].to_vec()));
}
CKD_SHA256_KDF => CKM_SHA256,
CKD_SHA384_KDF => CKM_SHA384,
CKD_SHA512_KDF => CKM_SHA512,
_ => return Err(HsmError::MechanismParamInvalid),
};
let mut okm = Zeroizing::new(Vec::with_capacity(key_len + 64));
let mut counter: u32 = 1;
while okm.len() < key_len {
let mut input = Zeroizing::new(Vec::with_capacity(z.len() + 4 + shared_data.len()));
input.extend_from_slice(z.as_bytes());
input.extend_from_slice(&counter.to_be_bytes());
input.extend_from_slice(shared_data);
let block = Zeroizing::new(backend.compute_digest(hash_mech, &input)?);
okm.extend_from_slice(&block);
counter += 1;
}
okm.truncate(key_len);
Ok(RawKeyMaterial::new(std::mem::take(&mut *okm)))
}
pub const MAX_DERIVED_GENERIC_SECRET_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DerivedKeyTemplate {
pub key_type: CK_KEY_TYPE,
pub value_len: Option<usize>,
pub sensitive: bool,
pub extractable: bool,
}
impl DerivedKeyTemplate {
pub fn resolve(
template: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)],
default_key_type: CK_KEY_TYPE,
config: &AlgorithmConfig,
) -> HsmResult<Self> {
let default_extractable = config.derived_keys_default_extractable();
let mut resolved = Self {
key_type: default_key_type,
value_len: None,
sensitive: !default_extractable,
extractable: default_extractable,
};
for (attr_type, value) in template {
match *attr_type {
CKA_KEY_TYPE => {
resolved.key_type =
read_ck_ulong(value).ok_or(HsmError::AttributeValueInvalid)?;
}
CKA_VALUE_LEN => {
let len = read_ck_ulong(value).ok_or(HsmError::AttributeValueInvalid)?;
resolved.value_len =
Some(usize::try_from(len).map_err(|_| HsmError::KeySizeRange)?);
}
CKA_SENSITIVE => resolved.sensitive = parse_bbool(value)?,
CKA_EXTRACTABLE => resolved.extractable = parse_bbool(value)?,
_ => {}
}
}
if !matches!(resolved.key_type, CKK_AES | CKK_GENERIC_SECRET) {
return Err(HsmError::TemplateInconsistent);
}
if let Some(len) = resolved.value_len {
resolved.check_len(len)?;
}
Ok(resolved)
}
pub fn check_len(&self, len: usize) -> HsmResult<()> {
let ok = match self.key_type {
CKK_AES => matches!(len, 16 | 24 | 32),
_ => (1..=MAX_DERIVED_GENERIC_SECRET_LEN).contains(&len),
};
if ok {
Ok(())
} else {
Err(HsmError::KeySizeRange)
}
}
}
fn parse_bbool(value: &[u8]) -> HsmResult<bool> {
match value {
[b] => Ok(*b != 0),
_ => Err(HsmError::AttributeValueInvalid),
}
}
fn validate_okm_len(len: usize) -> HsmResult<()> {
if len == 0 || len > MAX_OKM_LEN {
tracing::error!(
"ECDH: requested OKM length {} is out of range (1..={})",
len,
MAX_OKM_LEN
);
return Err(HsmError::KeySizeRange);
}
Ok(())
}
const HKDF_SALT: &[u8] = b"CratonHSM-ECDH-HKDF-Salt-v1";
fn apply_hkdf(ikm: &[u8], info: &[u8], okm_len: usize) -> HsmResult<Vec<u8>> {
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::new(Some(HKDF_SALT), ikm);
let mut okm = vec![0u8; okm_len];
hk.expand(info, &mut okm).map_err(|e| {
use zeroize::Zeroize;
okm.zeroize();
tracing::error!("HKDF expand failed: {}", e);
HsmError::GeneralError
})?;
Ok(okm)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::rustcrypto_backend::RustCryptoBackend;
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn kdf(kdf: CK_EC_KDF_TYPE, z: &str, shared: &str, len: usize) -> HsmResult<Vec<u8>> {
let z = RawKeyMaterial::new(hex(z));
apply_ec_kdf(&RustCryptoBackend, kdf, &z, &hex(shared), len, false)
.map(|k| k.as_bytes().to_vec())
}
#[test]
fn x963_sha256_known_answer() {
let out = kdf(
CKD_SHA256_KDF,
"96c05619d56c328ab95fe84b18264b08725b85e33fd34f08",
"",
16,
)
.unwrap();
assert_eq!(out, hex("443024c3dae66b95e6f5670601558f71"));
}
#[test]
fn x963_sha256_shared_info_multi_block() {
let out = kdf(
CKD_SHA256_KDF,
"22518b10e70f2a3f243810ae3254139efbee04aa57c7af7d",
"75eef81aa3041e33b80971203d2c0c52",
128,
)
.unwrap();
assert_eq!(
out,
hex(concat!(
"c498af77161cc59f2962b9a713e2b215152d139766ce34a776df11866a69bf2e",
"52a13d9c7c6fc878c50c5ea0bc7b00e0da2447cfd874f6cf92f30d0097111485",
"500c90c3af8b487872d04685d14c8d1dc8d7fa08beb0ce0ababc11f0bd496269",
"142d43525a78e5bc79a17f59676a5706dc54d54d4d1f0bd7e386128ec26afc21",
))
);
}
#[test]
fn ckd_null_keeps_rightmost_bytes() {
let z = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
assert_eq!(
kdf(CKD_NULL, z, "", 16).unwrap(),
hex("101112131415161718191a1b1c1d1e1f")
);
assert_eq!(kdf(CKD_NULL, z, "", 32).unwrap(), hex(z));
assert!(matches!(
kdf(CKD_NULL, z, "", 33),
Err(HsmError::KeySizeRange)
));
}
#[test]
fn ckd_null_rejects_shared_data() {
assert!(matches!(
kdf(CKD_NULL, "00112233", "aa", 4),
Err(HsmError::MechanismParamInvalid)
));
}
#[test]
fn ckd_null_refused_in_fips_mode() {
let z = RawKeyMaterial::new(vec![7u8; 32]);
assert!(matches!(
apply_ec_kdf(&RustCryptoBackend, CKD_NULL, &z, &[], 32, true),
Err(HsmError::MechanismParamInvalid)
));
assert!(apply_ec_kdf(&RustCryptoBackend, CKD_SHA256_KDF, &z, &[], 32, true).is_ok());
}
#[test]
fn unsupported_kdfs_rejected() {
for k in [CKD_SHA1_KDF, CKD_SHA224_KDF, 0, 0xdead] {
assert!(
matches!(
kdf(k, "00112233", "", 4),
Err(HsmError::MechanismParamInvalid)
),
"kdf {k:#x}"
);
}
}
fn ulong(v: CK_KEY_TYPE) -> Vec<u8> {
v.to_ne_bytes().to_vec()
}
#[test]
fn derived_template_defaults_are_secure() {
let t = DerivedKeyTemplate::resolve(&[], CKK_AES, &AlgorithmConfig::default()).unwrap();
assert_eq!(
t,
DerivedKeyTemplate {
key_type: CKK_AES,
value_len: None,
sensitive: true,
extractable: false,
}
);
}
#[test]
fn derived_template_config_flag_flips_defaults() {
let config = AlgorithmConfig {
derived_keys_extractable_by_default: true,
..AlgorithmConfig::default()
};
let t = DerivedKeyTemplate::resolve(&[], CKK_GENERIC_SECRET, &config).unwrap();
assert!(!t.sensitive && t.extractable);
let tpl = vec![(CKA_SENSITIVE, vec![1]), (CKA_EXTRACTABLE, vec![0])];
let t = DerivedKeyTemplate::resolve(&tpl, CKK_GENERIC_SECRET, &config).unwrap();
assert!(t.sensitive && !t.extractable);
let fips = AlgorithmConfig {
fips_approved_only: true,
..config
};
let t = DerivedKeyTemplate::resolve(&[], CKK_GENERIC_SECRET, &fips).unwrap();
assert!(t.sensitive && !t.extractable);
}
#[test]
fn derived_template_allows_non_sensitive_extractable() {
let tpl = vec![(CKA_SENSITIVE, vec![0]), (CKA_EXTRACTABLE, vec![1])];
let t = DerivedKeyTemplate::resolve(&tpl, CKK_AES, &AlgorithmConfig::default()).unwrap();
assert!(!t.sensitive && t.extractable);
}
#[test]
fn derived_template_key_type_and_length() {
let config = AlgorithmConfig::default();
let generic48 = vec![
(CKA_KEY_TYPE, ulong(CKK_GENERIC_SECRET)),
(CKA_VALUE_LEN, ulong(48)),
];
let t = DerivedKeyTemplate::resolve(&generic48, CKK_AES, &config).unwrap();
assert_eq!((t.key_type, t.value_len), (CKK_GENERIC_SECRET, Some(48)));
let aes48 = vec![(CKA_KEY_TYPE, ulong(CKK_AES)), (CKA_VALUE_LEN, ulong(48))];
assert!(matches!(
DerivedKeyTemplate::resolve(&aes48, CKK_AES, &config),
Err(HsmError::KeySizeRange)
));
let rsa = vec![(CKA_KEY_TYPE, ulong(CKK_RSA))];
assert!(matches!(
DerivedKeyTemplate::resolve(&rsa, CKK_AES, &config),
Err(HsmError::TemplateInconsistent)
));
let bad_bool = vec![(CKA_SENSITIVE, vec![0, 0])];
assert!(matches!(
DerivedKeyTemplate::resolve(&bad_bool, CKK_AES, &config),
Err(HsmError::AttributeValueInvalid)
));
}
}