crates/nornir-build-thing/tests/history_end_to_end.rs
//! **The build history over real artifacts** — attest, lose it, regenerate it,
//! verify it, and watch each of those fail.
//!
//! `history.rs`'s own tests build attestations from synthetic bytes. That covers
//! the layout, the determinism and the refusals, but it cannot cover the claim
//! the package actually makes:
//!
//! > a real binary, built by a real linker, is attested from the receipt IN IT
//! > and the sha256 OF it; the package regenerates byte-identically from those
//! > same artifacts; and holding the artifact is enough to check the claim.
//!
//! So this file builds real binaries and works from those. It is slow — three
//! small `rustc` invocations — and it is the only thing here that is evidence.
mod common;
use common::build_fixture;
use std::collections::BTreeMap;
use std::path::Path;
use nornir_build_thing::contract::{Coordinate, RECEIPT_VERSION, Receipt};
use nornir_build_thing::history::{
AttestationVerdict, HistoryPackage, PACKAGE_KIND, Unattestable, attest,
};
use nornir_build_thing::receipt::emit_source;
/// A receipt for one atom of the same recipe. `hash` is set, because an unpinned
/// recipe is refused and that refusal has its own unit test.
fn receipt(atom: &str) -> Receipt {
let mut settings = BTreeMap::new();
settings.insert("feature.gpu".to_string(), "false".to_string());
settings.insert("profile".to_string(), "release".to_string());
let mut inputs = BTreeMap::new();
inputs.insert("source-commit".to_string(), "9f2c1ab".to_string());
inputs.insert("target".to_string(), "x86_64-unknown-linux-gnu".to_string());
Receipt {
receipt_version: RECEIPT_VERSION,
build_thing: Coordinate::parse("korp-build-thing:0.3.1:a91f4c2e").unwrap(),
atom: atom.to_string(),
target: "exe".to_string(),
settings,
inputs,
}
}
/// **The whole lane in one test**, because the halves only mean anything against
/// each other: a package that attests everything proves nothing, and a verifier
/// that passes everything proves less.
///
/// Seven assertions, each with a subject that can go red on its own:
///
/// 1. two real binaries carrying receipts are attested from their own bytes;
/// 2. a binary carrying NO receipt is REFUSED BY NAME, not silently skipped —
/// the red probe for (1), and the failure mode that matters most, because a
/// history that quietly covers two of three looks complete;
/// 3. the package regenerates byte-identically from the same artifacts — "lose
/// it and it comes back" as a measurement;
/// 4. …including from COPIES under different names in a different directory,
/// which is what makes the package a function of bytes rather than of paths;
/// 5. holding the artifact is enough to verify: hash it, compare;
/// 6. one flipped byte in the artifact is caught, with both hashes named — the
/// red probe for (5);
/// 7. an artifact the history does not cover is NOT MEASURED rather than
/// attested — the release-gate case, and never a pass.
#[test]
fn real_artifacts_are_attested_regenerated_and_verified_and_each_one_can_fail() {
let root = tempfile::tempdir().expect("scratch dir");
let root = root.path();
let server = build_fixture(
root,
"atomserver",
&emit_source(&receipt("korp-server")).expect("generate"),
true,
);
let ui = build_fixture(
root,
"atomui",
&emit_source(&receipt("korp-ui")).expect("generate"),
true,
);
// Same shape, same flags, no emit — so anything the history says about it is
// about the receipt and not about the fixture.
let unreceipted = build_fixture(root, "atomnone", "", true);
// (1) + (2) One call, both outcomes. The refusals come back as data because
// the release gate has to be able to fail on them.
let (package, refused) = HistoryPackage::from_artifacts(&[&server, &ui, &unreceipted]);
assert_eq!(
package.len(),
2,
"both receipted artifacts must be attested"
);
assert_eq!(
refused.len(),
1,
"and exactly one must be refused: {refused:?}"
);
match &refused[0] {
Unattestable::NoReceipt(p) => assert_eq!(
p, &unreceipted,
"the refusal must name the artifact it is about"
),
other => panic!("wrong refusal for a binary with no receipt: {other}"),
}
assert!(
refused[0].to_string().contains(".nordisk.buildthing"),
"the refusal must say what is missing: {}",
refused[0]
);
// The attestation carries the receipt VERBATIM and the sha of the bytes.
let attested = attest(&server).expect("a receipted binary is attestable");
assert_eq!(attested.receipt, receipt("korp-server"));
let on_disk = std::fs::read(&server).expect("read the artifact");
assert_eq!(
attested.artifact.sha256,
nornir_hash::sha256_hex(&on_disk),
"the attested hash is the hash of the artifact's bytes"
);
assert_eq!(attested.artifact.bytes, on_disk.len() as u64);
// (3) Regeneration. `from_artifacts` is the only constructor, so this is
// literally the same code path — which is the point: there is no second
// writer that could drift from it.
let (again, _) = HistoryPackage::from_artifacts(&[&ui, &server]);
assert_eq!(
again.files().expect("files"),
package.files().expect("files"),
"the package did not regenerate byte-identically from the same artifacts"
);
// (4) …and from copies with other names, in another directory.
let elsewhere = root.join("elsewhere");
std::fs::create_dir_all(&elsewhere).expect("dir");
let copy_a = elsewhere.join("renamed-a");
let copy_b = elsewhere.join("renamed-b");
std::fs::copy(&server, ©_a).expect("copy");
std::fs::copy(&ui, ©_b).expect("copy");
let (from_copies, refused_copies) = HistoryPackage::from_artifacts(&[©_a, ©_b]);
assert!(refused_copies.is_empty(), "{refused_copies:?}");
assert_eq!(
from_copies.files().expect("files"),
package.files().expect("files"),
"the package is not a function of the artifacts' bytes — a rename changed it"
);
// (5) The attestation checked against the artifact, which is the whole
// premise: no registry, no network, no signature to trust.
let verdict = package.verify_artifact(&server);
assert_eq!(verdict, AttestationVerdict::Attested, "{verdict}");
assert!(verdict.holds());
// (6) THE RED PROBE for (5). One byte appended — still an ELF, still parses,
// still carries the very same receipt. Only the hash moved.
let tampered = elsewhere.join("tampered");
let mut bytes = on_disk.clone();
bytes.push(0);
std::fs::write(&tampered, &bytes).expect("write");
let verdict = package.verify_artifact(&tampered);
match &verdict {
AttestationVerdict::Unreadable(why) => {
assert!(
why.contains(&nornir_hash::sha256_hex(&bytes)),
"the miss must name the hash that was not found: {why}"
);
}
other => panic!("a tampered artifact was not caught: {other}"),
}
assert!(!verdict.holds());
// …and the same tampering caught by the attestation directly, which is the
// arm that names BOTH hashes.
let one = package
.by_artifact(&attested.artifact.sha256)
.expect("the history covers it");
match one.verify(&tampered) {
AttestationVerdict::ArtifactMismatch {
attested: a,
actual,
} => {
assert_eq!(a, attested.artifact.sha256);
assert_eq!(actual, nornir_hash::sha256_hex(&bytes));
}
other => panic!("a tampered artifact was not caught by its attestation: {other}"),
}
// (7) An artifact the history does not cover is NOT MEASURED. This is the
// release gate's case — step 3 before step 4 — and an unmeasured check is
// never an approved check.
let verdict = package.verify_artifact(&unreceipted);
assert!(
matches!(verdict, AttestationVerdict::Unreadable(_)),
"an uncovered artifact must not verify: {verdict}"
);
assert!(!verdict.holds());
}
/// **A package edited after it was written is caught by the artifact itself.**
///
/// Its own test because it is the one dishonesty the hash alone cannot see: the
/// bytes are right and the *claim about them* is wrong. Whoever holds the
/// artifact can still tell, because the receipt is inside the bytes.
#[test]
fn an_attestation_whose_receipt_was_edited_is_caught_against_the_real_artifact() {
let root = tempfile::tempdir().expect("scratch dir");
let root = root.path();
let bin = build_fixture(
root,
"editedclaim",
&emit_source(&receipt("korp-server")).expect("generate"),
true,
);
let mut a = attest(&bin).expect("attestable");
assert_eq!(
a.verify(&bin),
AttestationVerdict::Attested,
"the untouched attestation must pass, or the red below proves nothing"
);
// Rewrite the claim, keep the hash. This is what a forged history looks like:
// the same artifact attributed to a different atom.
a.receipt.atom = "korp-ui".to_string();
assert_eq!(
a.verify(&bin),
AttestationVerdict::ReceiptMismatch,
"an attestation rewritten after the fact must not verify"
);
a.receipt.atom = "korp-server".to_string();
a.receipt
.settings
.insert("feature.gpu".to_string(), "true".to_string());
assert_eq!(
a.verify(&bin),
AttestationVerdict::ReceiptMismatch,
"a flipped setting is the same forgery in a smaller place"
);
}
/// The package the sealer is handed is a plain path→bytes list with nothing
/// absolute, nothing host-specific and nothing dated in it.
///
/// This crate seals no archive itself (it links no znippy, by design), so the
/// only thing it can promise the sealer is that what it hands over is portable.
/// A timestamp or a build-host path in here would make two identical builds
/// produce two different archives, which is the property the whole package rests
/// on.
#[test]
fn the_files_handed_to_the_sealer_carry_no_absolute_path_and_no_date() {
let root = tempfile::tempdir().expect("scratch dir");
let root = root.path();
let bin = build_fixture(
root,
"portable",
&emit_source(&receipt("korp-server")).expect("generate"),
true,
);
let (package, _) = HistoryPackage::from_artifacts(&[&bin]);
let files = package.files().expect("files");
let tmp = root.to_string_lossy().to_string();
for (path, bytes) in &files {
assert!(
!path.starts_with('/') && !path.contains(".."),
"archive path escapes the archive: {path}"
);
let body = String::from_utf8_lossy(bytes);
assert!(
!body.contains(&tmp),
"{path} leaks the build directory {tmp}"
);
assert!(
!body.contains("/home/") && !body.contains("/tmp/"),
"{path} leaks an absolute path"
);
}
// LAW 2 on the sweep above: it can actually see a leaked path. Without this,
// a mistyped `contains` would pass over every file forever.
let planted = format!("{{\"where\":\"{tmp}\"}}");
assert!(
planted.contains(&tmp),
"the leak check cannot see a leak, so it can never fail"
);
// The manifest says what kind of package this is, in the file holger routes on.
let index = files
.iter()
.find(|(p, _)| p == nornir_build_thing::history::INDEX_PATH)
.map(|(_, b)| String::from_utf8_lossy(b).to_string())
.expect("every package has a manifest");
assert!(index.contains(PACKAGE_KIND), "{index}");
}
/// A directory of build outputs, attested in one call, with the non-artifacts
/// named rather than skipped.
///
/// This is the shape the forge actually calls with: `target/release` holds the
/// binaries next to `.d` files, `build/` and other rubble. Everything that is not
/// an object file at all comes back as NOT MEASURED — distinct from "carries no
/// receipt", because they are different problems with different fixes.
#[test]
fn a_file_that_is_not_an_object_file_at_all_is_not_measured_rather_than_unreceipted() {
let root = tempfile::tempdir().expect("scratch dir");
let root = root.path();
let notes = root.join("notes.txt");
std::fs::write(¬es, b"this is not an ELF").expect("write");
let (package, refused) = HistoryPackage::from_artifacts(&[¬es]);
assert!(package.is_empty());
match &refused[0] {
Unattestable::Unreadable { artifact, why } => {
assert_eq!(artifact, ¬es);
assert!(!why.is_empty(), "an unmeasured file must say why");
}
other => panic!("a text file was reported as {other}"),
}
assert!(
refused[0].to_string().contains("NOT MEASURED"),
"an unparsable file is unmeasured, never merely unreceipted: {}",
refused[0]
);
// And a missing file is unmeasured too — never an empty, healthy-looking
// package.
let (_, refused) = HistoryPackage::from_artifacts(&[Path::new("/does/not/exist")]);
assert!(matches!(refused[0], Unattestable::Unreadable { .. }));
}