nordisk/heimdall
guests/verify-bundle/tests/policy_and_params.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The caller's inputs: Holger's trust policy, the instant, the target and the
//! anti-rollback state. The guest has no clock: every time decision rests on
//! the `now` it is handed, and a bad parameter document is a refusal.
mod common;
use common::*;
use chrono::{Duration, TimeZone, Utc};
use holger_errcode as ec;
use holger_update_bundle::{CertificateValidity, TrustMode};
const PARAMS: &str = "heimdall-bundle-1";
fn run_at(fx: &Fixture, now: chrono::DateTime<Utc>, extra: &str) -> Outcome {
verify_bundle(&fx.pack(), ¶ms_for_policy(&fx.policy, now, extra))
}
// ------------------------------------------------------------------ time
#[test]
fn the_same_bundle_is_accepted_before_its_expiry_and_refused_after_it() {
let fx = good();
assert!(run_at(&fx, t0() + Duration::days(29), "").is_accepted());
let o = run_at(&fx, t0() + Duration::days(31), "");
assert_refused(&o, ec::UPDATE_EXPIRED.code, "expired");
}
#[test]
fn a_bundle_older_than_the_policy_maximum_age_is_refused() {
let mut fx = good();
fx.policy.maximum_bundle_age_secs = Some(3600);
assert!(run_at(&fx, t0() + Duration::minutes(59), "").is_accepted());
assert_refused(
&run_at(&fx, t0() + Duration::hours(2), ""),
ec::UPDATE_TOO_OLD.code,
"maximum age",
);
}
#[test]
fn a_bundle_without_an_expiry_is_refused_when_the_policy_requires_one() {
let mut fx = build(Spec {
expires_in: None,
..Spec::default()
});
fx.policy.require_expiry = true;
assert_refused(&fx.guest(), ec::UPDATE_EXPIRY_MISSING.code, "expiry");
}
fn windowed_release(nb: (i32, u32, u32), na: (i32, u32, u32)) -> (Fixture, Signer) {
let mut fx = good();
let w = (
Utc.with_ymd_and_hms(nb.0, nb.1, nb.2, 0, 0, 0).unwrap(),
Utc.with_ymd_and_hms(na.0, na.1, na.2, 0, 0, 0).unwrap(),
);
let s = self_signed_window("release-signer", Some(w));
fx.policy.trusted_release_signers = vec![s.fingerprint.clone()];
fx.resign_with(&s);
(fx, s)
}
#[test]
fn a_release_certificate_that_expired_is_refused() {
let (fx, s) = windowed_release((2020, 1, 1), (2021, 1, 1));
assert_refused(
&fx.guest(),
ec::UPDATE_CERTIFICATE_EXPIRED.code,
&s.fingerprint,
);
}
#[test]
fn a_release_certificate_not_yet_in_force_is_refused() {
let (fx, s) = windowed_release((2030, 1, 1), (2031, 1, 1));
assert_refused(
&fx.guest(),
ec::UPDATE_CERTIFICATE_NOT_YET_VALID.code,
&s.fingerprint,
);
}
#[test]
fn an_expired_certificate_is_accepted_only_when_the_policy_waives_the_clock_by_name() {
let (mut fx, _) = windowed_release((2020, 1, 1), (2021, 1, 1));
fx.policy.certificate_validity = CertificateValidity::IgnoreUnreliableClock;
assert!(fx.guest().is_accepted());
}
#[test]
fn an_approver_certificate_that_expired_is_refused() {
let mut fx = good();
let w = (
Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(),
);
let old = self_signed_window("security-reviewer", Some(w));
fx.policy.approved_signers[0].fingerprint = old.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);
}
holger_update_bundle::finalize::add_approval(
&mut fx.members,
"security-reviewer",
&old.cert_der,
&old.key_pkcs8,
None,
fx.now,
)
.unwrap();
fx.resign();
assert_refused(
&fx.guest(),
ec::UPDATE_CERTIFICATE_EXPIRED.code,
&old.fingerprint,
);
}
// ------------------------------------------------------- evidence, target
#[test]
fn a_required_vex_that_the_bundle_lacks_is_refused() {
let mut fx = good();
fx.policy.require_vex = true;
assert_refused(&fx.guest(), ec::UPDATE_EVIDENCE_MISSING.code, "vex");
}
#[test]
fn a_required_scan_that_the_bundle_lacks_is_refused() {
let mut fx = good();
fx.policy.require_scan = true;
assert_refused(&fx.guest(), ec::UPDATE_EVIDENCE_MISSING.code, "scan");
}
#[test]
fn a_bundle_for_another_product_is_refused() {
let fx = good();
let o = verify_bundle(
&fx.pack(),
&fx.params_with("target: (product: Some(\"OtherDrive\"))"),
);
assert_refused(&o, ec::UPDATE_TARGET_MISMATCH.code, "OtherDrive");
let ok = verify_bundle(&fx.pack(), &fx.params_with("target: (product: Some(\"ExampleDrive\"), model: Some(\"ACS880\"), revision: Some(\"R4\"))"));
assert!(ok.is_accepted(), "{:?}", ok.reason());
}
#[test]
fn a_bundle_for_another_model_or_revision_is_refused() {
let fx = good();
let m = verify_bundle(
&fx.pack(),
&fx.params_with("target: (model: Some(\"ACS999\"))"),
);
assert_refused(&m, ec::UPDATE_TARGET_MISMATCH.code, "ACS999");
let r = verify_bundle(
&fx.pack(),
&fx.params_with("target: (revision: Some(\"R9\"))"),
);
assert_refused(&r, ec::UPDATE_TARGET_MISMATCH.code, "R9");
}
#[test]
fn a_product_outside_the_policy_allow_list_is_refused() {
let mut fx = good();
fx.policy.allowed_products = vec!["SomethingElse".into()];
assert_refused(&fx.guest(), ec::UPDATE_TARGET_MISMATCH.code, "allow-list");
fx.policy.allowed_products = vec![];
fx.policy.allowed_models = vec!["NOPE".into()];
assert_refused(&fx.guest(), ec::UPDATE_TARGET_MISMATCH.code, "allow-list");
}
// --------------------------------------------------------- anti-rollback
fn with_accepted(fx: &Fixture, entries: &str) -> Outcome {
verify_bundle(
&fx.pack(),
&fx.params_with(&format!("accepted: Some([{entries}])")),
)
}
#[test]
fn a_sequence_older_than_the_accepted_one_is_a_rollback() {
let fx = good();
let o = with_accepted(&fx, "(sequence: 9, bundle_id: \"b9\", core_digest: \"x\")");
assert_refused(
&o,
ec::UPDATE_SEQUENCE_ROLLBACK.code,
"not newer than the accepted 9",
);
}
#[test]
fn the_same_sequence_with_different_content_is_a_replay() {
let fx = good();
let o = with_accepted(
&fx,
"(sequence: 5, bundle_id: \"other\", core_digest: \"x\")",
);
assert_refused(&o, ec::UPDATE_SEQUENCE_REPLAY.code, "sequence 5");
}
#[test]
fn a_bundle_id_reused_with_different_content_is_refused() {
let fx = good();
let o = with_accepted(
&fx,
"(sequence: 1, bundle_id: \"bundle-0005\", core_digest: \"x\")",
);
assert_refused(&o, ec::UPDATE_BUNDLE_ID_REUSE.code, "bundle-0005");
}
#[test]
fn re_importing_the_exact_accepted_bundle_is_accepted_and_a_newer_one_too() {
let fx = good();
let same = format!(
"(sequence: 5, bundle_id: \"bundle-0005\", core_digest: \"{}\")",
fx.candidate_digest
);
assert!(with_accepted(&fx, &same).is_accepted());
assert!(
with_accepted(&fx, "(sequence: 4, bundle_id: \"b4\", core_digest: \"x\")").is_accepted()
);
assert!(with_accepted(&fx, "").is_accepted());
}
// ----------------------------------------------------------- parameters
#[test]
fn parameters_that_are_not_ron_are_refused() {
let fx = good();
assert_refused(&verify_bundle(&fx.pack(), b"{\"now\": 1}"), PARAMS, "");
assert_refused(&verify_bundle(&fx.pack(), b""), PARAMS, "");
}
#[test]
fn parameters_that_are_not_utf8_are_refused() {
let fx = good();
assert_refused(
&verify_bundle(&fx.pack(), &[0xff, 0xfe, 0x00]),
PARAMS,
"UTF-8",
);
}
#[test]
fn parameters_without_a_now_are_refused_because_the_guest_has_no_clock() {
let fx = good();
let p = format!("VerifyParams(policy: {})", fx.policy.to_ron().unwrap());
assert_refused(&verify_bundle(&fx.pack(), p.as_bytes()), PARAMS, "now");
}
#[test]
fn an_oversized_parameter_document_is_refused() {
let fx = good();
let mut p = fx.params();
p.extend(std::iter::repeat_n(b' ', 1024 * 1024));
assert_refused(&verify_bundle(&fx.pack(), &p), PARAMS, "cap");
}
#[test]
fn a_policy_that_states_no_trust_mode_is_refused_not_defaulted() {
let mut fx = good();
fx.policy.trust_mode = None;
assert_refused(&fx.guest(), ec::UPDATE_TRUST_MODE_UNSET.code, "trust_mode");
}
#[test]
fn a_rooted_chain_policy_with_no_roots_is_refused() {
let mut fx = good();
fx.policy.trust_mode = Some(TrustMode::RootedChain);
assert_refused(
&fx.guest(),
ec::UPDATE_TRUST_ROOTS_EMPTY.code,
"no trusted root",
);
}
#[test]
fn a_pinned_policy_that_lists_roots_is_refused_as_incoherent() {
let mut fx = good();
fx.policy.trusted_roots = vec![hex(&fx.release.cert_der)];
assert_refused(&fx.guest(), ec::UPDATE_POLICY.code, "PinnedSigners");
}
#[test]
fn a_policy_of_another_schema_is_refused() {
let mut fx = good();
fx.policy.schema_version = "holger.bundle-trust-policy/v2".into();
assert_refused(&fx.guest(), ec::UPDATE_POLICY_SCHEMA_UNSUPPORTED.code, "v2");
}
#[test]
fn a_root_that_is_not_hex_is_refused() {
let mut fx = build(Spec {
rooted: true,
..Spec::default()
});
fx.policy.trusted_roots.push("zz-not-hex".into());
assert_refused(&fx.guest(), ec::UPDATE_POLICY.code, "not valid hex");
}
#[test]
fn a_root_that_is_hex_but_no_certificate_is_refused() {
let mut fx = build(Spec {
rooted: true,
..Spec::default()
});
fx.policy.trusted_roots.push("00ff".into());
assert_refused(&fx.guest(), ec::UPDATE_CERTIFICATE.code, "certificate");
}
#[test]
fn an_empty_policy_trusts_nobody() {
let fx = good();
let p = format!(
"VerifyParams(now: \"{}\", policy: (schema_version: \"holger.bundle-trust-policy/v1\", trust_mode: Some(PinnedSigners)))",
t0().to_rfc3339()
);
let o = verify_bundle(&fx.pack(), p.as_bytes());
assert_refused(
&o,
ec::UPDATE_UNTRUSTED_RELEASE_SIGNER.code,
&fx.release.fingerprint,
);
}
// ------------------------------------------------- Heimdall's string pairs
fn pairs_for(fx: &Fixture) -> Vec<(String, String)> {
vec![
("now".into(), t0().to_rfc3339()),
("policy".into(), fx.policy.to_ron().unwrap()),
]
}
fn run_pairs(fx: &Fixture, pairs: &[(String, String)]) -> Outcome {
heimdall_guest_verify_bundle::verify_bundle_pairs(
&fx.pack(),
pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())),
)
}
#[test]
fn string_pair_parameters_carry_the_same_decision_as_the_ron_document() {
let fx = good();
let o = run_pairs(&fx, &pairs_for(&fx));
assert!(o.is_accepted(), "{:?}", o.reason());
assert_eq!(o, fx.guest());
let mut p = pairs_for(&fx);
p.push(("product".into(), "OtherDrive".into()));
assert_refused(
&run_pairs(&fx, &p),
ec::UPDATE_TARGET_MISMATCH.code,
"OtherDrive",
);
let mut p = pairs_for(&fx);
p.push((
"accepted".into(),
"[(sequence: 9, bundle_id: \"b9\", core_digest: \"x\")]".into(),
));
assert_refused(&run_pairs(&fx, &p), ec::UPDATE_SEQUENCE_ROLLBACK.code, "9");
let mut p = pairs_for(&fx);
p.push(("emit".into(), "Report".into()));
assert_eq!(run_pairs(&fx, &p).emitted.len(), 1);
}
#[test]
fn string_pair_parameters_refuse_an_unknown_key_a_missing_now_and_a_missing_policy() {
let fx = good();
let mut p = pairs_for(&fx);
p.push(("trust_everything".into(), "yes".into()));
assert_refused(&run_pairs(&fx, &p), PARAMS, "unknown key");
assert_refused(&run_pairs(&fx, &pairs_for(&fx)[1..]), PARAMS, "now");
assert_refused(&run_pairs(&fx, &pairs_for(&fx)[..1]), PARAMS, "policy");
let p = vec![
("now".to_string(), "yesterday".to_string()),
pairs_for(&fx)[1].clone(),
];
assert_refused(&run_pairs(&fx, &p), PARAMS, "now");
}