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/the_estate_declaration_runs_the_intake_chain.rs

//! **The declaration every Holger estate ships runs the security gateway** (lane W2).
//!
//! The server boots holger-recipe's `estate_ron::medium()` — the very file
//! `cargo xtask estate-media` stages on the `holger` and `holger-keep` media — with
//! its paths moved into a temporary directory and its binds on loopback, and its
//! `{estate}` filled in as the box fills it from the installer's feed. Then, over
//! the real doors:
//!
//! * the console principal (the fed static token) ingests a wheel into `incoming`
//!   and walks it to POLICY_CHECKED through the REQUIRED scan (vulnerability
//!   against the offline intel database, sbom);
//! * the console never approves its own import;
//! * the console mints two invites into the approver group, two people enrol with
//!   a passkey and log in, and their two distinct approvals make it APPROVED
//!   (`approvals: 2`);
//! * the console seals it onward to AVAILABLE, production serves the bytes and
//!   lists them in the PEP 503 index;
//! * the audit chain verifies, and the korp root the medium stages as PEM is the
//!   DER the module verifier reads.
//!
//! The passkey method is added exactly as a paired box adds it from the feed's
//! `passkey_rp_id` (server-cli `paired::passkey_from_feed`).

use std::io::Write;
use traits::test_support::hold_port;

use holger_recipe::estate_ron;
use holger_server_lib::auth::AuthMethodConfig;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use nornir_auth_webauthn::softkey::SoftKey;

const ESTATE: &str = "test3.holger.rs";
const ORIGIN: &str = "https://test3.holger.rs";
const CONSOLE_TOKEN: &str = "console-bearer-of-sufficient-length-0123456789";
const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";

struct Running {
    data: tempfile::TempDir,
    _cfg: tempfile::NamedTempFile,
    holger: Holger,
    base: String,
}

fn start() -> Running {
    let mut last = String::new();
    for _ in 0..8 {
        let (grpc, http) = (hold_port(), hold_port());
        match try_start(grpc, http) {
            Ok(r) => return r,
            Err(e) => last = e,
        }
    }
    panic!("server never started; last error: {last}");
}

/// The intel database a box gets by import, installed by hand: no advisory for
/// the demo wheel, a fresh snapshot.
fn install_intel(dir: &std::path::Path) {
    use holger_intel::chrono::Utc;
    use holger_intel::{AdvisorySet, InstalledIntel, LocalIntelDb};
    let set = AdvisorySet {
        schema: holger_intel::set::SET_SCHEMA.into(),
        generated_at: Utc::now(),
        sources: vec![],
        advisories: vec![],
    };
    let db = LocalIntelDb::open(dir);
    let bytes = set.to_json_bytes().unwrap();
    db.stage_generation(1, &bytes).unwrap();
    db.activate(&InstalledIntel {
        schema: holger_intel::db::INSTALLED_SCHEMA.into(),
        bundle_id: "holger-intel-1".into(),
        sequence: 1,
        supplier: holger_intel::DEFAULT_SUPPLIER.into(),
        snapshot_at: Utc::now(),
        imported_at: Utc::now(),
        candidate_digest: "00".into(),
        release_signer_fingerprint: "00".into(),
        advisories_file: LocalIntelDb::generation_file(1),
        advisories_sha256: holger_server_lib::security::blob_digest_hex(&bytes),
        advisories: 0,
    })
    .unwrap();
}

fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
    let data = tempfile::tempdir().unwrap();
    let d = data.path().display().to_string();
    // The medium's own file, its paths moved under the temporary directory.
    let medium = estate_ron::medium();
    let ron = medium
        .replace(
            &format!("\"{}/", holger_recipe::packaging::BOX_HOLGER_DATA),
            &format!("\"{d}/holger/"),
        )
        .replace(
            &format!("\"{}/", estate_ron::BOX_APPLIANCE_ROOT),
            &format!("\"{d}/"),
        )
        .replace(
            &format!("\"{}/", estate_ron::RELEASE_ROOT),
            &format!("\"{d}/"),
        )
        // A test build scans in-process and runs no clamd; the shipped declaration keeps the
        // defaults (`sandbox: required`, ClamAV required). The medium already carries the
        // `rules` key, so it is REPLACED, never added beside it (RON refuses a duplicate key).
        .replace("rules: (sandbox: required),", "rules: (sandbox: off),")
        .replace("required: true)))", "required: false)))")
        .replace("\"0.0.0.0:50051\"", &format!("\"127.0.0.1:{grpc}\""))
        .replace("\"0.0.0.0:8443\"", &format!("\"127.0.0.1:{http}\""));
    assert!(!ron.contains("\"/data"), "every path moved:\n{ron}");
    // What the installer feeds and the medium stages.
    std::fs::write(
        data.path().join(estate_ron::CONSOLE_TOKEN_FILE),
        CONSOLE_TOKEN,
    )
    .unwrap();
    std::fs::set_permissions(
        data.path().join(estate_ron::CONSOLE_TOKEN_FILE),
        <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o600),
    )
    .unwrap();
    std::fs::write(
        data.path().join(estate_ron::KORP_ROOT_STAGED),
        estate_ron::KORP_ROOT_PEM,
    )
    .unwrap();
    install_intel(&data.path().join("holger/intel"));
    let mut cfg = tempfile::NamedTempFile::new().unwrap();
    cfg.write_all(ron.as_bytes()).unwrap();
    let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e:#}"))?;
    // The box: the estate's name from the join reply, the passkey plane from the feed.
    holger.resolve_estate(Some(ESTATE));
    holger
        .auth
        .as_mut()
        .expect("the estate declares auth")
        .methods
        .push(AuthMethodConfig::Passkey {
            rp_id: ESTATE.into(),
            rp_origin: ORIGIN.into(),
            rp_name: "holger".into(),
            session_key_file: None,
            bootstrap_until_first_admin: true,
        });
    holger
        .instantiate_backends()
        .map_err(|e| format!("backends: {e:#}"))?;
    wire_holger(&mut holger).map_err(|e| format!("wire: {e:#}"))?;
    holger.start().map_err(|e| format!("start: {e:#}"))?;
    Ok(Running {
        data,
        _cfg: cfg,
        holger,
        base: format!("http://127.0.0.1:{http}"),
    })
}

async fn wait_healthy(c: &reqwest::Client, base: &str) {
    for _ in 0..200 {
        if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
            if r.status() == 200 {
                return;
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("gateway never became healthy at {base}");
}

async fn post(
    c: &reqwest::Client,
    base: &str,
    path: &str,
    bearer: Option<&str>,
    body: serde_json::Value,
) -> (u16, serde_json::Value) {
    let mut req = c.post(format!("{base}{path}")).json(&body);
    if let Some(b) = bearer {
        req = req.bearer_auth(b);
    }
    let r = req.send().await.expect("the door answers");
    let status = r.status().as_u16();
    let text = r.text().await.unwrap_or_default();
    (
        status,
        serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)),
    )
}

/// An invite into the approver group, minted by `by` (a person's stepped-up session: the
/// console's machine token invites no approver, HOL-NCSC), redeemed by a new person's
/// passkey, and a user-verified login: the approver's session token.
async fn enrol_approver(c: &reqwest::Client, base: &str, by: &str, name: &str, seed: u8) -> String {
    let (status, minted) = post(
        c,
        base,
        "/-/auth/passkey/invites",
        Some(by),
        serde_json::json!({ "display": name, "groups": [estate_ron::APPROVER_GROUP] }),
    )
    .await;
    assert_eq!(status, 200, "the admin mints an approver invite: {minted}");
    assert_eq!(
        minted["groups"],
        serde_json::json!([estate_ron::APPROVER_GROUP])
    );
    let secret = minted["invite"]
        .as_str()
        .expect("the invite secret")
        .to_owned();
    let mut key = SoftKey::new(seed, format!("cred-{name}-0001").as_bytes());
    let (status, start) = post(
        c,
        base,
        "/-/auth/passkey/register/start",
        None,
        serde_json::json!({ "display": name, "invite": secret }),
    )
    .await;
    assert_eq!(status, 200, "{name} starts enrolment: {start}");
    let ceremony = start["ceremony_id"].as_str().unwrap().to_owned();
    let (client_data, attestation) = key.register(
        ESTATE,
        ORIGIN,
        start["publicKey"]["challenge"].as_str().unwrap(),
        true,
    );
    let (status, reg) = post(
        c,
        base,
        "/-/auth/passkey/register/finish",
        None,
        serde_json::json!({ "ceremony_id": ceremony, "id": key.credential_id_b64(), "response": { "clientDataJSON": client_data, "attestationObject": attestation } }),
    )
    .await;
    assert_eq!(status, 200, "{name} enrols with the invite: {reg}");
    let (_, start) = post(
        c,
        base,
        "/-/auth/passkey/login/start",
        None,
        serde_json::json!({ "uv": "required" }),
    )
    .await;
    let ceremony = start["ceremony_id"].as_str().unwrap().to_owned();
    let (client_data, auth_data, signature) = key.authenticate(
        ESTATE,
        ORIGIN,
        start["publicKey"]["challenge"].as_str().unwrap(),
        true,
    );
    let (status, out) = post(
        c,
        base,
        "/-/auth/passkey/login/finish",
        None,
        serde_json::json!({ "ceremony_id": ceremony, "id": key.credential_id_b64(), "response": { "clientDataJSON": client_data, "authenticatorData": auth_data, "signature": signature, "userHandle": null } }),
    )
    .await;
    assert_eq!(status, 200, "{name} logs in: {out}");
    out["token"].as_str().expect("a session token").to_owned()
}

/// The first admin, enrolled by the console's bearer (the box's first-admin flow), and a
/// user-verified login: the admin's session token.
async fn enrol_first_admin(c: &reqwest::Client, base: &str, name: &str, seed: u8) -> String {
    let mut key = SoftKey::new(seed, format!("cred-{name}-0001").as_bytes());
    let (status, start) = post(
        c,
        base,
        "/-/auth/passkey/register/start",
        Some(CONSOLE_TOKEN),
        serde_json::json!({ "display": name, "role": "admin" }),
    )
    .await;
    assert_eq!(status, 200, "the console enrols the first admin: {start}");
    let ceremony = start["ceremony_id"].as_str().unwrap().to_owned();
    let (client_data, attestation) = key.register(
        ESTATE,
        ORIGIN,
        start["publicKey"]["challenge"].as_str().unwrap(),
        true,
    );
    let (status, reg) = post(
        c,
        base,
        "/-/auth/passkey/register/finish",
        None,
        serde_json::json!({ "ceremony_id": ceremony, "id": key.credential_id_b64(), "response": { "clientDataJSON": client_data, "attestationObject": attestation } }),
    )
    .await;
    assert_eq!(status, 200, "{name} enrols: {reg}");
    let (_, start) = post(
        c,
        base,
        "/-/auth/passkey/login/start",
        None,
        serde_json::json!({ "uv": "required" }),
    )
    .await;
    let ceremony = start["ceremony_id"].as_str().unwrap().to_owned();
    let (client_data, auth_data, signature) = key.authenticate(
        ESTATE,
        ORIGIN,
        start["publicKey"]["challenge"].as_str().unwrap(),
        true,
    );
    let (status, out) = post(
        c,
        base,
        "/-/auth/passkey/login/finish",
        None,
        serde_json::json!({ "ceremony_id": ceremony, "id": key.credential_id_b64(), "response": { "clientDataJSON": client_data, "authenticatorData": auth_data, "signature": signature, "userHandle": null } }),
    )
    .await;
    assert_eq!(status, 200, "{name} logs in: {out}");
    out["token"].as_str().expect("a session token").to_owned()
}

/// `POST /-/intake/advance` as `bearer`: (status, body).
async fn advance(c: &reqwest::Client, base: &str, bearer: &str) -> (u16, String) {
    let r = c
        .post(format!("{base}/-/intake/advance?name={WHEEL}&verb=advance"))
        .bearer_auth(bearer)
        .send()
        .await
        .unwrap();
    (r.status().as_u16(), r.text().await.unwrap_or_default())
}

#[tokio::test(flavor = "multi_thread")]
async fn the_shipped_estate_declaration_runs_the_intake_chain_end_to_end() {
    let r = start();
    let c = tls_roots(reqwest::Client::builder())
        .build()
        .expect("reqwest client");
    wait_healthy(&c, &r.base).await;

    // ── what the declaration turned on ──
    let crates = r
        .holger
        .repositories
        .iter()
        .find(|x| x.ron_name == "crates")
        .expect("the crates repository");
    assert_eq!(
        crates.ron_base_url.as_deref(),
        Some("https://repo.test3.holger.rs"),
        "the estate's own name, fed"
    );
    let chain = r
        .holger
        .intake()
        .expect("the declaration declares the intake chain");
    let der = std::fs::read(r.data.path().join("holger/plugins/trust/korp-root-ca.der"))
        .expect("the staged PEM became DER");
    assert_eq!(
        holger_server_lib::security::blob_digest_hex(&der),
        "6b19bf479c9d00594f1a1504f0ca7d93da81b0e4f03187a3254fa11248570b16",
        "the published korp root, the ONE box's [plugin_root] pin"
    );
    assert_eq!(
        r.holger.plugin_trust_roots(),
        vec![r.data.path().join("holger/plugins/trust/korp-root-ca.der")]
    );

    // ── the console imports; nothing serves an INGESTED wheel ──
    let wheel = wheel_bytes();
    let put = c
        .put(format!("{}/incoming/{WHEEL}", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .body(wheel.clone())
        .send()
        .await
        .unwrap();
    assert_eq!(
        put.status(),
        201,
        "the console ingests: {}",
        put.text().await.unwrap_or_default()
    );
    // The shipped declaration gates reads (HCON-5, `access: (reads: Gated)`): an anonymous
    // reader is refused by name, a signed-in one reads.
    let anon = c
        .get(format!("{}/production/{WHEEL}", r.base))
        .send()
        .await
        .unwrap();
    assert_eq!(anon.status(), 401, "the estate medium gates reads");
    assert!(
        anon.text().await.unwrap().starts_with("[read-gated]"),
        "refused by name"
    );
    let get = c
        .get(format!("{}/production/{WHEEL}", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(get.status(), 404, "production serves nothing INGESTED");
    let put = c
        .put(format!("{}/production/{WHEEL}", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .body(wheel.clone())
        .send()
        .await
        .unwrap();
    assert_eq!(put.status(), 403, "nothing is written past the chain");

    // ── the required scan at SCANNED, then POLICY_CHECKED ──
    for want in ["SCANNED", "POLICY_CHECKED"] {
        let (status, body) = advance(&c, &r.base, CONSOLE_TOKEN).await;
        assert_eq!(status, 200, "→ {want}: {body}");
        assert!(body.contains(want), "→ {want}: {body}");
    }
    let state = c
        .get(format!("{}/-/intake/state?name={WHEEL}", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .send()
        .await
        .unwrap()
        .text()
        .await
        .unwrap();
    for kind in ["vulnerability", "sbom"] {
        assert!(
            state.contains(kind),
            "the SCANNED entry carries {kind} evidence: {state}"
        );
    }

    // ── one console login can approve nothing ──
    let (status, body) = advance(&c, &r.base, CONSOLE_TOKEN).await;
    assert_eq!(status, 403, "the console never approves: {body}");
    assert!(
        body.starts_with("[holger-duty-1]") || body.starts_with("[holger-duty-2]"),
        "{body}"
    );

    // ── two invited approvers: one is not enough, two are ──
    // The console's machine token invites no approver (HOL-NCSC, `holger-auth-6`); it enrols
    // the first admin, whose stepped-up session invites them.
    let (status, out) = post(
        &c,
        &r.base,
        "/-/auth/passkey/invites",
        Some(CONSOLE_TOKEN),
        serde_json::json!({ "display": "anna", "groups": [estate_ron::APPROVER_GROUP] }),
    )
    .await;
    assert_eq!(status, 403, "the console mints no approver invite: {out}");
    assert!(out.to_string().contains("[holger-auth-6]"), "{out}");
    let admin = enrol_first_admin(&c, &r.base, "olga", 0x4f).await;
    let anna = enrol_approver(&c, &r.base, &admin, "anna", 0x41).await;
    let bert = enrol_approver(&c, &r.base, &admin, "bert", 0x42).await;
    let (status, body) = advance(&c, &r.base, &anna).await;
    assert_eq!(status, 200, "anna approves: {body}");
    assert!(body.contains("POLICY_CHECKED"), "one of two: {body}");
    let (status, body) = advance(&c, &r.base, &bert).await;
    assert_eq!(status, 200, "bert approves: {body}");
    assert!(body.contains("APPROVED"), "two of two: {body}");

    // ── the console seals onward to AVAILABLE ──
    for want in ["SEALED", "PROMOTED", "AVAILABLE"] {
        let (status, body) = advance(&c, &r.base, CONSOLE_TOKEN).await;
        assert_eq!(status, 200, "→ {want}: {body}");
    }
    assert_eq!(
        chain
            .state(&holger_server_lib::ArtifactId {
                namespace: None,
                name: WHEEL.into(),
                version: String::new()
            })
            .name(),
        "AVAILABLE"
    );
    let get = c
        .get(format!("{}/production/{WHEEL}", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(get.status(), 200);
    assert_eq!(
        get.bytes().await.unwrap().as_ref(),
        &wheel[..],
        "the wheel, byte for byte"
    );
    let index = c
        .get(format!("{}/production/simple/demo-pkg/", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(index.status(), 200, "the PEP 503 index");
    assert!(index.text().await.unwrap().contains(WHEEL));

    // ── the audit chain verifies ──
    let verify = c
        .get(format!("{}/-/audit/verify", r.base))
        .bearer_auth(CONSOLE_TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(verify.status(), 200, "the console audits");
    let v: serde_json::Value = verify.json().await.unwrap();
    assert_eq!(v["intact"], serde_json::json!(true), "{v}");
}

/// A small but well-formed wheel (stored-only zip with METADATA and WHEEL).
fn wheel_bytes() -> Vec<u8> {
    fn crc32(data: &[u8]) -> u32 {
        let mut crc = 0xFFFF_FFFFu32;
        for b in data {
            crc ^= *b as u32;
            for _ in 0..8 {
                crc = if crc & 1 != 0 {
                    (crc >> 1) ^ 0xEDB8_8320
                } else {
                    crc >> 1
                };
            }
        }
        !crc
    }
    fn member(out: &mut Vec<u8>, central: &mut Vec<u8>, name: &str, body: &[u8]) {
        let crc = crc32(body);
        let offset = out.len() as u32;
        out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
        out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        out.extend_from_slice(&crc.to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(name.len() as u16).to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(name.as_bytes());
        out.extend_from_slice(body);
        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
        central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&crc.to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(name.len() as u16).to_le_bytes());
        central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&offset.to_le_bytes());
        central.extend_from_slice(name.as_bytes());
    }
    let mut out = Vec::new();
    let mut central = Vec::new();
    member(
        &mut out,
        &mut central,
        "demo_pkg/__init__.py",
        b"__version__ = \"1.0.0\"\n",
    );
    member(
        &mut out,
        &mut central,
        "demo_pkg-1.0.0.dist-info/METADATA",
        b"Metadata-Version: 2.1\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n",
    );
    member(
        &mut out,
        &mut central,
        "demo_pkg-1.0.0.dist-info/WHEEL",
        b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
    );
    let cd_offset = out.len() as u32;
    let cd_len = central.len() as u32;
    out.extend_from_slice(&central);
    out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
    out.extend_from_slice(&[0, 0, 0, 0, 3, 0, 3, 0]);
    out.extend_from_slice(&cd_len.to_le_bytes());
    out.extend_from_slice(&cd_offset.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out
}

/// reqwest 0.13 with the ring provider and the bundled roots, as the other door tests build it.
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
    let _ = rustls::crypto::ring::default_provider().install_default();
    b.tls_certs_only(
        webpki_root_certs::TLS_SERVER_ROOT_CERTS
            .iter()
            .filter_map(|c| reqwest::Certificate::from_der(c).ok()),
    )
}