use std::path::Path;
use rcgen::{KeyPair, PublicKeyData, SignatureAlgorithm, SigningKey};
use crate::config::LocalCaConfig;
pub enum CaSigningKey {
Software(KeyPair),
#[cfg(feature = "hsm")]
Pkcs11(super::pkcs11::Pkcs11SigningKey),
}
impl PublicKeyData for CaSigningKey {
fn der_bytes(&self) -> &[u8] {
match self {
Self::Software(key) => key.der_bytes(),
#[cfg(feature = "hsm")]
Self::Pkcs11(key) => key.der_bytes(),
}
}
fn algorithm(&self) -> &'static SignatureAlgorithm {
match self {
Self::Software(key) => key.algorithm(),
#[cfg(feature = "hsm")]
Self::Pkcs11(key) => key.algorithm(),
}
}
}
impl SigningKey for CaSigningKey {
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, rcgen::Error> {
match self {
Self::Software(key) => key.sign(msg),
#[cfg(feature = "hsm")]
Self::Pkcs11(key) => key.sign(msg),
}
}
}
impl std::fmt::Debug for CaSigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Software(_) => f.write_str("CaSigningKey::Software([elided])"),
#[cfg(feature = "hsm")]
Self::Pkcs11(key) => write!(f, "CaSigningKey::Pkcs11({key:?})"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySource {
File,
Pkcs11,
}
impl KeySource {
pub fn parse(value: &str) -> anyhow::Result<Self> {
match value {
"file" => Ok(Self::File),
"pkcs11" => Ok(Self::Pkcs11),
other => anyhow::bail!(
"unsupported local_ca key_source: `{other}` (expected \"file\" or \"pkcs11\")"
),
}
}
}
#[cfg_attr(not(feature = "hsm"), allow(dead_code))]
pub fn read_pin(cfg: &LocalCaConfig) -> anyhow::Result<String> {
let pkcs11 = &cfg.pkcs11;
if !pkcs11.pin_file.is_empty() {
let path = Path::new(&pkcs11.pin_file);
crate::pemfile::warn_if_key_is_readable("local_ca_pkcs11_pin_permissive", path);
let raw = std::fs::read_to_string(path).map_err(|error| {
anyhow::anyhow!(
"local_ca pkcs11 pin_file `{}` could not be read: {error}",
pkcs11.pin_file
)
})?;
let pin = raw.trim_end().to_string();
if pin.is_empty() {
anyhow::bail!("local_ca pkcs11 pin_file `{}` is empty", pkcs11.pin_file);
}
return Ok(pin);
}
if !pkcs11.pin.is_empty() {
return Ok(pkcs11.pin.clone());
}
anyhow::bail!(
"local_ca key_source = \"pkcs11\" needs a user PIN: set \
signer.local_ca.pkcs11.pin_file, or the \
ACME_PROXY_SIGNER__LOCAL_CA__PKCS11__PIN environment variable"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Pkcs11Config;
use crate::testutil::TempDir;
use rcgen::{CertificateParams, Issuer};
fn config_with(pkcs11: Pkcs11Config) -> LocalCaConfig {
LocalCaConfig {
pkcs11,
..LocalCaConfig::default()
}
}
#[test]
fn key_source_parses_the_two_known_values() {
assert_eq!(KeySource::parse("file").unwrap(), KeySource::File);
assert_eq!(KeySource::parse("pkcs11").unwrap(), KeySource::Pkcs11);
}
#[test]
fn an_unknown_key_source_is_an_error_naming_it() {
let error = KeySource::parse("hsm").unwrap_err().to_string();
assert!(error.contains("hsm"), "{error}");
assert!(error.contains("pkcs11"), "{error}");
}
#[test]
fn the_software_variant_signs_exactly_as_the_bare_key_pair_does() {
let key_pair = KeyPair::generate().unwrap();
let wrapped = CaSigningKey::Software(KeyPair::from_pem(&key_pair.serialize_pem()).unwrap());
assert_eq!(wrapped.der_bytes(), key_pair.der_bytes());
assert_eq!(
wrapped.subject_public_key_info(),
key_pair.subject_public_key_info()
);
assert_eq!(wrapped.algorithm(), key_pair.algorithm());
let mut ca_params = CertificateParams::new(Vec::<String>::new()).unwrap();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
let ca_cert = ca_params.self_signed(&wrapped).unwrap();
let issuer = Issuer::new(ca_params, wrapped);
let leaf_key = KeyPair::generate().unwrap();
let csr = CertificateParams::new(vec!["example.com".to_string()])
.unwrap()
.serialize_request(&leaf_key)
.unwrap();
let parsed =
rcgen::CertificateSigningRequestParams::from_der(&csr.der().to_vec().into()).unwrap();
let leaf = parsed.signed_by(&issuer).unwrap();
let (_, parsed_leaf) = x509_parser::parse_x509_certificate(leaf.der()).unwrap();
let (_, parsed_ca) = x509_parser::parse_x509_certificate(ca_cert.der()).unwrap();
assert!(
parsed_leaf
.verify_signature(Some(parsed_ca.public_key()))
.is_ok()
);
}
#[test]
fn debug_never_renders_key_material() {
let key = CaSigningKey::Software(KeyPair::generate().unwrap());
let rendered = format!("{key:?}");
assert!(rendered.contains("elided"), "{rendered}");
}
#[test]
fn a_pin_file_wins_over_the_pin_field() {
let dir = TempDir::new("pin");
let path = dir.write("hsm.pin", "from-file\n");
let cfg = config_with(Pkcs11Config {
pin: "from-config".to_string(),
pin_file: path.to_string_lossy().into_owned(),
..Pkcs11Config::default()
});
assert_eq!(read_pin(&cfg).unwrap(), "from-file");
}
#[test]
fn a_trailing_newline_is_not_part_of_the_pin() {
let dir = TempDir::new("pin");
for written in ["1234\n", "1234\r\n", "1234"] {
let path = dir.write("hsm.pin", written);
let cfg = config_with(Pkcs11Config {
pin_file: path.to_string_lossy().into_owned(),
..Pkcs11Config::default()
});
assert_eq!(read_pin(&cfg).unwrap(), "1234", "for {written:?}");
}
}
#[test]
fn the_pin_field_is_used_when_no_file_is_configured() {
let cfg = config_with(Pkcs11Config {
pin: "1234".to_string(),
..Pkcs11Config::default()
});
assert_eq!(read_pin(&cfg).unwrap(), "1234");
}
#[test]
fn no_pin_at_all_is_an_error_naming_both_ways_to_set_one() {
let error = read_pin(&config_with(Pkcs11Config::default()))
.unwrap_err()
.to_string();
assert!(error.contains("pin_file"), "{error}");
assert!(
error.contains("ACME_PROXY_SIGNER__LOCAL_CA__PKCS11__PIN"),
"{error}"
);
}
#[test]
fn a_missing_pin_file_is_an_error_naming_the_path() {
let cfg = config_with(Pkcs11Config {
pin_file: "/nonexistent/acme-proxy/hsm.pin".to_string(),
..Pkcs11Config::default()
});
let error = read_pin(&cfg).unwrap_err().to_string();
assert!(error.contains("/nonexistent/acme-proxy/hsm.pin"), "{error}");
}
#[test]
fn an_empty_pin_file_is_an_error() {
let dir = TempDir::new("pin");
let path = dir.write("hsm.pin", "\n");
let cfg = config_with(Pkcs11Config {
pin_file: path.to_string_lossy().into_owned(),
..Pkcs11Config::default()
});
assert!(read_pin(&cfg).is_err());
}
}