use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use cryptoki::context::{CInitializeArgs, CInitializeFlags, Pkcs11};
use cryptoki::error::{Error as CryptokiError, RvError};
use cryptoki::mechanism::{Mechanism, MechanismType};
use cryptoki::object::{Attribute, AttributeType, ObjectClass, ObjectHandle};
use cryptoki::session::{Session, UserType};
use cryptoki::slot::Slot;
use cryptoki::types::AuthPin;
use rcgen::{PublicKeyData, SignatureAlgorithm, SigningKey};
use ring::digest;
use simple_asn1::{ASN1Block, BigInt, BigUint};
use tracing::{error, info, warn};
use crate::config::LocalCaConfig;
const OID_P256: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
const OID_P384: &[u8] = &[0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22];
static CONTEXTS: OnceLock<Mutex<HashMap<PathBuf, Arc<Pkcs11>>>> = OnceLock::new();
fn context_for(module_path: &str) -> anyhow::Result<Arc<Pkcs11>> {
let key = std::fs::canonicalize(module_path).unwrap_or_else(|_| PathBuf::from(module_path));
let mut contexts = CONTEXTS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.map_err(|_| anyhow::anyhow!("the PKCS#11 context registry mutex is poisoned"))?;
if let Some(context) = contexts.get(&key) {
return Ok(context.clone());
}
let context = Pkcs11::new(&key).map_err(|error| {
anyhow::anyhow!(
"PKCS#11 module `{}` could not be loaded: {error}",
module_path
)
})?;
context
.initialize(CInitializeArgs::new(CInitializeFlags::OS_LOCKING_OK))
.map_err(|error| {
anyhow::anyhow!(
"PKCS#11 module `{}` failed C_Initialize: {error}",
module_path
)
})?;
let context = Arc::new(context);
contexts.insert(key, context.clone());
Ok(context)
}
fn login_as_user(session: &Session, pin: &AuthPin) -> Result<(), CryptokiError> {
match session.login(UserType::User, Some(pin)) {
Ok(()) => Ok(()),
Err(CryptokiError::Pkcs11(RvError::UserAlreadyLoggedIn, _)) => Ok(()),
Err(error) => Err(error),
}
}
struct Reconnect {
context: Arc<Pkcs11>,
slot: Slot,
pin: AuthPin,
key_label: String,
key_id: Option<Vec<u8>>,
}
pub struct Pkcs11SigningKey {
session: Mutex<Session>,
key_handle: Mutex<ObjectHandle>,
public_key_der: Vec<u8>,
algorithm: &'static SignatureAlgorithm,
mechanism: MechanismChoice,
reconnect: Reconnect,
description: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MechanismChoice {
Hashing(MechanismType),
RawWithDigest(&'static digest::Algorithm),
}
impl std::fmt::Debug for MechanismChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hashing(MechanismType::ECDSA_SHA384) => f.write_str("CKM_ECDSA_SHA384"),
Self::Hashing(_) => f.write_str("CKM_ECDSA_SHA256"),
Self::RawWithDigest(algorithm) if std::ptr::eq(*algorithm, &digest::SHA384) => {
f.write_str("CKM_ECDSA+SHA384")
}
Self::RawWithDigest(_) => f.write_str("CKM_ECDSA+SHA256"),
}
}
}
impl Pkcs11SigningKey {
pub fn open(cfg: &LocalCaConfig) -> anyhow::Result<Self> {
let pkcs11 = &cfg.pkcs11;
if pkcs11.module_path.is_empty() {
anyhow::bail!(
"local_ca key_source = \"pkcs11\" needs signer.local_ca.pkcs11.module_path \
(e.g. \"/usr/lib/softhsm/libsofthsm2.so\" or \"/usr/lib/libykcs11.so\")"
);
}
if pkcs11.key_label.is_empty() {
anyhow::bail!(
"local_ca key_source = \"pkcs11\" needs signer.local_ca.pkcs11.key_label; \
list what the token holds with \
`pkcs11-tool --module {} --list-objects --login`",
pkcs11.module_path
);
}
let key_id = parse_key_id(&pkcs11.key_id)?;
let pin = AuthPin::from(super::key::read_pin(cfg)?);
let context = context_for(&pkcs11.module_path)?;
let slot = resolve_slot(&context, pkcs11.token_label.as_str(), pkcs11.slot_id)?;
let session = context.open_ro_session(slot).map_err(|error| {
anyhow::anyhow!("could not open a PKCS#11 session on slot {slot}: {error}")
})?;
login_as_user(&session, &pin).map_err(|error| {
anyhow::anyhow!(
"PKCS#11 login failed on slot {slot}: {error} \
(check the PIN in signer.local_ca.pkcs11.pin_file — note that a token \
typically blocks the PIN after a few wrong attempts)"
)
})?;
let key_handle = find_private_key(&session, &pkcs11.key_label, key_id.as_deref())?;
let (public_key_der, algorithm) =
read_public_key(&session, &pkcs11.key_label, key_id.as_deref(), key_handle)?;
let mechanism = choose_mechanism(&context, slot, algorithm)?;
let description = format!(
"module={} slot={} key_label={}",
pkcs11.module_path, slot, pkcs11.key_label
);
info!(
event = "local_ca_pkcs11_opened",
outcome = "success",
module = %pkcs11.module_path,
slot = %slot,
key_label = %pkcs11.key_label,
algorithm = ?algorithm,
mechanism = ?mechanism,
"the local CA's issuing key is on a PKCS#11 token",
);
Ok(Self {
session: Mutex::new(session),
key_handle: Mutex::new(key_handle),
public_key_der,
algorithm,
mechanism,
reconnect: Reconnect {
context,
slot,
pin,
key_label: pkcs11.key_label.clone(),
key_id,
},
description,
})
}
fn sign_once(&self, msg: &[u8]) -> Result<Vec<u8>, CryptokiError> {
let (mechanism, payload);
match self.mechanism {
MechanismChoice::Hashing(kind) => {
mechanism = mechanism_from_type(kind);
payload = msg.to_vec();
}
MechanismChoice::RawWithDigest(algorithm) => {
mechanism = Mechanism::Ecdsa;
payload = digest::digest(algorithm, msg).as_ref().to_vec();
}
}
let session = self
.session
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let handle = *self
.key_handle
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
session.sign(&mechanism, handle, &payload)
}
fn reconnect(&self) -> anyhow::Result<()> {
let session = self
.reconnect
.context
.open_ro_session(self.reconnect.slot)?;
login_as_user(&session, &self.reconnect.pin)?;
let handle = find_private_key(
&session,
&self.reconnect.key_label,
self.reconnect.key_id.as_deref(),
)?;
*self
.session
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = session;
*self
.key_handle
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = handle;
Ok(())
}
}
impl PublicKeyData for Pkcs11SigningKey {
fn der_bytes(&self) -> &[u8] {
&self.public_key_der
}
fn algorithm(&self) -> &'static SignatureAlgorithm {
self.algorithm
}
}
impl SigningKey for Pkcs11SigningKey {
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, rcgen::Error> {
let raw = match self.sign_once(msg) {
Ok(raw) => raw,
Err(error) if is_recoverable(&error) => {
warn!(
event = "local_ca_pkcs11_session_lost",
outcome = "advisory",
error = %error,
token = %self.description,
"reopening the PKCS#11 session and retrying the signature once",
);
self.reconnect().map_err(|error| {
error!(
event = "local_ca_pkcs11_reconnect_failed",
outcome = "failure",
error = %error,
token = %self.description,
);
rcgen::Error::RemoteKeyError
})?;
self.sign_once(msg).map_err(|error| {
error!(
event = "local_ca_pkcs11_sign_retry_failed",
outcome = "failure",
error = %error,
token = %self.description,
);
rcgen::Error::RemoteKeyError
})?
}
Err(error) => {
error!(
event = "local_ca_pkcs11_sign_failed",
outcome = "failure",
error = %error,
token = %self.description,
);
return Err(rcgen::Error::RemoteKeyError);
}
};
raw_ecdsa_to_der(&raw).map_err(|error| {
error!(
event = "local_ca_pkcs11_signature_malformed",
outcome = "failure",
error = %error,
len = raw.len(),
token = %self.description,
);
rcgen::Error::RemoteKeyError
})
}
}
impl std::fmt::Debug for Pkcs11SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pkcs11SigningKey")
.field("token", &self.description)
.field("mechanism", &self.mechanism)
.finish_non_exhaustive()
}
}
fn is_recoverable(error: &CryptokiError) -> bool {
matches!(
error,
CryptokiError::Pkcs11(
RvError::SessionHandleInvalid
| RvError::SessionClosed
| RvError::DeviceError
| RvError::DeviceRemoved
| RvError::UserNotLoggedIn
| RvError::ObjectHandleInvalid,
_
)
)
}
fn raw_ecdsa_to_der(raw: &[u8]) -> anyhow::Result<Vec<u8>> {
if raw.is_empty() || !raw.len().is_multiple_of(2) {
anyhow::bail!(
"a raw ECDSA signature must be an even, non-zero number of bytes, got {}",
raw.len()
);
}
let (r, s) = raw.split_at(raw.len() / 2);
let sequence = ASN1Block::Sequence(
0,
vec![
ASN1Block::Integer(0, BigInt::from(BigUint::from_bytes_be(r))),
ASN1Block::Integer(0, BigInt::from(BigUint::from_bytes_be(s))),
],
);
simple_asn1::to_der(&sequence)
.map_err(|error| anyhow::anyhow!("could not DER-encode the ECDSA signature: {error}"))
}
fn unwrap_ec_point(attribute: &[u8]) -> anyhow::Result<Vec<u8>> {
if attribute.len() < 2 {
anyhow::bail!(
"CKA_EC_POINT is too short to be a public key ({} bytes)",
attribute.len()
);
}
if attribute[0] == 0x04 {
let (header, length) = match attribute[1] {
len @ 0x00..=0x7f => (2usize, len as usize),
0x81 if attribute.len() >= 3 => (3usize, attribute[2] as usize),
0x82 if attribute.len() >= 4 => (
4usize,
u16::from_be_bytes([attribute[2], attribute[3]]) as usize,
),
_ => (0, 0),
};
if header != 0 && header + length == attribute.len() {
let point = &attribute[header..];
if point.first() == Some(&0x04) {
return Ok(point.to_vec());
}
}
return Ok(attribute.to_vec());
}
anyhow::bail!(
"CKA_EC_POINT does not hold an uncompressed EC point (first byte 0x{:02x}); \
compressed points are not supported",
attribute[0]
)
}
fn parse_key_id(key_id: &str) -> anyhow::Result<Option<Vec<u8>>> {
if key_id.is_empty() {
return Ok(None);
}
let cleaned: String = key_id
.chars()
.filter(|c| !c.is_whitespace() && *c != ':')
.collect();
let bytes = hex::decode(&cleaned).map_err(|error| {
anyhow::anyhow!("signer.local_ca.pkcs11.key_id `{key_id}` is not hexadecimal: {error}")
})?;
Ok(Some(bytes))
}
fn resolve_slot(context: &Pkcs11, token_label: &str, slot_id: Option<u64>) -> anyhow::Result<Slot> {
let slots = context
.get_slots_with_token()
.map_err(|error| anyhow::anyhow!("could not list PKCS#11 slots: {error}"))?;
if !token_label.is_empty() {
let mut seen = Vec::new();
for slot in &slots {
match context.get_token_info(*slot) {
Ok(info) => {
let label = info.label().trim().to_string();
if label == token_label {
return Ok(*slot);
}
seen.push(label);
}
Err(error) => {
warn!(
event = "local_ca_pkcs11_token_info_failed",
outcome = "failure",
slot = %slot,
error = %error,
);
}
}
}
anyhow::bail!("no PKCS#11 token labelled `{token_label}`; tokens present: {seen:?}");
}
if let Some(id) = slot_id {
let wanted = Slot::try_from(id)
.map_err(|error| anyhow::anyhow!("slot_id {id} is not a valid slot: {error}"))?;
if slots.contains(&wanted) {
return Ok(wanted);
}
anyhow::bail!("no PKCS#11 token in slot {id}; slots with a token: {slots:?}");
}
anyhow::bail!(
"local_ca key_source = \"pkcs11\" needs signer.local_ca.pkcs11.token_label \
(preferred) or slot_id; tokens present: {slots:?}"
)
}
fn find_private_key(
session: &Session,
key_label: &str,
key_id: Option<&[u8]>,
) -> anyhow::Result<ObjectHandle> {
let mut template = vec![
Attribute::Class(ObjectClass::PRIVATE_KEY),
Attribute::Label(key_label.as_bytes().to_vec()),
];
if let Some(id) = key_id {
template.push(Attribute::Id(id.to_vec()));
}
let handles = session
.find_objects(&template)
.map_err(|error| anyhow::anyhow!("searching for the private key failed: {error}"))?;
match handles.len() {
0 => anyhow::bail!(
"no PKCS#11 private key labelled `{key_label}`{}; list what the token holds with \
`pkcs11-tool --module <module> --list-objects --login`",
match key_id {
Some(id) => format!(" with id {}", hex::encode(id)),
None => String::new(),
}
),
1 => Ok(handles[0]),
n => anyhow::bail!(
"{n} PKCS#11 private keys are labelled `{key_label}`; set \
signer.local_ca.pkcs11.key_id to pick one"
),
}
}
fn read_public_key(
session: &Session,
key_label: &str,
key_id: Option<&[u8]>,
private_key: ObjectHandle,
) -> anyhow::Result<(Vec<u8>, &'static SignatureAlgorithm)> {
let mut template = vec![
Attribute::Class(ObjectClass::PUBLIC_KEY),
Attribute::Label(key_label.as_bytes().to_vec()),
];
if let Some(id) = key_id {
template.push(Attribute::Id(id.to_vec()));
}
let public = session.find_objects(&template).unwrap_or_default();
let handle = public.first().copied().unwrap_or(private_key);
let attributes = session
.get_attributes(handle, &[AttributeType::EcPoint, AttributeType::EcParams])
.map_err(|error| {
anyhow::anyhow!(
"could not read the public key of `{key_label}` from the token: {error} \
(the CA certificate's key must be readable to confirm it matches)"
)
})?;
let mut point = None;
let mut params = None;
for attribute in attributes {
match attribute {
Attribute::EcPoint(value) => point = Some(value),
Attribute::EcParams(value) => params = Some(value),
_ => {}
}
}
let point = point.ok_or_else(|| {
anyhow::anyhow!(
"the PKCS#11 key `{key_label}` exposes no CKA_EC_POINT; only ECDSA keys are \
supported by this backend"
)
})?;
let params = params
.ok_or_else(|| anyhow::anyhow!("the PKCS#11 key `{key_label}` exposes no CKA_EC_PARAMS"))?;
let algorithm = algorithm_for_curve(¶ms)?;
Ok((unwrap_ec_point(&point)?, algorithm))
}
fn algorithm_for_curve(params: &[u8]) -> anyhow::Result<&'static SignatureAlgorithm> {
match params {
OID_P256 => Ok(&rcgen::PKCS_ECDSA_P256_SHA256),
OID_P384 => Ok(&rcgen::PKCS_ECDSA_P384_SHA384),
other => anyhow::bail!(
"unsupported PKCS#11 curve (CKA_EC_PARAMS {}); this backend supports \
P-256 (secp256r1) and P-384 (secp384r1)",
hex::encode(other)
),
}
}
fn choose_mechanism(
context: &Pkcs11,
slot: Slot,
algorithm: &'static SignatureAlgorithm,
) -> anyhow::Result<MechanismChoice> {
let (hashing, digest_algorithm) = if algorithm == &rcgen::PKCS_ECDSA_P256_SHA256 {
(MechanismType::ECDSA_SHA256, &digest::SHA256)
} else {
(MechanismType::ECDSA_SHA384, &digest::SHA384)
};
let available = context.get_mechanism_list(slot).unwrap_or_else(|error| {
warn!(
event = "local_ca_pkcs11_mechanism_list_failed",
outcome = "failure",
slot = %slot,
error = %error,
);
Vec::new()
});
if available.contains(&hashing) {
return Ok(MechanismChoice::Hashing(hashing));
}
if available.is_empty() || available.contains(&MechanismType::ECDSA) {
return Ok(MechanismChoice::RawWithDigest(digest_algorithm));
}
anyhow::bail!(
"the token in slot {slot} supports neither {hashing} nor CKM_ECDSA, so this backend \
cannot sign with it"
)
}
fn mechanism_from_type(kind: MechanismType) -> Mechanism<'static> {
match kind {
MechanismType::ECDSA_SHA384 => Mechanism::EcdsaSha384,
_ => Mechanism::EcdsaSha256,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_raw_signature_becomes_a_der_sequence_of_two_integers() {
let raw: Vec<u8> = (1u8..=64).collect();
let der = raw_ecdsa_to_der(&raw).unwrap();
let blocks = simple_asn1::from_der(&der).unwrap();
let ASN1Block::Sequence(_, items) = &blocks[0] else {
panic!("expected a SEQUENCE, got {blocks:?}");
};
assert_eq!(items.len(), 2);
assert!(matches!(items[0], ASN1Block::Integer(..)));
assert!(matches!(items[1], ASN1Block::Integer(..)));
}
#[test]
fn a_high_bit_half_stays_positive() {
let mut raw = vec![0u8; 64];
raw[0] = 0xff; raw[32] = 0x7f; raw[63] = 0x01;
let der = raw_ecdsa_to_der(&raw).unwrap();
let blocks = simple_asn1::from_der(&der).unwrap();
let ASN1Block::Sequence(_, items) = &blocks[0] else {
panic!("expected a SEQUENCE");
};
let zero = BigInt::from(0u8);
for item in items {
let ASN1Block::Integer(_, value) = item else {
panic!("expected INTEGER, got {item:?}");
};
assert!(
*value > zero,
"both halves are unsigned; a negative INTEGER means the bytes were read \
as signed, and the signature would verify nowhere",
);
}
assert_eq!(&der[..4], &[0x30, 0x45, 0x02, 0x21], "{der:02x?}");
assert_eq!(der[4], 0x00, "r must be zero-padded: {der:02x?}");
}
#[test]
fn leading_zeros_are_dropped() {
let mut raw = vec![0u8; 64];
raw[31] = 0x09; raw[63] = 0x0a; let der = raw_ecdsa_to_der(&raw).unwrap();
assert_eq!(der, vec![0x30, 0x06, 0x02, 0x01, 0x09, 0x02, 0x01, 0x0a]);
}
#[test]
fn a_malformed_raw_signature_is_an_error_not_a_bad_certificate() {
assert!(raw_ecdsa_to_der(&[]).is_err());
assert!(raw_ecdsa_to_der(&[0x01, 0x02, 0x03]).is_err(), "odd length");
}
#[test]
fn a_der_wrapped_ec_point_is_unwrapped() {
let mut point = vec![0x04];
point.extend(std::iter::repeat_n(0xab, 64));
let mut wrapped = vec![0x04, 0x41];
wrapped.extend_from_slice(&point);
assert_eq!(unwrap_ec_point(&wrapped).unwrap(), point);
}
#[test]
fn a_bare_ec_point_is_taken_as_is() {
let mut point = vec![0x04];
point.extend(std::iter::repeat_n(0xcd, 64));
assert_eq!(unwrap_ec_point(&point).unwrap(), point);
}
#[test]
fn a_long_form_der_wrapped_ec_point_is_unwrapped() {
let mut point = vec![0x04];
point.extend(std::iter::repeat_n(0xef, 96));
let mut wrapped = vec![0x04, 0x81, 0x61];
wrapped.extend_from_slice(&point);
assert_eq!(unwrap_ec_point(&wrapped).unwrap(), point);
}
#[test]
fn a_compressed_or_empty_ec_point_is_refused() {
assert!(unwrap_ec_point(&[]).is_err());
assert!(unwrap_ec_point(&[0x04]).is_err(), "too short");
assert!(unwrap_ec_point(&[0x02, 0x20, 0xaa]).is_err());
}
#[test]
fn the_two_supported_curves_map_to_rcgen_algorithms() {
assert_eq!(
algorithm_for_curve(OID_P256).unwrap(),
&rcgen::PKCS_ECDSA_P256_SHA256
);
assert_eq!(
algorithm_for_curve(OID_P384).unwrap(),
&rcgen::PKCS_ECDSA_P384_SHA384
);
}
#[test]
fn an_unsupported_curve_is_an_error_naming_it() {
let p521 = &[0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23];
let error = algorithm_for_curve(p521).unwrap_err().to_string();
assert!(error.contains("P-256"), "{error}");
assert!(error.contains(&hex::encode(p521)), "{error}");
}
#[test]
fn key_ids_are_parsed_as_hex_and_tolerate_separators() {
assert_eq!(parse_key_id("").unwrap(), None);
assert_eq!(parse_key_id("01ff").unwrap(), Some(vec![0x01, 0xff]));
assert_eq!(parse_key_id("01:ff").unwrap(), Some(vec![0x01, 0xff]));
assert!(parse_key_id("nothex").is_err());
}
}
#[cfg(test)]
mod softhsm {
use super::*;
use crate::config::{LocalCaConfig, Pkcs11Config};
use crate::signer::local_ca::LocalCa;
use crate::signer::SignerBackend;
use crate::testutil::TempDir;
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyUsagePurpose};
use std::path::Path;
use std::sync::OnceLock;
const MODULE_CANDIDATES: &[&str] = &[
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib64/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
"/usr/local/lib/softhsm/libsofthsm2.so",
];
const SO_PIN: &str = "3737";
const USER_PIN: &str = "1234";
const TOKEN_LABEL: &str = "acme-proxy-test";
const KEY_LABEL: &str = "ca-key";
fn module_path() -> Option<&'static str> {
MODULE_CANDIDATES
.iter()
.copied()
.find(|path| Path::new(path).exists())
}
struct Lab {
_dir: TempDir,
module_path: &'static str,
ca_pem_path: std::path::PathBuf,
crl_path: std::path::PathBuf,
pin_path: std::path::PathBuf,
}
impl Lab {
fn config(&self) -> LocalCaConfig {
LocalCaConfig {
cert_path: self.ca_pem_path.to_string_lossy().into_owned(),
crl_path: self.crl_path.to_string_lossy().into_owned(),
key_source: "pkcs11".to_string(),
pkcs11: Pkcs11Config {
module_path: self.module_path.to_string(),
token_label: TOKEN_LABEL.to_string(),
key_label: KEY_LABEL.to_string(),
pin_file: self.pin_path.to_string_lossy().into_owned(),
..Pkcs11Config::default()
},
..LocalCaConfig::default()
}
}
}
fn lab() -> Option<&'static Lab> {
static LAB: OnceLock<Option<Lab>> = OnceLock::new();
LAB.get_or_init(|| {
let module_path = module_path()?;
Some(build_lab(module_path).expect("SoftHSM2 is present, so the lab must build"))
})
.as_ref()
}
const REQUIRE_LAB: &str = "ACME_PROXY_REQUIRE_SOFTHSM";
macro_rules! lab_or_skip {
() => {
match lab() {
Some(lab) => lab,
None => {
assert!(
std::env::var_os(REQUIRE_LAB).is_none(),
"{REQUIRE_LAB} is set, so skipping is a failure: no SoftHSM2 \
module was found (looked in {MODULE_CANDIDATES:?}). Either the \
module moved or the install did not happen — in both cases \
these tests were about to report green without running."
);
eprintln!(
"skipping: no SoftHSM2 module found (looked in {MODULE_CANDIDATES:?}); \
install softhsm2 to run the PKCS#11 tests"
);
return;
}
}
};
}
#[test]
fn the_pkcs11_lab_is_available_when_it_is_required() {
if std::env::var_os(REQUIRE_LAB).is_none() {
return;
}
assert!(
module_path().is_some(),
"{REQUIRE_LAB} is set but no SoftHSM2 module was found in \
{MODULE_CANDIDATES:?}"
);
}
fn build_lab(module_path: &'static str) -> anyhow::Result<Lab> {
let dir = TempDir::new("softhsm");
let tokens = dir.path().join("tokens");
std::fs::create_dir_all(&tokens)?;
let conf = dir.write(
"softhsm2.conf",
&format!(
"directories.tokendir = {}\nobjectstore.backend = file\nlog.level = ERROR\n",
tokens.display()
),
);
unsafe { std::env::set_var("SOFTHSM2_CONF", &conf) };
let context = context_for(module_path)?;
let slot = *context
.get_all_slots()?
.first()
.ok_or_else(|| anyhow::anyhow!("SoftHSM2 exposes no slots"))?;
let so_pin = AuthPin::from(SO_PIN.to_string());
let user_pin = AuthPin::from(USER_PIN.to_string());
context.init_token(slot, &so_pin, TOKEN_LABEL)?;
let slot = resolve_slot(&context, TOKEN_LABEL, None)?;
{
let session = context.open_rw_session(slot)?;
session.login(UserType::So, Some(&so_pin))?;
session.init_pin(&user_pin)?;
}
let session = context.open_rw_session(slot)?;
session.login(UserType::User, Some(&user_pin))?;
session.generate_key_pair(
&Mechanism::EccKeyPairGen,
&[
Attribute::Token(true),
Attribute::Label(KEY_LABEL.as_bytes().to_vec()),
Attribute::EcParams(OID_P256.to_vec()),
Attribute::Verify(true),
],
&[
Attribute::Token(true),
Attribute::Private(true),
Attribute::Label(KEY_LABEL.as_bytes().to_vec()),
Attribute::Sign(true),
Attribute::Sensitive(true),
Attribute::Extractable(false),
],
)?;
drop(session);
let pin_path = dir.write("hsm.pin", &format!("{USER_PIN}\n"));
let ca_pem_path = dir.join("ca.pem");
let crl_path = dir.join("ca.crl");
let cfg = LocalCaConfig {
cert_path: ca_pem_path.to_string_lossy().into_owned(),
key_source: "pkcs11".to_string(),
pkcs11: Pkcs11Config {
module_path: module_path.to_string(),
token_label: TOKEN_LABEL.to_string(),
key_label: KEY_LABEL.to_string(),
pin_file: pin_path.to_string_lossy().into_owned(),
..Pkcs11Config::default()
},
..LocalCaConfig::default()
};
let signing_key = Pkcs11SigningKey::open(&cfg)?;
let mut params = CertificateParams::new(Vec::<String>::new())?;
params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
params
.distinguished_name
.push(DnType::CommonName, "acme-proxy softhsm test CA");
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let ca_cert = params.self_signed(&signing_key)?;
std::fs::write(&ca_pem_path, ca_cert.pem())?;
Ok(Lab {
_dir: dir,
module_path,
ca_pem_path,
crl_path,
pin_path,
})
}
fn make_csr_der(name: &str) -> Vec<u8> {
let key_pair = rcgen::KeyPair::generate().unwrap();
let params = CertificateParams::new(vec![name.to_string()]).unwrap();
params.serialize_request(&key_pair).unwrap().der().to_vec()
}
#[tokio::test]
async fn a_token_backed_ca_issues_a_verifiable_leaf() {
let lab = lab_or_skip!();
let ca = LocalCa::load_or_generate(&lab.config(), &crate::signer::CarriedState::new())
.expect("the token-backed CA must load");
let outcome = ca
.issue(
"ord-hsm",
&make_csr_der("example.com"),
&[crate::sqlite::order::Identifier::dns("example.com")],
crate::signer::RequestedValidity::default(),
)
.await
.expect("issuance through the token must succeed");
let chain = match outcome {
crate::signer::IssueOutcome::Issued(chain) => chain,
crate::signer::IssueOutcome::Processing => panic!("local_ca issues synchronously"),
};
assert_eq!(chain.matches("-----BEGIN CERTIFICATE-----").count(), 2);
let leaf_der = crate::cert::leaf_der_from_chain(&chain).unwrap();
let ca_der =
crate::cert::leaf_der_from_chain(&std::fs::read_to_string(&lab.ca_pem_path).unwrap())
.unwrap();
let (_, leaf) = x509_parser::parse_x509_certificate(&leaf_der).unwrap();
let (_, ca_cert) = x509_parser::parse_x509_certificate(&ca_der).unwrap();
leaf.verify_signature(Some(ca_cert.public_key()))
.expect("the leaf must verify against the CA that signed it");
}
#[tokio::test]
async fn a_token_backed_ca_signs_a_verifiable_crl() {
let lab = lab_or_skip!();
let ca =
LocalCa::load_or_generate(&lab.config(), &crate::signer::CarriedState::new()).unwrap();
let outcome = ca
.issue(
"ord-hsm",
&make_csr_der("revoke.example"),
&[crate::sqlite::order::Identifier::dns("revoke.example")],
crate::signer::RequestedValidity::default(),
)
.await
.unwrap();
let crate::signer::IssueOutcome::Issued(chain) = outcome else {
panic!("local_ca issues synchronously");
};
let leaf_der = crate::cert::leaf_der_from_chain(&chain).unwrap();
let (serial_hex, _) = crate::cert::cert_serial_and_spki(&leaf_der).unwrap();
ca.revoke(&leaf_der, Some(1)).await.unwrap();
let crl_der = ca.crl_der().await.expect("a CRL is always present");
use x509_parser::prelude::FromDer;
let (_, crl) =
x509_parser::revocation_list::CertificateRevocationList::from_der(&crl_der).unwrap();
let serials: Vec<String> = crl
.iter_revoked_certificates()
.map(|r| r.raw_serial_as_string().replace(':', ""))
.collect();
assert!(
serials.iter().any(|s| s.eq_ignore_ascii_case(&serial_hex)),
"expected {serial_hex} in {serials:?}",
);
}
#[test]
fn a_key_label_that_matches_nothing_is_a_startup_error() {
let lab = lab_or_skip!();
let mut cfg = lab.config();
cfg.pkcs11.key_label = "not-the-ca-key".to_string();
let error = match LocalCa::load_or_generate(&cfg, &crate::signer::CarriedState::new()) {
Err(error) => error.to_string(),
Ok(_) => panic!("a key label matching nothing must not start the server"),
};
assert!(error.contains("not-the-ca-key"), "{error}");
}
#[test]
fn a_missing_certificate_is_an_error_and_no_ca_is_generated() {
let lab = lab_or_skip!();
let dir = TempDir::new("softhsm-nocert");
let mut cfg = lab.config();
cfg.cert_path = dir.join("absent.pem").to_string_lossy().into_owned();
cfg.key_path = dir.join("absent.key").to_string_lossy().into_owned();
let error = match LocalCa::load_or_generate(&cfg, &crate::signer::CarriedState::new()) {
Err(error) => error.to_string(),
Ok(_) => panic!("pkcs11 mode must not generate a CA"),
};
assert!(error.contains("absent.pem"), "{error}");
assert!(error.contains("does not generate"), "{error}");
assert!(!dir.join("absent.pem").exists(), "no CA may be written");
assert!(!dir.join("absent.key").exists(), "no key may be written");
}
#[test]
fn a_wrong_pin_fails_at_startup() {
let lab = lab_or_skip!();
let dir = TempDir::new("softhsm-badpin");
let mut cfg = lab.config();
cfg.pkcs11.pin_file = dir
.write("hsm.pin", "9999\n")
.to_string_lossy()
.into_owned();
let error = match LocalCa::load_or_generate(&cfg, &crate::signer::CarriedState::new()) {
Err(error) => error.to_string(),
Ok(_) => panic!("a wrong PIN must not start the server"),
};
assert!(error.contains("login failed"), "{error}");
}
#[tokio::test]
async fn the_file_key_source_still_works_with_the_feature_on() {
let dir = TempDir::new("softhsm-file");
let cfg = LocalCaConfig {
cert_path: dir.join("ca.pem").to_string_lossy().into_owned(),
key_path: dir.join("ca.key").to_string_lossy().into_owned(),
crl_path: dir.join("ca.crl").to_string_lossy().into_owned(),
..LocalCaConfig::default()
};
assert_eq!(cfg.key_source, "file", "the default must not have moved");
let ca = LocalCa::load_or_generate(&cfg, &crate::signer::CarriedState::new()).unwrap();
let outcome = ca
.issue(
"ord-file",
&make_csr_der("example.com"),
&[crate::sqlite::order::Identifier::dns("example.com")],
crate::signer::RequestedValidity::default(),
)
.await
.unwrap();
assert!(matches!(
outcome,
crate::signer::IssueOutcome::Issued(chain) if chain.matches("BEGIN CERTIFICATE").count() == 2
));
}
#[test]
fn a_second_backend_over_the_same_module_opens_fine() {
let lab = lab_or_skip!();
let first =
LocalCa::load_or_generate(&lab.config(), &crate::signer::CarriedState::new()).unwrap();
let second =
LocalCa::load_or_generate(&lab.config(), &crate::signer::CarriedState::new()).unwrap();
drop((first, second));
}
}