// SPDX-License-Identifier: AGPL-3.0-only
//! Every tampering of a real bundle is REFUSED with Holger's own refusal code,
//! and Holger's verifier refuses the same archive too (the differential oracle).
//!
//! Two attackers: one who can only change bytes (no key at all), and one insider
//! who holds the RELEASE key but not the approvers' keys ([`Fixture::resign`]).
mod common;
use common::*;
use holger_errcode as ec;
/// The guest refuses with `code`, and Holger's verifier refuses the same bytes.
#[track_caller]
fn both_refuse(fx: &Fixture, code: &ec::ErrCode, needle: &str) {
let archive = fx.pack();
let o = verify_bundle(&archive, &fx.params());
assert_refused(&o, code.code, needle);
let h = fx.holger(&archive);
assert!(
h.is_err(),
"Holger ACCEPTED what the guest refused ({needle})"
);
}
fn flip(b: &mut [u8], i: usize) {
b[i] ^= 0x01;
}
// ------------------------------------------------- byte-level, no key
#[test]
fn a_flipped_bit_in_the_payload_is_refused_as_a_digest_mismatch() {
let mut fx = good();
flip(fx.members.get_mut("payload/firmware.bin").unwrap(), 0);
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "payload/firmware.bin");
}
#[test]
fn a_flipped_bit_in_an_evidence_file_is_refused() {
let mut fx = good();
flip(fx.members.get_mut("evidence/sbom/app.cdx.json").unwrap(), 3);
both_refuse(
&fx,
&ec::UPDATE_DIGEST_MISMATCH,
"evidence/sbom/app.cdx.json",
);
}
#[test]
fn a_changed_core_json_is_refused_before_its_signature_is_believed() {
let mut fx = good();
let c = fx.members.get_mut("core.json").unwrap();
let s = String::from_utf8(c.clone())
.unwrap()
.replace("ExampleDrive", "EvilDrive");
*c = s.into_bytes();
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "core.json");
}
#[test]
fn a_flipped_bit_in_the_release_signature_is_refused() {
let mut fx = good();
flip(fx.members.get_mut(RELEASE_SIG).unwrap(), 10);
both_refuse(
&fx,
&ec::UPDATE_RELEASE_SIGNATURE_INVALID,
"release signature",
);
}
#[test]
fn a_truncated_release_signature_is_refused() {
let mut fx = good();
fx.members.get_mut(RELEASE_SIG).unwrap().truncate(63);
both_refuse(
&fx,
&ec::UPDATE_RELEASE_SIGNATURE_INVALID,
"release signature",
);
}
#[test]
fn an_empty_release_signature_is_refused() {
let mut fx = good();
fx.members.insert(RELEASE_SIG.into(), Vec::new());
both_refuse(
&fx,
&ec::UPDATE_RELEASE_SIGNATURE_INVALID,
"release signature",
);
}
#[test]
fn a_missing_release_signature_is_refused() {
let mut fx = good();
fx.members.remove(RELEASE_SIG);
both_refuse(&fx, &ec::UPDATE_MEMBER_MISSING, "release.sig");
}
#[test]
fn a_missing_release_manifest_is_refused() {
let mut fx = good();
fx.members.remove(RELEASE_MANIFEST);
both_refuse(&fx, &ec::UPDATE_MEMBER_MISSING, "release-manifest.json");
}
#[test]
fn a_release_certificate_swapped_for_a_stranger_is_refused() {
let mut fx = good();
fx.members
.insert(RELEASE_CRT.into(), self_signed("stranger").cert_der);
both_refuse(
&fx,
&ec::UPDATE_RELEASE_SIGNATURE_INVALID,
"release signature",
);
}
#[test]
fn a_release_certificate_that_is_garbage_is_refused() {
let mut fx = good();
fx.members
.insert(RELEASE_CRT.into(), b"not a certificate".to_vec());
both_refuse(&fx, &ec::UPDATE_CERTIFICATE, "certificate");
}
#[test]
fn a_flipped_bit_in_the_release_manifest_is_refused() {
let mut fx = good();
let m = fx.members.get_mut(RELEASE_MANIFEST).unwrap();
let s = String::from_utf8(m.clone()).unwrap();
// change one hex digit of a recorded digest
let i = s.find("\"sha256\": \"").unwrap() + 11;
let mut b = s.into_bytes();
b[i] = if b[i] == b'0' { b'1' } else { b'0' };
*m = b;
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "digest mismatch");
}
#[test]
fn an_injected_stowaway_payload_is_refused_by_name_before_it_is_read() {
let mut fx = good();
fx.members
.insert("payload/stowaway.sh".into(), b"rm -rf /".to_vec());
both_refuse(&fx, &ec::UPDATE_MEMBER_UNREFERENCED, "payload/stowaway.sh");
}
#[test]
fn an_injected_control_member_is_refused() {
let mut fx = good();
fx.members
.insert("signatures/extra.sig".into(), vec![0; 64]);
both_refuse(&fx, &ec::UPDATE_MEMBER_UNREFERENCED, "signatures/extra.sig");
}
#[test]
fn a_removed_payload_is_refused_as_missing() {
let mut fx = good();
fx.members.remove("payload/firmware.bin");
both_refuse(&fx, &ec::UPDATE_MEMBER_MISSING, "payload/firmware.bin");
}
#[test]
fn a_removed_approval_is_refused() {
let mut fx = good();
let k = fx
.members
.keys()
.find(|k| k.ends_with(".approval.json"))
.unwrap()
.clone();
fx.members.remove(&k);
// The gate (control plane) counts approvers before it sweeps for missing
// members, exactly as Holger's `load_bundle_verified` does.
both_refuse(&fx, &ec::UPDATE_TWO_PERSON_RULE, "1 distinct approver(s)");
}
#[test]
fn a_payload_grown_by_one_byte_is_refused() {
let mut fx = good();
fx.members
.get_mut("payload/firmware.bin")
.unwrap()
.push(b'!');
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "payload/firmware.bin");
}
#[test]
fn a_payload_truncated_by_one_byte_is_refused() {
let mut fx = good();
fx.members.get_mut("payload/firmware.bin").unwrap().pop();
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "payload/firmware.bin");
}
#[test]
fn an_archive_with_no_members_at_all_is_refused() {
let fx = good();
let empty = zwriter::RawArchive::from_members(&MemberMap::new(), 1024).encode();
let o = verify_bundle(&empty, &fx.params());
assert_refused(&o, ec::UPDATE_MEMBER_MISSING.code, "release-manifest.json");
}
// --------------------------------------- the insider with the release key
#[test]
fn a_release_signer_who_swaps_the_payload_is_refused_by_the_approved_content_list() {
// The manifest and the members agree perfectly; core.json (what the two
// approvers signed) still names the old payload.
let mut fx = good();
fx.members
.insert("payload/firmware.bin".into(), b"evil-firmware".to_vec());
fx.resign();
both_refuse(&fx, &ec::UPDATE_DIGEST_MISMATCH, "payload/firmware.bin");
}
#[test]
fn a_release_signer_who_adds_an_undeclared_payload_is_refused() {
let mut fx = good();
fx.members
.insert("payload/backdoor.bin".into(), b"x".to_vec());
fx.resign();
both_refuse(&fx, &ec::UPDATE_MEMBER_UNREFERENCED, "payload/backdoor.bin");
}
#[test]
fn a_release_signer_who_rewrites_core_json_breaks_every_approval() {
let mut fx = good();
let c = fx.members.get_mut("core.json").unwrap();
*c = String::from_utf8(c.clone())
.unwrap()
.replace("1.2.3", "6.6.6")
.into_bytes();
fx.resign();
// manifest.json (the envelope) still names the old core digest, so the
// first thing to break is either the envelope or the approvals' digest.
let o = verify_bundle(&fx.pack(), &fx.params());
assert!(!o.is_accepted());
assert!(fx.holger(&fx.pack()).is_err());
}
#[test]
fn a_release_signer_who_drops_an_approval_fails_the_two_person_rule() {
let mut fx = good();
let gone: Vec<String> = fx
.members
.keys()
.filter(|k| k.contains("system-owner"))
.cloned()
.collect();
for k in gone {
fx.members.remove(&k);
}
fx.resign();
both_refuse(
&fx,
&ec::UPDATE_TWO_PERSON_RULE,
"1 distinct approver(s), need 2",
);
}
#[test]
fn a_release_signer_who_forges_an_approval_statement_is_refused_on_its_signature() {
let mut fx = good();
let k = fx
.members
.keys()
.find(|k| k.ends_with(".approval.json"))
.unwrap()
.clone();
let s = String::from_utf8(fx.members[&k].clone()).unwrap();
fx.members
.insert(k, s.replace("\"approve\"", "\"approve\" ").into_bytes());
fx.resign();
both_refuse(
&fx,
&ec::UPDATE_APPROVAL_SIGNATURE_INVALID,
"approval signature invalid",
);
}
#[test]
fn a_release_signer_who_approves_under_a_strangers_certificate_is_untrusted() {
let mut fx = good();
let stranger = self_signed("stranger");
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"security-reviewer",
&stranger.cert_der,
&stranger.key_pkcs8,
None,
fx.now,
)
.unwrap();
fx.resign();
both_refuse(&fx, &ec::UPDATE_UNTRUSTED_SIGNER, &stranger.fingerprint);
}
#[test]
fn a_release_signer_who_also_approves_breaks_the_distinctness_rule() {
let mut fx = good();
// the release key is ALSO listed as a reviewer, and approves
fx.policy.approved_signers[0].fingerprint = fx.release.fingerprint.clone();
let keys: Vec<String> = fx
.members
.keys()
.filter(|k| k.contains("security-reviewer"))
.cloned()
.collect();
for k in keys {
fx.members.remove(&k);
}
let rel = fx.release.clone();
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"security-reviewer",
&rel.cert_der,
&rel.key_pkcs8,
None,
fx.now,
)
.unwrap();
fx.resign();
both_refuse(
&fx,
&ec::UPDATE_TWO_PERSON_RULE,
"release signer must be distinct",
);
}
#[test]
fn one_certificate_cannot_hold_both_required_roles() {
let mut fx = good();
let owner_fp = fx.owner.fingerprint.clone();
fx.policy.approved_signers[1].roles = vec!["system-owner".into(), "security-reviewer".into()];
// drop the real reviewer, let the owner approve twice under both roles
let keys: Vec<String> = fx
.members
.keys()
.filter(|k| k.contains("security-reviewer"))
.cloned()
.collect();
for k in keys {
fx.members.remove(&k);
}
let owner = fx.owner.clone();
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"security-reviewer",
&owner.cert_der,
&owner.key_pkcs8,
None,
fx.now,
)
.unwrap();
fx.policy.minimum_distinct_approvers = 1;
fx.resign();
let o = verify_bundle(&fx.pack(), &fx.params());
assert_refused(
&o,
ec::UPDATE_ROLE_MISSING.code,
"required approval role missing",
);
assert!(fx.holger(&fx.pack()).is_err());
let _ = owner_fp;
}
#[test]
fn a_release_signer_who_lies_about_the_candidate_digest_is_refused() {
let mut fx = good();
let rel = fx.release.clone();
fx.resign_with_digest(&rel, &"0".repeat(64));
both_refuse(
&fx,
&ec::UPDATE_CANDIDATE_DIGEST_MISMATCH,
"candidate digest",
);
}
#[test]
fn a_bundle_released_over_a_block_verdict_is_refused_though_every_signature_is_genuine() {
let spec = Spec {
bundle_id: "bundle-blocked".into(),
..Spec::default()
};
let now = t0();
let mut prep = preparation(&spec, now);
prep.policy_result.verdict = "block".into();
let cand = prep.build().unwrap();
let mut fx = good();
fx.members = cand.members;
fx.bundle_id = spec.bundle_id.clone();
let (r, o) = (fx.reviewer.clone(), fx.owner.clone());
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"security-reviewer",
&r.cert_der,
&r.key_pkcs8,
None,
now,
)
.unwrap();
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"system-owner",
&o.cert_der,
&o.key_pkcs8,
None,
now,
)
.unwrap();
fx.resign();
both_refuse(&fx, &ec::UPDATE_VERDICT_NOT_PASS, "\"block\"");
}
#[test]
fn a_release_signed_by_an_unlisted_key_is_an_untrusted_release_signer() {
let mut fx = good();
let mallory = self_signed("mallory");
fx.resign_with(&mallory);
both_refuse(
&fx,
&ec::UPDATE_UNTRUSTED_RELEASE_SIGNER,
&mallory.fingerprint,
);
}
#[test]
fn a_rooted_chain_leaf_from_a_foreign_ca_is_untrusted() {
let mut fx = build(Spec {
rooted: true,
..Spec::default()
});
let foreign = ca("foreign-ca");
let mallory = leaf("release-signer", &foreign);
fx.policy
.trusted_release_signers
.push(mallory.fingerprint.clone());
fx.resign_with(&mallory);
both_refuse(
&fx,
&ec::UPDATE_UNTRUSTED_RELEASE_SIGNER,
&mallory.fingerprint,
);
}
#[test]
fn a_leaf_issued_by_a_trusted_root_that_is_not_a_ca_is_untrusted() {
let mut fx = build(Spec {
rooted: true,
..Spec::default()
});
let fake_root = non_ca_root("not-a-ca");
fx.policy.trusted_roots.push(hex(&fake_root.der));
let mallory = leaf("release-signer", &fake_root);
fx.policy
.trusted_release_signers
.push(mallory.fingerprint.clone());
fx.resign_with(&mallory);
both_refuse(
&fx,
&ec::UPDATE_UNTRUSTED_RELEASE_SIGNER,
&mallory.fingerprint,
);
}
#[test]
fn an_envelope_that_disagrees_with_core_json_is_refused() {
let mut fx = good();
let m = fx.members.get_mut("manifest.json").unwrap();
*m = String::from_utf8(m.clone())
.unwrap()
.replace("\"sequence\": 5", "\"sequence\": 6")
.into_bytes();
fx.resign();
both_refuse(&fx, &ec::UPDATE_ENVELOPE_MISMATCH, "manifest.json");
}