server/lib/tests/the_evidence_signer_lives_in_an_hsm.rs
//! ★ **The evidence signer's key lives in a hardware token** (lane HOL-NCSC, NCSC P3): with
//! `attestation.signer_hsm: Some(Pkcs11(..))` Holger signs its DSSE evidence and audit
//! checkpoints through a PKCS#11 module; the key never leaves the token, and what it signs
//! verifies against the certificate and the root like a software key's. The PIN file is a
//! key file (`holger-key-1` at 0644). A build without `pkcs11` refuses the block by name
//! (`holger-key-2`).
//!
//! The token is SoftHSM2 (`softhsm2-util`, `libsofthsm2.so`). Where it is not installed the
//! signing test says so by name and does not run; `HOLGER_PKCS11_REQUIRED=1` (the gate's
//! SoftHSM container) makes that a failure.
use std::path::Path;
use holger_server_lib::read_ron_config;
fn ron(dir: &Path, module: &str, pin_file: &Path, cert: &Path, root: &Path) -> String {
format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
audit: (dir: "{a}", enabled: true),
attestation: Some((
trust_roots: ["{root}"],
signer_hsm: Some(Pkcs11(module: "{module}", token_label: "holger", key_label: "scan", pin_file: "{pin}")),
signer_cert: Some("{cert}"),
signer_alg: Some("ecdsa-p256"),
)),
)"#,
a = dir.join("audit").display(),
root = root.display(),
pin = pin_file.display(),
cert = cert.display(),
)
}
fn load(dir: &Path, text: &str) -> holger_server_lib::Holger {
let p = dir.join("h.ron");
std::fs::write(&p, text).unwrap();
read_ron_config(&p).expect("RON parses")
}
fn chmod(p: &Path, m: u32) {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(p, std::fs::Permissions::from_mode(m)).unwrap();
}
#[cfg(not(feature = "pkcs11"))]
#[test]
fn a_build_without_pkcs11_refuses_the_hsm_signer_by_name() {
if !cfg!(all(feature = "intake", feature = "sign")) {
return;
}
let d = tempfile::tempdir().unwrap();
let pin = d.path().join("pin");
std::fs::write(&pin, "1234").unwrap();
chmod(&pin, 0o600);
std::fs::write(d.path().join("leaf.der"), b"x").unwrap();
std::fs::write(d.path().join("root.der"), b"x").unwrap();
let h = load(
d.path(),
&ron(
d.path(),
"/usr/lib/softhsm/libsofthsm2.so",
&pin,
&d.path().join("leaf.der"),
&d.path().join("root.der"),
),
);
#[cfg(all(feature = "intake", feature = "sign"))]
{
let e = format!(
"{:#}",
h.attestation_signer()
.err()
.expect("refused without pkcs11")
);
assert!(e.contains("[holger-key-2]") && e.contains("pkcs11"), "{e}");
}
let _ = h;
}
/// The SoftHSM2 module, if this host has one.
#[cfg(feature = "pkcs11")]
fn softhsm() -> Option<std::path::PathBuf> {
let found = [
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
"/usr/local/lib/softhsm/libsofthsm2.so",
]
.iter()
.map(std::path::PathBuf::from)
.find(|p| p.is_file());
let tool = std::process::Command::new("softhsm2-util")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success());
match (found, tool) {
(Some(m), true) => Some(m),
_ => {
assert!(std::env::var("HOLGER_PKCS11_REQUIRED").is_err(), "HOLGER_PKCS11_REQUIRED is set and this host has no SoftHSM2 (softhsm2-util + libsofthsm2.so)");
eprintln!("SKIPPED the_evidence_signer_signs_through_pkcs11: no SoftHSM2 here (install softhsm2, or run the gate's SoftHSM container)");
None
}
}
}
#[cfg(feature = "pkcs11")]
#[test]
fn the_evidence_signer_signs_through_pkcs11_and_verifies() {
use holger_server_lib::audit::chain::CheckpointSigner as _;
use rcgen::{
BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, KeyUsagePurpose,
PKCS_ECDSA_P256_SHA256,
};
let Some(module) = softhsm() else { return };
let d = tempfile::tempdir().unwrap();
// A token of our own: SoftHSM2 keeps it under the directory its config names.
let tokens = d.path().join("tokens");
std::fs::create_dir_all(&tokens).unwrap();
let conf = d.path().join("softhsm2.conf");
std::fs::write(
&conf,
format!(
"directories.tokendir = {}\nobjectstore.backend = file\nlog.level = ERROR\n",
tokens.display()
),
)
.unwrap();
std::env::set_var("SOFTHSM2_CONF", &conf);
let run = |args: &[&str]| {
let o = std::process::Command::new("softhsm2-util")
.args(args)
.output()
.unwrap();
assert!(
o.status.success(),
"softhsm2-util {args:?}: {}",
String::from_utf8_lossy(&o.stderr)
);
};
run(&[
"--init-token",
"--free",
"--label",
"holger",
"--pin",
"1234",
"--so-pin",
"5678",
]);
// The evidence PKI: a root and a leaf; the leaf's key goes into the token.
let ca_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut ca = CertificateParams::new(vec![]).unwrap();
ca.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
];
let ca_cert = ca.self_signed(&ca_key).unwrap();
let issuer = Issuer::new(ca, ca_key);
let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut leaf = CertificateParams::new(vec![]).unwrap();
leaf.key_usages = vec![KeyUsagePurpose::DigitalSignature];
let leaf_cert = leaf.signed_by(&leaf_key, &issuer).unwrap();
let (root, cert, pem, pin) = (
d.path().join("root.der"),
d.path().join("leaf.der"),
d.path().join("leaf.pem"),
d.path().join("pin"),
);
std::fs::write(&root, ca_cert.der()).unwrap();
std::fs::write(&cert, leaf_cert.der()).unwrap();
std::fs::write(&pem, leaf_key.serialize_pem()).unwrap();
run(&[
"--import",
pem.to_str().unwrap(),
"--token",
"holger",
"--label",
"scan",
"--id",
"01",
"--pin",
"1234",
]);
std::fs::remove_file(&pem).unwrap();
std::fs::write(&pin, "1234\n").unwrap();
// The PIN file is a key file.
chmod(&pin, 0o644);
let h = load(
d.path(),
&ron(d.path(), module.to_str().unwrap(), &pin, &cert, &root),
);
let e = format!(
"{:#}",
h.attestation_signer()
.err()
.expect("a PIN others can read is refused")
);
assert!(
e.contains("[holger-key-1]") && e.contains("pin_file"),
"{e}"
);
chmod(&pin, 0o600);
let h = load(
d.path(),
&ron(d.path(), module.to_str().unwrap(), &pin, &cert, &root),
);
let signer = h
.attestation_signer()
.expect("the HSM signer opens")
.expect("a signer");
let signed = signer
.sign_checkpoint("application/vnd.holger.test+json", b"{\"n\":1}")
.expect("the token signs");
let env = znippy_common::sign::DsseEnvelope::from_json(&signed.envelope_json).unwrap();
let store = znippy_common::sign::CertStore::from_der_certs(&[ca_cert.der().to_vec()]).unwrap();
let ok = znippy_common::sign::dsse_verify(&env, &[leaf_cert.der().to_vec()], &store)
.expect("what the token signed verifies");
assert!(!ok.is_empty());
// A wrong label is refused by name, never a silent software fallback.
let bad = ron(d.path(), module.to_str().unwrap(), &pin, &cert, &root)
.replace("key_label: \"scan\"", "key_label: \"nope\"");
let e = format!(
"{:#}",
load(d.path(), &bad)
.attestation_signer()
.err()
.expect("no such key")
);
assert!(e.contains("[holger-key-2]") && e.contains("nope"), "{e}");
}