Gunnar

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(), &params_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");
}