server/lib/tests/key_files_are_the_processs_alone.rs
//! ★ **A private key file is the process's alone** (lane HOL-NCSC, NCSC P3): a key file
//! Holger loads (a door's TLS key, the evidence signer's key, a static token's file) whose
//! mode grants its group or others any bit, or that another user owns, refuses the load by
//! name (`holger-key-1`); the same file at 0600 loads. The one other shape accepted is a
//! Kubernetes secret volume (owned by root, its group the process's own, at most group-read).
use std::os::unix::fs::PermissionsExt as _;
use std::path::Path;
fn mode(p: &Path, m: u32) {
std::fs::set_permissions(p, std::fs::Permissions::from_mode(m)).unwrap();
}
#[cfg(all(feature = "intake", feature = "sign"))]
fn load(dir: &Path, ron: &str) -> holger_server_lib::Holger {
let p = dir.join("h.ron");
std::fs::write(&p, ron).unwrap();
holger_server_lib::read_ron_config(&p).expect("RON parses")
}
#[test]
fn the_keyfile_check_refuses_group_or_other_bits() {
use holger_server_lib::keyfile::read_private;
let d = tempfile::tempdir().unwrap();
let k = d.path().join("k.pk8");
std::fs::write(&k, b"secret").unwrap();
for m in [0o644, 0o640, 0o604, 0o660, 0o611] {
mode(&k, m);
let e = read_private(&k, "the test key").unwrap_err().to_string();
assert!(
e.contains("[holger-key-1]")
&& e.contains("the test key")
&& e.contains(&format!("{m:o}")),
"{m:o}: {e}"
);
}
for m in [0o600, 0o400] {
mode(&k, m);
assert_eq!(read_private(&k, "the test key").unwrap(), b"secret");
}
}
#[test]
fn a_doors_tls_key_readable_by_others_refuses_the_door() {
let d = tempfile::tempdir().unwrap();
let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let (cert, key) = (d.path().join("tls.crt"), d.path().join("tls.key"));
std::fs::write(&cert, ck.cert.pem()).unwrap();
std::fs::write(&key, ck.signing_key.serialize_pem()).unwrap();
mode(&key, 0o644);
let tls = holger_server_lib::exposed::tls::TlsSettings {
cert: cert.display().to_string(),
key: key.display().to_string(),
client_ca: None,
client_auth_optional: false,
crl: None,
};
for e in [
tls.rustls_gateway_config().err().map(|e| format!("{e:#}")),
tls.rustls_grpc_config().err().map(|e| format!("{e:#}")),
tls.tonic_config().err().map(|e| format!("{e:#}")),
] {
let e = e.expect("a key others can read refuses the door");
assert!(e.contains("[holger-key-1]"), "{e}");
}
mode(&key, 0o600);
tls.rustls_gateway_config().expect("a 0600 key serves");
}
#[cfg(all(feature = "intake", feature = "sign"))]
struct Pki {
root_der: std::path::PathBuf,
cert_der: std::path::PathBuf,
key_pkcs8: std::path::PathBuf,
}
#[cfg(all(feature = "intake", feature = "sign"))]
/// An evidence signer: a root and a leaf (ECDSA P-256), the leaf's key as PKCS#8.
fn pki(dir: &Path) -> Pki {
use rcgen::{
BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, PKCS_ECDSA_P256_SHA256,
};
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);
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 leaf_cert = CertificateParams::new(vec![])
.unwrap()
.signed_by(&leaf_key, &issuer)
.unwrap();
let p = Pki {
root_der: dir.join("root.der"),
cert_der: dir.join("scan.der"),
key_pkcs8: dir.join("scan.pk8"),
};
std::fs::write(&p.root_der, ca_cert.der()).unwrap();
std::fs::write(&p.cert_der, leaf_cert.der()).unwrap();
std::fs::write(&p.key_pkcs8, leaf_key.serialize_der()).unwrap();
p
}
#[cfg(all(feature = "intake", feature = "sign"))]
#[test]
fn the_evidence_signers_key_and_a_token_file_readable_by_others_are_refused() {
let d = tempfile::tempdir().unwrap();
let pki = pki(d.path());
let token = d.path().join("admin-token");
std::fs::write(&token, "a-token-of-sufficient-length-0123456789").unwrap();
let ron = format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
audit: (dir: "{a}", enabled: true),
auth: Some(( methods: [ StaticToken(subject: "ops", token_file: Some("{t}")) ], roles: {{"ops": Admin}} )),
attestation: Some((trust_roots: ["{root}"], signer_key: Some("{key}"), signer_cert: Some("{cert}"), signer_alg: Some("ecdsa-p256"))),
)"#,
a = d.path().join("audit").display(),
t = token.display(),
root = pki.root_der.display(),
key = pki.key_pkcs8.display(),
cert = pki.cert_der.display(),
);
mode(&pki.key_pkcs8, 0o644);
mode(&token, 0o644);
let h = load(d.path(), &ron);
let e = format!(
"{:#}",
match h.attestation_signer() {
Ok(_) => panic!("the signer key others can read is refused"),
Err(e) => e,
}
);
assert!(
e.contains("[holger-key-1]") && e.contains("attestation.signer_key"),
"{e}"
);
let e = format!(
"{:#}",
h.validate_auth_posture()
.expect_err("a token file others can read is refused")
);
assert!(
e.contains("[holger-key-1]") && e.contains("token_file"),
"{e}"
);
mode(&pki.key_pkcs8, 0o600);
mode(&token, 0o600);
let h = load(d.path(), &ron);
assert!(h.attestation_signer().unwrap().is_some());
h.validate_auth_posture().expect("a 0600 token file");
}