Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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");
}