nordisk/heimdall
guests/verify-bundle/tests/common/mod.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Real bundles, made the way Holger makes them: Holger's own
//! `holger-update-bundle` prepares the candidate, signs the approvals and
//! finalizes the release signature; znippy's own writer packs the archive.
//! Holger's own verifier is the differential oracle.
//!
//! An attacker who holds the RELEASE key (one insider, not two) is modelled by
//! [`Fixture::resign`]: it rebuilds and re-signs the release manifest exactly as
//! `finalize` does, without the approvals' consent.
#![allow(dead_code)]
pub mod zwriter;
use std::collections::BTreeMap;
use std::path::PathBuf;
use chrono::{DateTime, Duration, TimeZone, Utc};
use holger_update_bundle::core::{PolicyResult, SourceProvenance};
use holger_update_bundle::finalize::{add_approval, finalize};
use holger_update_bundle::manifest::ReleaseManifest;
use holger_update_bundle::policy::{ApprovedSigner, ExpiredPosture};
use holger_update_bundle::prepare::{EvidenceInput, PayloadInput, Preparation};
use holger_update_bundle::signer::sign_pkcs8;
use holger_update_bundle::{
BundleError, CertificateValidity, EvidenceKind, TrustMode, TrustPolicy, TrustedTime,
};
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair, PKCS_ED25519};
pub use heimdall_guest_verify_bundle::{output_name, verify_bundle, Outcome};
/// The member an output name was emitted for.
pub fn member_for<'a>(members: &'a MemberMap, name: &str) -> &'a Vec<u8> {
members
.iter()
.find(|(p, _)| output_name(p) == name)
.map(|(_, b)| b)
.unwrap_or_else(|| panic!("no member is emitted as {name}"))
}
pub type MemberMap = BTreeMap<String, Vec<u8>>;
pub const RELEASE_MANIFEST: &str = "signatures/release-manifest.json";
pub const RELEASE_SIG: &str = "signatures/release.sig";
pub const RELEASE_CRT: &str = "signatures/release.crt";
/// A fixed, reproducible "now" for every fixture.
pub fn t0() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 10, 30, 12, 0, 0).unwrap()
}
pub fn sha256_hex(b: &[u8]) -> String {
holger_update_bundle::digest::sha256_hex(b)
}
pub fn hex(b: &[u8]) -> String {
holger_update_bundle::digest::hex_lower(b)
}
// ------------------------------------------------------------------- PKI
#[derive(Clone)]
pub struct Signer {
pub cert_der: Vec<u8>,
pub key_pkcs8: Vec<u8>,
pub fingerprint: String,
}
pub struct Ca {
pub issuer: rcgen::Issuer<'static, KeyPair>,
pub der: Vec<u8>,
}
fn params_for(cn: &str, window: Option<(DateTime<Utc>, DateTime<Utc>)>) -> CertificateParams {
let mut p = CertificateParams::new(vec![]).unwrap();
p.distinguished_name.push(DnType::CommonName, cn);
if let Some((nb, na)) = window {
p.not_before = rcgen::date_time_ymd(
nb.format("%Y").to_string().parse().unwrap(),
nb.format("%m").to_string().parse().unwrap(),
nb.format("%d").to_string().parse().unwrap(),
);
p.not_after = rcgen::date_time_ymd(
na.format("%Y").to_string().parse().unwrap(),
na.format("%m").to_string().parse().unwrap(),
na.format("%d").to_string().parse().unwrap(),
);
}
p
}
pub fn self_signed(cn: &str) -> Signer {
self_signed_window(cn, None)
}
/// A self-signed Ed25519 signer valid only inside `window` (day precision).
pub fn self_signed_window(cn: &str, window: Option<(DateTime<Utc>, DateTime<Utc>)>) -> Signer {
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let cert = params_for(cn, window).self_signed(&key).unwrap();
let der = cert.der().to_vec();
Signer {
fingerprint: sha256_hex(&der),
cert_der: der,
key_pkcs8: key.serialize_der(),
}
}
pub fn ca(cn: &str) -> Ca {
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let mut p = params_for(cn, None);
p.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
let cert = p.self_signed(&key).unwrap();
Ca {
der: cert.der().to_vec(),
issuer: rcgen::Issuer::new(p, key),
}
}
/// A self-signed Ed25519 certificate that is NOT a CA, used as a would-be root.
pub fn non_ca_root(cn: &str) -> Ca {
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let p = params_for(cn, None);
let cert = p.self_signed(&key).unwrap();
Ca {
der: cert.der().to_vec(),
issuer: rcgen::Issuer::new(p, key),
}
}
pub fn leaf(cn: &str, ca: &Ca) -> Signer {
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let cert = params_for(cn, None).signed_by(&key, &ca.issuer).unwrap();
let der = cert.der().to_vec();
Signer {
fingerprint: sha256_hex(&der),
cert_der: der,
key_pkcs8: key.serialize_der(),
}
}
// ---------------------------------------------------------------- policy
pub fn pinned_policy(reviewer: &Signer, owner: &Signer, release: &Signer) -> TrustPolicy {
TrustPolicy {
schema_version: "holger.bundle-trust-policy/v1".into(),
trust_mode: Some(TrustMode::PinnedSigners),
trusted_roots: vec![],
certificate_validity: CertificateValidity::Enforce,
approved_signers: vec![
ApprovedSigner {
fingerprint: reviewer.fingerprint.clone(),
subject: None,
roles: vec!["security-reviewer".into()],
},
ApprovedSigner {
fingerprint: owner.fingerprint.clone(),
subject: None,
roles: vec!["system-owner".into()],
},
],
required_approval_roles: vec!["security-reviewer".into(), "system-owner".into()],
minimum_distinct_approvers: 2,
trusted_release_signers: vec![release.fingerprint.clone()],
release_signer_must_be_distinct: true,
maximum_bundle_age_secs: None,
require_expiry: true,
on_expired_metadata: Some(ExpiredPosture::ReadOnly),
require_sbom: true,
require_vex: false,
require_scan: false,
allowed_products: vec![],
allowed_models: vec![],
}
}
// --------------------------------------------------------------- bundles
pub struct Spec {
pub sequence: u64,
pub bundle_id: String,
pub payloads: Vec<(String, Vec<u8>, String)>,
pub evidence: Vec<(String, Vec<u8>, String, EvidenceKind)>,
pub expires_in: Option<Duration>,
pub rooted: bool,
}
impl Default for Spec {
fn default() -> Self {
Spec {
sequence: 5,
bundle_id: "bundle-0005".into(),
payloads: vec![(
"firmware.bin".into(),
b"firmware-v1".to_vec(),
"application/octet-stream".into(),
)],
evidence: vec![
(
"sbom/app.cdx.json".into(),
br#"{"bomFormat":"CycloneDX","specVersion":"1.5"}"#.to_vec(),
"application/json".into(),
EvidenceKind::Sbom,
),
(
"source-receipt.json".into(),
br#"{"mode":"Strict","complete":true}"#.to_vec(),
"application/json".into(),
EvidenceKind::SourceReceipt,
),
],
expires_in: Some(Duration::days(30)),
rooted: false,
}
}
}
pub struct Fixture {
pub members: MemberMap,
pub policy: TrustPolicy,
pub reviewer: Signer,
pub owner: Signer,
pub release: Signer,
pub candidate_digest: String,
pub bundle_id: String,
pub now: DateTime<Utc>,
}
pub fn preparation(spec: &Spec, now: DateTime<Utc>) -> Preparation {
Preparation {
bundle_id: spec.bundle_id.clone(),
sequence: spec.sequence,
created_at: now,
expires_at: spec.expires_in.map(|d| now + d),
producer: "intake-station".into(),
supplier: "ExampleCorp".into(),
product: "ExampleDrive".into(),
release_version: "1.2.3".into(),
supported_hardware_models: vec!["ACS880".into()],
supported_hardware_revisions: vec!["R4".into()],
source_provenance: SourceProvenance {
source_kind: "vendor-portal".into(),
source_uri: Some("https://vendor.example/downloads/fw".into()),
source_repository: None,
source_asset_path: None,
fetched_at: Some(now),
reported_size: Some(11),
vendor_checksum: None,
vendor_signature_status: Some("unverified".into()),
tls_origin: Some("vendor.example".into()),
transfer_receipt_digest: None,
},
policy_result: PolicyResult {
policy_digest: sha256_hex(b"trust-policy-v1"),
advisory_snapshot_digest: None,
verdict: "pass".into(),
non_production: true,
},
supersedes: vec![],
withdraws: vec![],
payloads: spec
.payloads
.iter()
.map(|(p, b, m)| PayloadInput {
relative_path: p.clone(),
bytes: b.clone(),
media_type: m.clone(),
})
.collect(),
evidence: spec
.evidence
.iter()
.map(|(p, b, m, k)| EvidenceInput {
relative_path: p.clone(),
bytes: b.clone(),
media_type: m.clone(),
kind: *k,
})
.collect(),
}
}
/// A valid, two-approval, release-signed bundle, built by Holger's library.
pub fn build(spec: Spec) -> Fixture {
let now = t0();
let (reviewer, owner, release, mut policy) = if spec.rooted {
let root = ca("bundle-root-ca");
let reviewer = leaf("security-reviewer", &root);
let owner = leaf("system-owner", &root);
let release = leaf("release-signer", &root);
let mut policy = pinned_policy(&reviewer, &owner, &release);
policy.trust_mode = Some(TrustMode::RootedChain);
policy.trusted_roots = vec![hex(&root.der)];
(reviewer, owner, release, policy)
} else {
let reviewer = self_signed("security-reviewer");
let owner = self_signed("system-owner");
let release = self_signed("release-signer");
let policy = pinned_policy(&reviewer, &owner, &release);
(reviewer, owner, release, policy)
};
if spec.expires_in.is_none() {
policy.require_expiry = false;
}
let candidate = preparation(&spec, now).build().unwrap();
let candidate_digest = candidate.candidate_digest.clone();
let mut members = candidate.members;
add_approval(
&mut members,
"security-reviewer",
&reviewer.cert_der,
&reviewer.key_pkcs8,
None,
now,
)
.unwrap();
add_approval(
&mut members,
"system-owner",
&owner.cert_der,
&owner.key_pkcs8,
None,
now,
)
.unwrap();
finalize(
&mut members,
&policy,
&release.cert_der,
&release.key_pkcs8,
now,
)
.unwrap();
Fixture {
members,
policy,
reviewer,
owner,
release,
candidate_digest,
bundle_id: spec.bundle_id,
now,
}
}
pub fn good() -> Fixture {
build(Spec::default())
}
impl Fixture {
/// The release-key holder rewrites the release manifest over the current
/// members and signs it — `finalize` minus every check `finalize` makes.
pub fn resign(&mut self) {
self.resign_with(&self.release.clone());
}
pub fn resign_with(&mut self, signer: &Signer) {
let core = self.members["core.json"].clone();
let digest = sha256_hex(&core);
self.resign_with_digest(signer, &digest);
}
pub fn resign_with_digest(&mut self, signer: &Signer, candidate_digest: &str) {
self.members.remove(RELEASE_MANIFEST);
self.members.remove(RELEASE_SIG);
self.members
.insert(RELEASE_CRT.into(), signer.cert_der.clone());
let m = ReleaseManifest::build(&self.members, &self.bundle_id, candidate_digest);
let bytes = m.to_json_bytes().unwrap();
let sig = sign_pkcs8(&signer.key_pkcs8, &bytes).unwrap();
self.members.insert(RELEASE_MANIFEST.into(), bytes);
self.members.insert(RELEASE_SIG.into(), sig);
}
/// Pack with znippy's own writer, every member stored (the guest reads it).
pub fn pack(&self) -> Vec<u8> {
pack_stored(&self.members)
}
pub fn params(&self) -> Vec<u8> {
params_for_policy(&self.policy, self.now, "")
}
pub fn params_with(&self, extra: &str) -> Vec<u8> {
params_for_policy(&self.policy, self.now, extra)
}
/// The guest's verdict on these members, packed stored.
pub fn guest(&self) -> Outcome {
verify_bundle(&self.pack(), &self.params())
}
/// Holger's own verifier on the same archive bytes (the oracle).
pub fn holger(
&self,
archive: &[u8],
) -> Result<holger_update_bundle::VerifyReport, BundleError> {
holger_verify(archive, &self.policy, self.now)
}
}
pub fn params_for_policy(policy: &TrustPolicy, now: DateTime<Utc>, extra: &str) -> Vec<u8> {
format!(
"VerifyParams(now: \"{}\", policy: {}, {extra})",
now.to_rfc3339(),
policy.to_ron().unwrap()
)
.into_bytes()
}
struct Fixed(DateTime<Utc>);
impl TrustedTime for Fixed {
fn source_name(&self) -> &'static str {
"test-fixed"
}
fn is_trusted(&self) -> bool {
true
}
fn now(&self) -> Result<DateTime<Utc>, holger_update_bundle::TimeError> {
Ok(self.0)
}
}
pub fn holger_verify(
archive: &[u8],
policy: &TrustPolicy,
now: DateTime<Utc>,
) -> Result<holger_update_bundle::VerifyReport, BundleError> {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("b.holger.znippy");
std::fs::write(&p, archive).unwrap();
holger_update_bundle::verify_bundle_file_at(
&p,
policy,
&holger_update_bundle::TargetSpec::default(),
None,
&Fixed(now),
)
}
/// Pack with znippy's writer and `SkipPolicy::already_compressed()`: every
/// member stored, nothing through OpenZL.
pub fn pack_stored(members: &MemberMap) -> Vec<u8> {
let staging = tempfile::tempdir().unwrap();
for (rel, bytes) in members {
let d = staging.path().join(rel);
std::fs::create_dir_all(d.parent().unwrap()).unwrap();
std::fs::write(&d, bytes).unwrap();
}
let out_dir = tempfile::tempdir().unwrap();
let out: PathBuf = out_dir.path().join("bundle");
znippy_compress::compress_dir_with_policy(
&staging.path().to_path_buf(),
&out,
znippy_compress::SkipPolicy::already_compressed(),
None,
None,
None,
)
.unwrap();
std::fs::read(out.with_extension("znippy")).unwrap()
}
/// Pack with Holger's own `pack_bundle` (znippy's default policy: compressible
/// members go through OpenZL).
pub fn pack_holger(members: &MemberMap) -> Vec<u8> {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("b.holger.znippy");
holger_update_bundle::pack_bundle(members, &p).unwrap();
std::fs::read(&p).unwrap()
}
/// The refusal code inside a tagged reason: `[holger-update-7] ...` -> `holger-update-7`.
pub fn code_of(o: &Outcome) -> String {
let code = o
.code()
.unwrap_or_else(|| panic!("expected a refusal, got Accepted"))
.to_string();
let r = o.reason().unwrap();
assert!(
r.starts_with(&format!("[{code}] ")),
"reason {r:?} does not carry its code {code}"
);
code
}
/// The holger code a BundleError carries.
pub fn holger_code(e: &BundleError) -> String {
e.code().code.to_string()
}
#[track_caller]
pub fn assert_refused(o: &Outcome, code: &str, needle: &str) {
let r = o
.reason()
.unwrap_or_else(|| panic!("expected refusal {code}, got Accepted"));
assert_eq!(code_of(o), code, "wrong code; reason: {r}");
assert!(
r.contains(needle),
"reason {r:?} does not mention {needle:?}"
);
assert!(o.emitted.is_empty(), "a refusal emitted outputs");
}