use crate::{CryptoError, KeyMetadata, Result};
use ed25519_dalek::{Signature as Ed25519Signature, Verifier as _, VerifyingKey};
use rand::RngCore;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HsmConfig {
pub hsm_type: HsmType,
pub connection: String,
pub slot: u32,
pub key_label_prefix: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum HsmType {
#[default]
ThalesLuna,
Utimaco,
AwsCloudHsm,
AzureKeyVault,
SoftHSM,
}
#[derive(Debug, Clone)]
pub struct HsmKeyHandle {
pub key_id: String,
pub slot: u32,
pub algorithm: String,
}
pub trait HsmSignerOps {
fn sign(&mut self, key_handle: &HsmKeyHandle, data: &[u8]) -> Result<Vec<u8>>;
}
pub trait HsmVerifierOps {
fn verify(&mut self, key_handle: &HsmKeyHandle, data: &[u8], signature: &[u8]) -> Result<bool>;
}
pub trait HsmSession: Send + HsmSignerOps + HsmVerifierOps {
fn generate_key_pair(&mut self, algorithm: &str, key_id: &str) -> Result<HsmKeyHandle>;
fn import_key(&mut self, key_id: &str, key_data: &[u8]) -> Result<HsmKeyHandle>;
fn close(&mut self);
}
pub struct SoftHsm {
config: HsmConfig,
keys: std::collections::HashMap<String, Vec<u8>>,
public_keys: std::collections::HashMap<String, Vec<u8>>,
}
impl SoftHsm {
pub fn new(config: HsmConfig) -> Self {
SoftHsm {
config,
keys: std::collections::HashMap::new(),
public_keys: std::collections::HashMap::new(),
}
}
}
impl HsmSignerOps for SoftHsm {
fn sign(&mut self, key_handle: &HsmKeyHandle, data: &[u8]) -> Result<Vec<u8>> {
let key_data = self
.keys
.get(&key_handle.key_id)
.ok_or_else(|| CryptoError::HsmError("Key not found".to_string()))?;
if key_data.len() != 32 {
return Err(CryptoError::HsmError(
"Invalid SoftHSM key size (expected 32-byte Ed25519 seed)".to_string(),
));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(key_data);
let key =
crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_handle.key_id.clone()));
Ok(key.sign(data))
}
}
impl HsmVerifierOps for SoftHsm {
fn verify(&mut self, key_handle: &HsmKeyHandle, data: &[u8], signature: &[u8]) -> Result<bool> {
if signature.len() != 64 {
return Ok(false);
}
let public_key = match self.public_keys.get(&key_handle.key_id) {
Some(pk) => pk,
None => {
return Err(CryptoError::HsmError(format!(
"Public key not found for key_id {}",
key_handle.key_id
)));
}
};
if public_key.len() != 32 {
return Err(CryptoError::HsmError(
"Invalid SoftHSM public key size (expected 32-byte Ed25519 public key)".to_string(),
));
}
let mut pk = [0u8; 32];
pk.copy_from_slice(public_key);
let verifying_key = VerifyingKey::from_bytes(&pk).map_err(|e| {
CryptoError::HsmError(format!("Invalid SoftHSM Ed25519 public key bytes: {e}"))
})?;
let mut sig = [0u8; 64];
sig.copy_from_slice(signature);
let signature = Ed25519Signature::from_bytes(&sig);
Ok(verifying_key.verify(data, &signature).is_ok())
}
}
impl HsmSession for SoftHsm {
fn generate_key_pair(&mut self, algorithm: &str, key_id: &str) -> Result<HsmKeyHandle> {
let mut key_data = vec![0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key_data);
let mut seed = [0u8; 32];
seed.copy_from_slice(&key_data);
let key = crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_id.to_string()));
self.public_keys
.insert(key_id.to_string(), key.verifying_key());
self.keys.insert(key_id.to_string(), key_data);
Ok(HsmKeyHandle {
key_id: key_id.to_string(),
slot: self.config.slot,
algorithm: algorithm.to_string(),
})
}
fn import_key(&mut self, key_id: &str, key_data: &[u8]) -> Result<HsmKeyHandle> {
if key_data.len() != 32 {
return Err(CryptoError::HsmError(
"SoftHSM import expects a 32-byte Ed25519 seed".to_string(),
));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(key_data);
let key = crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_id.to_string()));
self.public_keys
.insert(key_id.to_string(), key.verifying_key());
self.keys.insert(key_id.to_string(), key_data.to_vec());
Ok(HsmKeyHandle {
key_id: key_id.to_string(),
slot: self.config.slot,
algorithm: "ED25519".to_string(),
})
}
fn close(&mut self) {
self.keys.clear();
self.public_keys.clear();
}
}
pub fn create_hsm_session(config: &HsmConfig) -> Result<Box<dyn HsmSession>> {
if matches!(config.hsm_type, HsmType::SoftHSM) && is_production_environment() {
return Err(CryptoError::HsmError(
"SoftHSM is forbidden in production environment".to_string(),
));
}
match config.hsm_type {
HsmType::SoftHSM => Ok(Box::new(SoftHsm::new(config.clone()))),
_ => Err(CryptoError::HsmError(format!(
"HSM type {:?} not implemented",
config.hsm_type
))),
}
}
pub struct HsmSigner {
session: Box<dyn HsmSession>,
key_handle: HsmKeyHandle,
config: HsmConfig,
}
impl HsmSigner {
pub fn new(config: HsmConfig, key_id: &str) -> Result<Self> {
let mut session = create_hsm_session(&config)?;
let key_handle = session.generate_key_pair("ED25519", key_id)?;
Ok(HsmSigner {
session,
key_handle,
config,
})
}
pub fn sign(&mut self, data: &[u8]) -> Result<Vec<u8>> {
self.session.sign(&self.key_handle, data)
}
pub fn verify(&mut self, data: &[u8], signature: &[u8]) -> Result<bool> {
self.session.verify(&self.key_handle, data, signature)
}
pub fn metadata(&self) -> KeyMetadata {
let algorithm = match self.key_handle.algorithm.to_ascii_uppercase().as_str() {
"ED25519" => crate::SignatureAlgorithm::Ed25519,
"RSA" | "RSA-PSS" | "RSA-PSS-4096" => crate::SignatureAlgorithm::RsaPss4096,
_ => crate::SignatureAlgorithm::Ed25519,
};
KeyMetadata {
key_id: self.key_handle.key_id.clone(),
algorithm,
created_at: chrono::Utc::now().timestamp(),
key_type: crate::KeyType::HsmBacked,
hsm_slot: Some(self.config.slot.to_string()),
}
}
}
fn is_production_environment() -> bool {
[
std::env::var("ENV").ok(),
std::env::var("APP_ENV").ok(),
std::env::var("RUST_ENV").ok(),
]
.into_iter()
.flatten()
.any(|v| matches!(v.to_ascii_lowercase().as_str(), "prod" | "production"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soft_hsm() {
let config = HsmConfig {
hsm_type: HsmType::SoftHSM,
connection: "localhost".to_string(),
slot: 0,
key_label_prefix: "test".to_string(),
};
let mut session = create_hsm_session(&config).unwrap();
let handle = session.generate_key_pair("RSA", "test-key").unwrap();
let data = b"test data";
let signature = session.sign(&handle, data).unwrap();
assert!(session.verify(&handle, data, &signature).unwrap());
}
#[test]
fn test_hsm_signer_soft_hsm_roundtrip() {
let config = HsmConfig {
hsm_type: HsmType::SoftHSM,
connection: "local://softhsm".to_string(),
slot: 0,
key_label_prefix: "audit".to_string(),
};
let mut signer = HsmSigner::new(config, "audit-key").unwrap();
let data = b"daily publication payload";
let sig = signer.sign(data).unwrap();
assert!(signer.verify(data, &sig).unwrap());
assert!(!signer.verify(b"tampered", &sig).unwrap());
}
#[test]
fn test_soft_hsm_verify_rejects_signature_from_other_key() {
let config = HsmConfig {
hsm_type: HsmType::SoftHSM,
connection: "local://softhsm".to_string(),
slot: 0,
key_label_prefix: "audit".to_string(),
};
let mut s1 = HsmSigner::new(config.clone(), "k1").unwrap();
let mut s2 = HsmSigner::new(config, "k2").unwrap();
let data = b"daily publication payload";
let sig = s1.sign(data).unwrap();
assert!(!s2.verify(data, &sig).unwrap());
}
}