#[cfg(not(any(feature = "openssl", feature = "nss")))]
compile_error!(
"the `pkcs11-mgmt` feature requires at least one crypto backend; \
enable the `openssl` or `nss` feature"
);
use std::env;
use cryptoki::context::{CInitializeArgs, CInitializeFlags, Pkcs11};
use cryptoki::error::{Error as CkError, RvError};
use cryptoki::mechanism::Mechanism;
use cryptoki::object::{
Attribute, AttributeType, KeyType, MlDsaParameterSetType, MlKemParameterSetType, ObjectClass,
ParameterSetType,
};
use cryptoki::session::UserType;
use cryptoki::types::{AuthPin, Ulong};
use synta::{ObjectIdentifier, ToDer};
use crate::crypto::token_manager::{Pkcs11KeyInfo, SlotInfo, TokenManager};
use crate::crypto::{BackendPrivateKey, KeySpec, PrivateKeyError};
use crate::pkcs11_uri::Pkcs11Uri;
use crate::{oids, oids::EC_CURVE_P256, oids::EC_CURVE_P384, oids::EC_CURVE_P521};
const CANDIDATE_MODULE_PATHS: &[&str] = &[
"/usr/lib64/pkcs11/p11-kit-proxy.so",
"/usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-proxy.so",
"/usr/lib/aarch64-linux-gnu/pkcs11/p11-kit-proxy.so",
"/usr/lib/powerpc64le-linux-gnu/pkcs11/p11-kit-proxy.so",
"/usr/lib/s390x-linux-gnu/pkcs11/p11-kit-proxy.so",
"/usr/lib/pkcs11/p11-kit-proxy.so",
];
fn key_err(msg: impl Into<String>) -> PrivateKeyError {
PrivateKeyError(Box::new(std::io::Error::other(msg.into())))
}
fn from_cryptoki(e: CkError) -> PrivateKeyError {
PrivateKeyError(Box::new(e))
}
pub struct Pkcs11Manager {
pkcs11: Pkcs11,
}
impl Pkcs11Manager {
pub fn new(module_path: &str) -> Result<Self, PrivateKeyError> {
if !std::path::Path::new(module_path).is_absolute() {
return Err(key_err(format!(
"PKCS#11 module path must be absolute: '{module_path}'"
)));
}
let pkcs11 = Pkcs11::new(module_path).map_err(from_cryptoki)?;
match pkcs11.initialize(CInitializeArgs::new(CInitializeFlags::OS_LOCKING_OK)) {
Ok(()) => {}
Err(CkError::Pkcs11(RvError::CryptokiAlreadyInitialized, _)) => {}
Err(e) => return Err(from_cryptoki(e)),
}
Ok(Self { pkcs11 })
}
pub fn from_uri(uri: &Pkcs11Uri) -> Result<Self, PrivateKeyError> {
if let Some(path) = uri.attrs.module_path.as_deref() {
return Self::new(path);
}
Self::from_env()
}
pub fn from_env() -> Result<Self, PrivateKeyError> {
if let Ok(path) = env::var("PKCS11_MODULE_PATH") {
return Self::new(&path);
}
for candidate in CANDIDATE_MODULE_PATHS {
if std::path::Path::new(candidate).exists() {
return Self::new(candidate);
}
}
Err(key_err(format!(
"no PKCS#11 module found; set PKCS11_MODULE_PATH or install p11-kit; tried: {}",
CANDIDATE_MODULE_PATHS.join(", ")
)))
}
}
fn find_slot_by_token_name(
pkcs11: &Pkcs11,
token_name: &str,
) -> Result<cryptoki::slot::Slot, PrivateKeyError> {
let slots = pkcs11
.get_slots_with_initialized_token()
.map_err(from_cryptoki)?;
for slot in slots {
let info = pkcs11.get_token_info(slot).map_err(from_cryptoki)?;
if info.label().trim() == token_name.trim() {
return Ok(slot);
}
}
Err(key_err(format!("PKCS#11 token not found: '{token_name}'")))
}
fn login_if_needed(
session: &cryptoki::session::Session,
pin: Option<&str>,
) -> Result<(), PrivateKeyError> {
if let Some(pin_str) = pin {
match session.login(UserType::User, Some(&AuthPin::from(pin_str.to_owned()))) {
Ok(()) => {}
Err(CkError::Pkcs11(RvError::UserAlreadyLoggedIn, _)) => {}
Err(e) => return Err(from_cryptoki(e)),
}
}
Ok(())
}
fn map_key_type(kt: KeyType) -> &'static str {
if kt == KeyType::RSA {
"RSA"
} else if kt == KeyType::EC {
"EC"
} else if kt == KeyType::EC_EDWARDS {
"Ed"
} else if kt == KeyType::ML_DSA {
"ML-DSA"
} else if kt == KeyType::ML_KEM {
"ML-KEM"
} else {
"Unknown"
}
}
fn token_flags(info: &cryptoki::slot::TokenInfo) -> u64 {
let mut flags: u64 = 0;
if info.token_initialized() {
flags |= 0x0000_0400;
}
if info.login_required() {
flags |= 0x0000_0004;
}
if info.write_protected() {
flags |= 0x0000_0002;
}
if info.protected_authentication_path() {
flags |= 0x0000_0100;
}
flags
}
fn ml_dsa_param_set(name: &str) -> Option<ParameterSetType> {
match name {
"ML-DSA-44" => Some(MlDsaParameterSetType::ML_DSA_44.into()),
"ML-DSA-65" => Some(MlDsaParameterSetType::ML_DSA_65.into()),
"ML-DSA-87" => Some(MlDsaParameterSetType::ML_DSA_87.into()),
_ => None,
}
}
fn ml_kem_param_set(name: &str) -> Option<ParameterSetType> {
match name {
"ML-KEM-512" => Some(MlKemParameterSetType::ML_KEM_512.into()),
"ML-KEM-768" => Some(MlKemParameterSetType::ML_KEM_768.into()),
"ML-KEM-1024" => Some(MlKemParameterSetType::ML_KEM_1024.into()),
_ => None,
}
}
fn ec_oid_bytes(curve: &str) -> Option<Vec<u8>> {
let components: &[u32] = match curve {
"P-256" => EC_CURVE_P256,
"P-384" => EC_CURVE_P384,
"P-521" => EC_CURVE_P521,
"Ed25519" | "1.3.101.112" => oids::ED25519,
"Ed448" | "1.3.101.113" => oids::ED448,
_ => return None,
};
ObjectIdentifier::new(components).ok()?.to_der().ok()
}
impl TokenManager for Pkcs11Manager {
fn list_slots(&self) -> Result<Vec<SlotInfo>, PrivateKeyError> {
let slots = self
.pkcs11
.get_slots_with_initialized_token()
.map_err(from_cryptoki)?;
let mut result = Vec::with_capacity(slots.len());
for slot in slots {
let info = match self.pkcs11.get_token_info(slot) {
Ok(i) => i,
Err(e) => {
log::warn!(
"pkcs11: skipping slot {}: get_token_info failed: {e}",
slot.id()
);
continue;
}
};
result.push(SlotInfo {
slot_id: slot.id(),
token_label: info.label().trim().to_owned(),
manufacturer_id: info.manufacturer_id().trim().to_owned(),
model: info.model().trim().to_owned(),
serial_number: info.serial_number().trim().to_owned(),
flags: token_flags(&info),
});
}
Ok(result)
}
fn find_key(&self, uri: &Pkcs11Uri) -> Result<bool, PrivateKeyError> {
let token_name = uri
.attrs
.token
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'token=' for find_key"))?;
let obj_label = uri
.attrs
.object
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'object=' for find_key"))?;
let slot = find_slot_by_token_name(&self.pkcs11, token_name)?;
let session = self.pkcs11.open_ro_session(slot).map_err(from_cryptoki)?;
login_if_needed(&session, uri.attrs.pin_value())?;
let found = session
.find_objects(&[
Attribute::Class(ObjectClass::PRIVATE_KEY),
Attribute::Label(obj_label.as_bytes().to_vec()),
])
.map_err(from_cryptoki)?;
Ok(!found.is_empty())
}
fn list_keys(
&self,
token_name: &str,
pin: Option<&str>,
) -> Result<Vec<Pkcs11KeyInfo>, PrivateKeyError> {
let slot = find_slot_by_token_name(&self.pkcs11, token_name)?;
let session = self.pkcs11.open_ro_session(slot).map_err(from_cryptoki)?;
login_if_needed(&session, pin)?;
let handles = session
.find_objects(&[Attribute::Class(ObjectClass::PRIVATE_KEY)])
.map_err(from_cryptoki)?;
let mut keys = Vec::with_capacity(handles.len());
for handle in handles {
let attrs = match session.get_attributes(
handle,
&[
AttributeType::Label,
AttributeType::KeyType,
AttributeType::Id,
AttributeType::Modulus,
],
) {
Ok(a) => a,
Err(e) => {
log::warn!(
"pkcs11: skipping key object {:?}: get_attributes failed: {e}",
handle
);
continue;
}
};
let mut label = String::new();
let mut key_type_str: &'static str = "Unknown";
let mut key_bits: u32 = 0;
let mut id = Vec::new();
for attr in &attrs {
match attr {
Attribute::Label(bytes) => {
label = String::from_utf8_lossy(bytes).into_owned();
}
Attribute::KeyType(kt) => {
key_type_str = map_key_type(*kt);
}
Attribute::Id(bytes) => {
id = bytes.clone();
}
Attribute::Modulus(bytes) => {
let significant = bytes.iter().skip_while(|&&b| b == 0).count();
key_bits = (significant * 8) as u32;
}
_ => {}
}
}
keys.push(Pkcs11KeyInfo {
label,
id,
key_type: key_type_str.to_owned(),
key_bits,
});
}
Ok(keys)
}
fn delete_key(&self, uri: &Pkcs11Uri) -> Result<(), PrivateKeyError> {
let token_name = uri
.attrs
.token
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'token=' for delete_key"))?;
let obj_label = uri
.attrs
.object
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'object=' for delete_key"))?;
let slot = find_slot_by_token_name(&self.pkcs11, token_name)?;
let session = self.pkcs11.open_rw_session(slot).map_err(from_cryptoki)?;
login_if_needed(&session, uri.attrs.pin_value())?;
let label_bytes = obj_label.as_bytes().to_vec();
let mut priv_template = vec![
Attribute::Class(ObjectClass::PRIVATE_KEY),
Attribute::Label(label_bytes.clone()),
];
let mut pub_template = vec![
Attribute::Class(ObjectClass::PUBLIC_KEY),
Attribute::Label(label_bytes),
];
if let Some(id) = &uri.attrs.id {
priv_template.push(Attribute::Id(id.clone()));
pub_template.push(Attribute::Id(id.clone()));
}
let priv_handles = session
.find_objects(&priv_template)
.map_err(from_cryptoki)?;
if priv_handles.is_empty() {
return Err(key_err(format!(
"no private key with label '{obj_label}' found on token '{token_name}'"
)));
}
if priv_handles.len() > 1 {
return Err(key_err(format!(
"{} private keys with label '{obj_label}' found on token '{token_name}'; \
add id= to the URI to select the specific key",
priv_handles.len()
)));
}
for handle in priv_handles {
session.destroy_object(handle).map_err(from_cryptoki)?;
}
let pub_handles = session.find_objects(&pub_template).map_err(from_cryptoki)?;
if pub_handles.len() > 1 {
log::warn!(
"pkcs11: {} public keys with label '{obj_label}' on token '{token_name}'; \
deleting all",
pub_handles.len()
);
}
for handle in pub_handles {
session.destroy_object(handle).map_err(from_cryptoki)?;
}
Ok(())
}
fn generate_key_pair_in_token(
&self,
spec: &KeySpec,
uri: &Pkcs11Uri,
extractable: bool,
) -> Result<BackendPrivateKey, PrivateKeyError> {
let token_name = uri
.attrs
.token
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'token=' for key generation"))?;
let obj_label = uri
.attrs
.object
.as_deref()
.ok_or_else(|| key_err("PKCS#11 URI must contain 'object=' for key generation"))?;
let slot = find_slot_by_token_name(&self.pkcs11, token_name)?;
let session = self.pkcs11.open_rw_session(slot).map_err(from_cryptoki)?;
login_if_needed(&session, uri.attrs.pin_value())?;
let label_bytes = obj_label.as_bytes().to_vec();
match spec {
KeySpec::Rsa(bits) => {
if *bits < 2048 {
return Err(key_err(format!(
"RSA key size {bits} bits is below the 2048-bit minimum"
)));
}
let modulus_bits =
Ulong::try_from(*bits as usize).map_err(|e| key_err(e.to_string()))?;
let pub_tmpl = vec![
Attribute::Token(true),
Attribute::Verify(true),
Attribute::ModulusBits(modulus_bits),
Attribute::PublicExponent(vec![0x01, 0x00, 0x01]),
Attribute::Label(label_bytes.clone()),
];
let priv_tmpl = vec![
Attribute::Token(true),
Attribute::Private(true),
Attribute::Sensitive(true),
Attribute::Extractable(extractable),
Attribute::Sign(true),
Attribute::Label(label_bytes),
];
session
.generate_key_pair(&Mechanism::RsaPkcsKeyPairGen, &pub_tmpl, &priv_tmpl)
.map_err(from_cryptoki)?;
}
KeySpec::Ec(curve) => {
let ec_params = ec_oid_bytes(curve)
.ok_or_else(|| key_err(format!("unsupported EC curve: '{curve}'")))?;
let pub_tmpl = vec![
Attribute::Token(true),
Attribute::Verify(true),
Attribute::EcParams(ec_params),
Attribute::Label(label_bytes.clone()),
];
let priv_tmpl = vec![
Attribute::Token(true),
Attribute::Private(true),
Attribute::Sensitive(true),
Attribute::Extractable(extractable),
Attribute::Sign(true),
Attribute::Label(label_bytes),
];
session
.generate_key_pair(&Mechanism::EccKeyPairGen, &pub_tmpl, &priv_tmpl)
.map_err(from_cryptoki)?;
}
KeySpec::Ed25519 | KeySpec::Ed448 => {
let oid_name = if matches!(spec, KeySpec::Ed25519) {
"Ed25519"
} else {
"Ed448"
};
let ec_params = ec_oid_bytes(oid_name)
.ok_or_else(|| key_err(format!("internal: {oid_name} OID unavailable")))?;
let pub_tmpl = vec![
Attribute::Token(true),
Attribute::Verify(true),
Attribute::EcParams(ec_params),
Attribute::Label(label_bytes.clone()),
];
let priv_tmpl = vec![
Attribute::Token(true),
Attribute::Private(true),
Attribute::Sensitive(true),
Attribute::Extractable(extractable),
Attribute::Sign(true),
Attribute::Label(label_bytes),
];
session
.generate_key_pair(&Mechanism::EccEdwardsKeyPairGen, &pub_tmpl, &priv_tmpl)
.map_err(from_cryptoki)?;
}
KeySpec::MlDsa(ps) => {
let param_set = ml_dsa_param_set(ps).ok_or_else(|| {
key_err(format!(
"unsupported ML-DSA parameter set '{ps}'; \
expected ML-DSA-44, ML-DSA-65, or ML-DSA-87"
))
})?;
let pub_tmpl = vec![
Attribute::Token(true),
Attribute::Verify(true),
Attribute::ParameterSet(param_set),
Attribute::Label(label_bytes.clone()),
];
let priv_tmpl = vec![
Attribute::Token(true),
Attribute::Private(true),
Attribute::Sensitive(true),
Attribute::Extractable(extractable),
Attribute::Sign(true),
Attribute::ParameterSet(param_set),
Attribute::Label(label_bytes),
];
session
.generate_key_pair(&Mechanism::MlDsaKeyPairGen, &pub_tmpl, &priv_tmpl)
.map_err(from_cryptoki)?;
}
KeySpec::MlKem(ps) => {
let param_set = ml_kem_param_set(ps).ok_or_else(|| {
key_err(format!(
"unsupported ML-KEM parameter set '{ps}'; \
expected ML-KEM-512, ML-KEM-768, or ML-KEM-1024"
))
})?;
let pub_tmpl = vec![
Attribute::Token(true),
Attribute::Encrypt(true),
Attribute::Wrap(true),
Attribute::ParameterSet(param_set),
Attribute::Label(label_bytes.clone()),
];
let priv_tmpl = vec![
Attribute::Token(true),
Attribute::Private(true),
Attribute::Sensitive(true),
Attribute::Extractable(extractable),
Attribute::Decrypt(true),
Attribute::Unwrap(true),
Attribute::ParameterSet(param_set),
Attribute::Label(label_bytes),
];
session
.generate_key_pair(&Mechanism::MlKemKeyPairGen, &pub_tmpl, &priv_tmpl)
.map_err(from_cryptoki)?;
}
#[allow(unreachable_patterns)]
_ => {
return Err(key_err(format!(
"key type {spec:?} is not supported for HSM key generation"
)));
}
}
drop(session);
load_generated_key(uri)
}
}
fn load_generated_key(uri: &Pkcs11Uri) -> Result<BackendPrivateKey, PrivateKeyError> {
#[cfg(feature = "openssl")]
{
crate::openssl_backend::priv_load_from_pkcs11_uri(&uri.raw)
.map_err(|e| PrivateKeyError(Box::new(e)))
}
#[cfg(all(feature = "nss", not(feature = "openssl")))]
{
crate::nss_backend::priv_load_from_pkcs11_uri_nss(&uri.raw)
.map_err(|e| PrivateKeyError(Box::new(e)))
}
#[cfg(not(any(feature = "openssl", feature = "nss")))]
{
let _ = uri;
Err(key_err(
"no crypto backend (openssl or nss) enabled; cannot load generated key",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn find_test_module() -> Option<String> {
if let Ok(p) = std::env::var("PKCS11_MODULE_PATH") {
return Some(p);
}
for candidate in &[
"/usr/lib64/pkcs11/libkryoptic_pkcs11.so",
"/usr/lib/pkcs11/libkryoptic_pkcs11.so",
"/usr/lib64/softhsm/libsofthsm2.so",
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
] {
if std::path::Path::new(candidate).exists() {
return Some(candidate.to_string());
}
}
None
}
#[test]
fn list_slots_basic() {
let Some(module) = find_test_module() else {
eprintln!("no PKCS#11 module found — skipping");
return;
};
let mgr = match Pkcs11Manager::new(&module) {
Ok(m) => m,
Err(e) => {
eprintln!("PKCS#11 module unavailable ({e}) — skipping");
return;
}
};
let slots = mgr.list_slots().expect("list_slots");
eprintln!("found {} slot(s)", slots.len());
for s in &slots {
assert!(!s.token_label.is_empty(), "token_label must not be empty");
eprintln!(
" slot {}: {:?} / {:?}",
s.slot_id, s.token_label, s.manufacturer_id
);
}
}
}