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

nordisk/Skidbladnir

tests/bifrost_cli_contract.rs

//! **`skidbladnir bifrost` must not be quiet about two things.**
//!
//! 1. **A flag the verb cannot honour.** `--bundle` was parsed for every verb and used
//!    by only one, so `bifrost rollback --bundle release-0.5.0.znippy` answered
//!    "no prior generation retained for `nornir-server` to roll back to" — a reply to a
//!    question the operator had not asked. The bundle they named was dropped without a
//!    word. Same for `--trust-root`.
//!
//! 2. **Whether the bundle's AUTHORSHIP was checked.** `refresh_product` verifies an
//!    ed25519 signature only when a `TrustRoot` is pinned. The CLI hardcoded
//!    `trust_root: None` while its own missing-argument message called the file "the
//!    signed payload". Integrity (manifest root + per-entry content hashes) was
//!    checked; authorship was not, and nothing said so.
//!
//! The authenticity test asserts **on-disk applied output**, not the printed line: the
//! same bundle is deployed twice, once with and once without a pinned trust root, and
//! the generation directory must materialise in exactly one of the two runs. A run that
//! only compared stderr would stay green for a build that printed the warning and then
//! passed `None` anyway — which is precisely the bug.

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use skidbladnir::archive::{Archive, TarZstd};

const EXE: &str = env!("CARGO_BIN_EXE_skidbladnir");

/// A real, packable product tree carrying the entrypoint
/// `ComponentSpec::nornir_server()` declares (`linux-x86_64/nornir-server`), packed
/// with the same [`TarZstd`] engine the CLI uses — so the bundle is genuinely valid and
/// verification passes for a reason, not because nothing was checked.
fn nornir_server_bundle(dir: &Path) -> PathBuf {
    let staging = dir.join("staging");
    std::fs::create_dir_all(staging.join("linux-x86_64")).unwrap();
    std::fs::write(
        staging.join("linux-x86_64/nornir-server"),
        b"#!/bin/sh\nexit 0\n",
    )
    .unwrap();
    let bundle = dir.join("nornir-server-test.tar.zst");
    TarZstd::new()
        .pack(&staging, "test-0.0.1", &bundle)
        .expect("pack a valid bundle");
    assert!(
        skidbladnir::archive::manifest_path(&bundle).exists(),
        "the fixture must carry a sidecar manifest, or `verify` would fail for the wrong reason"
    );
    bundle
}

fn bifrost(args: &[&str]) -> Output {
    Command::new(EXE)
        .arg("bifrost")
        .args(args)
        .output()
        .expect("run skidbladnir bifrost")
}

/// Is there an unfolded `nornir-server` under this prefix? The generation directory is
/// the APPLIED result of getting past verification — it exists only if `refresh_product`
/// reached step 2.
fn unfolded_entrypoints(prefix: &Path) -> Vec<PathBuf> {
    fn walk(d: &Path, out: &mut Vec<PathBuf>) {
        let Ok(rd) = std::fs::read_dir(d) else { return };
        for e in rd.flatten() {
            // NOT `p.is_dir()`: that follows symlinks, and `software/current` is a
            // symlink to the version dir, so the walk descended the same generation
            // twice and reported two entrypoints for one install (measured: left: 2).
            let Ok(ft) = e.file_type() else { continue };
            if ft.is_symlink() {
                continue;
            }
            let p = e.path();
            if ft.is_dir() {
                walk(&p, out);
            } else if p.file_name().and_then(|n| n.to_str()) == Some("nornir-server") {
                out.push(p);
            }
        }
    }
    let mut out = Vec::new();
    walk(&prefix.join("software"), &mut out);
    out
}

#[test]
fn rollback_refuses_a_bundle_instead_of_silently_dropping_it() {
    let tmp = tempfile::tempdir().unwrap();
    let out = bifrost(&[
        "rollback",
        "--bundle",
        "/nonexistent/release-0.5.0.znippy",
        "--prefix",
        tmp.path().join("p").to_str().unwrap(),
        "--root",
        tmp.path().join("r").to_str().unwrap(),
    ]);
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        stderr.contains("cannot honour `--bundle`"),
        "a rollback cannot act on a bundle, so naming one must be refused BY NAME; got:\n{stderr}"
    );
    assert!(
        !stderr.contains("no prior generation retained"),
        "the old behaviour: the bundle was dropped and the operator got an answer to a \
         different question:\n{stderr}"
    );
    assert_ne!(out.status.code(), Some(0), "refusing must not exit 0");
}

#[test]
fn rollback_refuses_a_trust_root_it_would_never_use() {
    let tmp = tempfile::tempdir().unwrap();
    let out = bifrost(&[
        "rollback",
        "--trust-root",
        &"aa".repeat(32),
        "--prefix",
        tmp.path().join("p").to_str().unwrap(),
    ]);
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        stderr.contains("cannot honour `--trust-root`"),
        "a rollback verifies nothing, so accepting a trust root would imply a check that \
         never happens; got:\n{stderr}"
    );
}

#[test]
fn a_bad_trust_root_is_rejected_before_the_unit_is_touched() {
    let tmp = tempfile::tempdir().unwrap();
    let bundle = nornir_server_bundle(tmp.path());
    let prefix = tmp.path().join("p");
    let out = bifrost(&[
        "deploy",
        "--bundle",
        bundle.to_str().unwrap(),
        "--trust-root",
        "deadbeef", // 4 bytes, not 32
        "--prefix",
        prefix.to_str().unwrap(),
        "--root",
        tmp.path().join("r").to_str().unwrap(),
    ]);
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        stderr.contains("32-byte ed25519 public key"),
        "a malformed fingerprint must name the expected form; got:\n{stderr}"
    );
    assert!(
        unfolded_entrypoints(&prefix).is_empty(),
        "nothing may be unfolded when the trust root itself is unusable"
    );
}

/// **The applied proof that `--trust-root` is threaded through, not printed and
/// discarded.** The same valid, UNSIGNED bundle is deployed twice.
///
/// - no `--trust-root` → integrity-only gate passes → the generation IS unfolded
///   (the deploy then fails further on, at the systemd D-Bus restart, which needs root
///   — irrelevant here; getting that far is the point).
/// - `--trust-root <any key>` → authenticity is now MANDATORY, the bundle carries no
///   signature → refused at verify → NOTHING is unfolded.
///
/// With the old hardcoded `trust_root: None` both runs unfold, so the second assertion
/// is the red. Asserting the on-disk difference — rather than the printed notice — is
/// what makes this test unable to pass for a build that merely warns.
#[test]
fn a_pinned_trust_root_actually_refuses_an_unsigned_bundle() {
    let tmp = tempfile::tempdir().unwrap();
    let bundle = nornir_server_bundle(tmp.path());
    let root = tmp.path().join("r");

    let unpinned_prefix = tmp.path().join("unpinned");
    let out = bifrost(&[
        "deploy",
        "--bundle",
        bundle.to_str().unwrap(),
        "--prefix",
        unpinned_prefix.to_str().unwrap(),
        "--root",
        root.to_str().unwrap(),
    ]);
    let unpinned_stderr = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        unpinned_stderr.contains("authenticity NOT verified"),
        "an integrity-only deploy must SAY that authorship was not checked; got:\n{unpinned_stderr}"
    );
    let unfolded = unfolded_entrypoints(&unpinned_prefix);
    assert_eq!(
        unfolded.len(),
        1,
        "the control arm must get PAST verification and unfold the generation, or the \
         comparison below proves nothing; stderr was:\n{unpinned_stderr}"
    );

    let pinned_prefix = tmp.path().join("pinned");
    let out = bifrost(&[
        "deploy",
        "--bundle",
        bundle.to_str().unwrap(),
        "--trust-root",
        &"11".repeat(32),
        "--prefix",
        pinned_prefix.to_str().unwrap(),
        "--root",
        root.to_str().unwrap(),
    ]);
    let pinned_stderr = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        pinned_stderr.contains("authenticity PINNED"),
        "a pinned deploy must name the CA it will accept; got:\n{pinned_stderr}"
    );
    assert!(
        unfolded_entrypoints(&pinned_prefix).is_empty(),
        "an UNSIGNED bundle must be refused when a trust root is pinned — nothing may reach \
         disk. Anything unfolded here means the CLI printed the pin and then passed \
         `trust_root: None`; stderr was:\n{pinned_stderr}"
    );
    assert_ne!(
        out.status.code(),
        Some(0),
        "a refused deploy must not exit 0"
    );
}