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