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

nordisk/holger

build-space/tests/auth_ship_small.rs

//! # Ship-small proof — the small core links NO OIDC closure (task #57)
//!
//! The pluggable auth providers are feature-gated sub-crates. The whole point is
//! that a build pins only the providers it needs: server-lib's small core
//! (`downstream`, which every server carries) ships the tiny `nornir-auth-bearer`
//! provider and the mTLS transport provider, and links NONE of the heavy OIDC
//! closure (`nornir-auth-oidc` + its `jsonwebtoken`/JWKS dependencies). This test
//! proves the pinning by resolving the actual Cargo dependency graph with
//! `cargo tree` of `holger-server-lib`:
//!
//! * `--no-default-features --features downstream` (bearer + mTLS) — the graph MUST
//!   contain `nornir-auth-bearer` and MUST NOT contain `nornir-auth-oidc` or
//!   `jsonwebtoken`. (RED-when-broken: if the OIDC closure leaked into the core,
//!   this fails.)
//! * `--no-default-features --features full` — the graph MUST contain BOTH
//!   providers (the full modern-auth surface, `--features full` on a product).
//!
//! The same gate guards the HEAVY off-by-default providers: `nornir-auth-webauthn`
//! (WebAuthn / FIDO2 / passkeys) pulls the native **openssl** stack (MPL-2.0
//! `webauthn-rs`); no build links that backend, `full` included: none may link
//! openssl.
//!
//! This is the concrete "ship small" gate: the core carries no crypto/JWT stack it
//! doesn't use.

use std::path::Path;
use std::process::Command;

/// The holger repo root (the build-space crate's parent dir).
fn repo_root() -> &'static Path {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("holger repo root is the build-space crate's parent")
}

/// `cargo tree` of `holger-server-lib` for a feature set, edges = normal (no
/// dev/build), as one string. `None` if cargo could not be invoked at all (so a
/// cargo-less sandbox skips rather than false-fails).
fn dep_tree(features: &str) -> Option<String> {
    let out = Command::new(env!("CARGO"))
        .current_dir(repo_root())
        .args([
            "tree",
            "-p",
            "holger-server-lib",
            "--no-default-features",
            "--features",
            features,
            "-e",
            "normal",
            "--prefix",
            "none",
        ])
        .output()
        .ok()?;
    if !out.status.success() {
        panic!(
            "cargo tree (features {features}) failed:\n{}",
            String::from_utf8_lossy(&out.stderr)
        );
    }
    Some(String::from_utf8_lossy(&out.stdout).into_owned())
}

/// `cargo tree` of an arbitrary package (optionally with a feature set), edges =
/// normal, as one string. `None` if cargo could not be invoked.
fn dep_tree_pkg(pkg: &str, features: Option<&str>) -> Option<String> {
    let mut args = vec!["tree", "-p", pkg, "-e", "normal", "--prefix", "none"];
    if let Some(f) = features {
        args.extend_from_slice(&["--no-default-features", "--features", f]);
    }
    let out = Command::new(env!("CARGO"))
        .current_dir(repo_root())
        .args(&args)
        .output()
        .ok()?;
    if !out.status.success() {
        panic!(
            "cargo tree -p {pkg} failed:\n{}",
            String::from_utf8_lossy(&out.stderr)
        );
    }
    Some(String::from_utf8_lossy(&out.stdout).into_owned())
}

/// Outbound-HTTP client crates a zero-egress artifact must never link.
const HTTP_CLIENTS: &[&str] = &[
    "reqwest",
    "ureq",
    "isahc",
    "curl",
    "surf",
    "attohttpc",
    "awc",
    "http-client",
];

fn assert_no_http_client(what: &str, tree: &str) {
    for c in HTTP_CLIENTS {
        assert!(
            !tree.contains(c),
            "{what} must link NO outbound HTTP client, but its tree contains {c:?}:\n{tree}"
        );
    }
}

/// Zero-egress proof for the Secure Update Gateway: the bundle crate (`holger-cli
/// bundle` / `holger-cli verify` run it) and the server's small core link no outbound
/// HTTP client. Non-vacuous: server-lib `full` DOES link one (federation), proving the
/// assertion can actually detect an HTTP client.
#[test]
fn secure_update_zero_egress_links_no_http_client() {
    // The network-free bundle verifier crate.
    if let Some(t) = dep_tree_pkg("holger-update-bundle", None) {
        assert_no_http_client("holger-update-bundle (holger-cli verify)", &t);
    } else {
        eprintln!("cargo unavailable — skipping zero-egress proof");
        return;
    }
    // The small core every server carries (server-lib `downstream`).
    if let Some(t) = dep_tree("downstream") {
        assert_no_http_client("the small core (server-lib downstream)", &t);
    }
    // The bundle verifier's clock on a zero-egress downstream (`secure_update` ->
    // `clock-follow`): the estate's signed peer, measured WITHOUT an HTTP client.
    // Non-vacuous: the tree must actually carry estate-clock and its ssh-key.
    if let Some(t) = dep_tree("downstream,clock-follow") {
        assert_no_http_client("the core + clock-follow (the verifier's clock)", &t);
        assert!(
            t.contains("estate-clock") && t.contains("ssh-key"),
            "clock-follow must link estate-clock's signed peer (ssh-key):\n{t}"
        );
    }
    // Non-vacuous anchor: `full` is NOT zero-egress — it pulls federation
    // (skidbladnir → reqwest). If this ever stops linking reqwest, the assertions
    // above would be trivially passing and this catches it.
    if let Some(full) = dep_tree("full") {
        assert!(
            full.contains("reqwest") || full.contains("ureq"),
            "`full` is expected to link an HTTP client (federation); if not, the \
             zero-egress assertions above are vacuous:\n{full}"
        );
        assert!(
            full.contains("skidbladnir"),
            "`full` is expected to link the federation dep skidbladnir:\n{full}"
        );
    }
}

/// S7: the secure side's intel import and its local advisory database link no
/// HTTP client — the `holger-intel` crate's default tree is zero-egress, and
/// only its `fetch` feature (the online `intel build` side) adds one. Non-vacuous:
/// `fetch` must bring `ureq` in.
#[test]
fn intel_secure_side_links_no_http_client() {
    let Some(t) = dep_tree_pkg("holger-intel", None) else {
        eprintln!("cargo unavailable — skipping intel zero-egress proof");
        return;
    };
    assert_no_http_client("holger-intel (intel import / local advisory DB)", &t);
    assert!(
        t.contains("holger-update-bundle"),
        "intel rides the update-bundle trust code:\n{t}"
    );
    if let Some(online) = dep_tree_pkg("holger-intel", Some("fetch")) {
        assert!(
            online.contains("ureq"),
            "the online `fetch` side must link its client:\n{online}"
        );
    }
}

#[test]
fn the_small_core_excludes_the_oidc_closure() {
    let Some(core) = dep_tree("downstream") else {
        eprintln!("cargo unavailable — skipping ship-small proof");
        return;
    };

    // The bearer provider IS present (the one provider every server ships).
    assert!(
        core.contains("nornir-auth-bearer"),
        "the small core must link nornir-auth-bearer, tree:\n{core}"
    );
    // The mTLS provider IS present too — it is the tiny transport provider the core
    // pins to terminate client certs, and it
    // carries NO crypto/HTTP closure (the handshake lives in the transport), so it
    // does not violate ship-small below.
    assert!(
        core.contains("nornir-auth-mtls"),
        "the small core must link the tiny nornir-auth-mtls transport provider, tree:\n{core}"
    );
    // The OIDC provider + its JWT crypto closure are ABSENT — the ship-small proof.
    assert!(
        !core.contains("nornir-auth-oidc"),
        "the small core must NOT link nornir-auth-oidc (OIDC closure leaked), tree:\n{core}"
    );
    assert!(
        !core.contains("jsonwebtoken"),
        "the small core must NOT link jsonwebtoken (JWT stack leaked) — \
         adding nornir-auth-mtls must not drag in any crypto, tree:\n{core}"
    );
    // The OAuth2 provider (RFC 7662 introspection + its `oauth2` closure) is ABSENT
    // too — it is pinned only by `full`, so the core links none of it.
    assert!(
        !core.contains("nornir-auth-oauth2"),
        "the small core must NOT link nornir-auth-oauth2 (OAuth2 closure leaked), tree:\n{core}"
    );
    // The WebAuthn/FIDO2 passkey provider is ABSENT — and crucially so is its
    // native `openssl` stack. RED-when-broken: if `auth-webauthn` ever leaked into
    // the core it would drag webauthn-rs + openssl into an airgap binary.
    assert!(
        !core.contains("nornir-auth-webauthn"),
        "the small core must NOT link nornir-auth-webauthn (passkey backend leaked), tree:\n{core}"
    );
    assert!(
        !core.contains("openssl"),
        "the small core must NOT link openssl (webauthn-rs native drag), tree:\n{core}"
    );
}

#[test]
fn full_tier_links_both_modern_providers() {
    let Some(full) = dep_tree("full") else {
        eprintln!("cargo unavailable — skipping ship-small proof");
        return;
    };
    assert!(
        full.contains("nornir-auth-bearer"),
        "`full` links the bearer provider"
    );
    assert!(
        full.contains("nornir-auth-oidc"),
        "`full` links the OIDC provider (the full modern-auth surface), tree:\n{full}"
    );
    assert!(
        full.contains("nornir-auth-mtls"),
        "`full` links the mTLS transport provider, tree:\n{full}"
    );
    assert!(
        full.contains("nornir-auth-oauth2"),
        "`full` links the OAuth2 provider (RFC 7662 introspection + PKCE / \
         client-credentials), tree:\n{full}"
    );
    // ── The passkey provider IS linked by `full` now, and openssl still is not.
    //
    // This assertion used to read `!full.contains("nornir-auth-webauthn")`, and the
    // note under it named the exact condition for lifting it: "wire
    // `AuthMethodConfig::WebAuthn` first". That has now happened, so the ban is
    // lifted in the one direction it was ever about — and TIGHTENED in the other.
    //
    // The old objection was two things stacked, and they came apart cleanly:
    //
    //  1. NO DISPATCH ARM. `validate_request` sees one stateless credential per
    //     request and a ceremony is two stateful round-trips, so the method was
    //     linked and unreachable. Resolved by noticing that the ceremony is not the
    //     credential: it runs on holger's own `/-/auth/passkey/...` door and mints
    //     a MAC'd session token, and `AuthMethodConfig::Passkey` dispatches THAT
    //     through the ordinary bearer path.
    //  2. THE OPENSSL DRAG. Still entirely valid, and still enforced below. What
    //     changed is that it is no longer a reason to have no passkeys at all:
    //     `nornir-auth-webauthn` now ships a DEFAULT pure-Rust backend (p256 +
    //     ciborium, measured at 24 added crates with no FFI), and holger's
    //     `auth-passkey` feature selects that one. The openssl-backed `webauthn-rs`
    //     path stays behind that crate's own off-by-default `webauthn` feature,
    //     which holger's `auth-webauthn` feature selects and NO tier pins.
    //
    // Why holger wants passkeys linked at all is one word: airgap. Passkeys riding
    // in as OIDC — the old note's answer — requires an IdP, and an IdP inside a
    // sealed environment is a service somebody must raise, seal, upgrade and keep
    // running or nobody logs in anywhere.
    assert!(
        full.contains("nornir-auth-webauthn"),
        "`full` must link nornir-auth-webauthn — `auth-passkey` is what gives an \
         airgapped deployment a human login with no IdP, tree:\n{full}"
    );
    // THE assertion, and it is now the load-bearing one: the pure backend is what
    // may be linked, the openssl one is what may not. RED-when-broken: flipping
    // holger's `auth-passkey` over to the leaf's `webauthn` feature, or anything
    // that drags openssl into the server binary by another route, fails here.
    assert!(
        !full.contains("openssl"),
        "`full` must NOT link the native openssl stack — holger's TLS is \
         rustls/ring everywhere, and an artifact whose whole purpose is to be built \
         and shipped into a sealed environment must not need a C library on its \
         build host. If this fired after a passkey change, `auth-passkey` has been \
         pointed at the webauthn-rs backend instead of the pure-Rust one, \
         tree:\n{full}"
    );
    assert!(
        !full.contains("webauthn-rs"),
        "`full` must NOT link webauthn-rs — the pure-Rust p256/ciborium backend \
         is the one holger pins, tree:\n{full}"
    );
}

/// **The appliance carries the passkey door, and pays nothing else for it.**
///
/// The appliance rows (`mvp-server-musl` and the metered ones in `holger-recipe`) name
/// `auth-passkey` because they are the AIRGAP shape: an ISO-installed box with no
/// shell, inside a bubble that has no IdP. Without the feature the binary does not
/// merely refuse a passkey login — `exposed::http`'s `/-/auth/passkey` arm is
/// `#[cfg(feature = "auth-passkey")]`, so the path is not claimed at all and falls
/// through to the repository router's generic `404 Unknown repository`. MEASURED
/// 2026-09-20 on the deployed holger.rs binary, which is what put the feature on
/// the row.
///
/// The three things the row must NOT have paid, each of which has bitten before:
/// an HTTP client (the zero-egress keystone), the native openssl stack (the reason
/// `auth-webauthn` is pinned by nothing), and `holger-front` (a box with no browser
/// reaching it has no business serving a public web page — `6f02ddf`'s leak).
#[test]
fn the_appliance_row_links_the_passkey_door_and_stays_zero_egress() {
    const ROW: &str = "mvp-server,appliance,auth-passkey";
    let Some(tree) = dep_tree_pkg("holger", Some(ROW)) else {
        eprintln!("cargo unavailable — skipping appliance passkey proof");
        return;
    };
    assert!(
        tree.contains("nornir-auth-webauthn"),
        "the appliance row `{ROW}` must link the passkey provider — it is the only \
         human login an airgapped box has, tree:\n{tree}"
    );
    assert_no_http_client(&format!("appliance row `{ROW}`"), &tree);
    assert!(
        !tree.contains("openssl"),
        "the appliance row `{ROW}` must NOT link the native openssl stack — \
         `auth-passkey` selects the pure-Rust p256/ciborium backend, tree:\n{tree}"
    );
    assert!(
        !tree.contains("holger-front"),
        "the appliance row `{ROW}` must NOT link `holger-front` — the public web \
         page belongs to the host a browser reaches, not to the sealed box, \
         tree:\n{tree}"
    );
    // Non-vacuous: the same row WITHOUT the feature must not carry the provider,
    // so the assertion above is testing the feature and not the tier.
    if let Some(bare) = dep_tree_pkg("holger", Some("mvp-server,appliance")) {
        assert!(
            !bare.contains("nornir-auth-webauthn"),
            "`mvp-server,appliance` without `auth-passkey` must NOT carry the passkey \
             provider, or the proof above is vacuous:\n{bare}"
        );
    }
}