Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

update-bundle/src/tests/approvals.rs

//! Approval-policy adversarial tests. These drive the approval + two-person logic
//! directly (before finalize) to isolate it from the release-manifest coverage.

use chrono::Utc;

use super::testkit::*;
use crate::approval::{ApprovalStatement, Decision};
use crate::finalize::{add_approval, candidate_digest};
use crate::policy::{ApprovedSigner, CertificateValidity, ExpiredPosture, TrustMode, TrustPolicy};
use crate::schema::POLICY_SCHEMA;
use crate::signer::{sign_pkcs8, SignerCert};
use crate::verify::{collect_roots, enforce_two_person, verify_approvals};
use crate::MemberMap;

fn candidate() -> MemberMap {
    base_preparation(5, "bundle-0005", Utc::now())
        .build()
        .unwrap()
        .members
}

fn check(m: &MemberMap, policy: &TrustPolicy, release_fp: &str) -> crate::Result<usize> {
    let dg = candidate_digest(m).unwrap();
    let roots = collect_roots(policy)?;
    let approvals = verify_approvals(m, policy, &dg, &roots, policy.trust_mode()?, Utc::now())?;
    enforce_two_person(policy, &approvals, release_fp)?;
    Ok(approvals.len())
}

#[test]
fn two_valid_distinct_approvers_pass() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let mut m = candidate();
    add_approval(
        &mut m,
        "security-reviewer",
        &reviewer.cert_der,
        &reviewer.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    let policy = pinned_policy(&reviewer, &owner, &release);
    assert_eq!(check(&m, &policy, &release.fingerprint).unwrap(), 2);
}

#[test]
fn one_approver_fails() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let mut m = candidate();
    add_approval(
        &mut m,
        "security-reviewer",
        &reviewer.cert_der,
        &reviewer.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    let policy = pinned_policy(&reviewer, &owner, &release);
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

#[test]
fn same_certificate_for_two_roles_fails() {
    let dual = self_signed("dual-role");
    let release = self_signed("release-signer");
    let policy = TrustPolicy {
        schema_version: POLICY_SCHEMA.into(),
        trust_mode: Some(TrustMode::PinnedSigners),
        trusted_roots: vec![],
        certificate_validity: CertificateValidity::Enforce,
        approved_signers: vec![ApprovedSigner {
            fingerprint: dual.fingerprint.clone(),
            subject: None,
            roles: vec!["security-reviewer".into(), "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![],
    };
    let mut m = candidate();
    add_approval(
        &mut m,
        "security-reviewer",
        &dual.cert_der,
        &dual.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    add_approval(
        &mut m,
        "system-owner",
        &dual.cert_der,
        &dual.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    // Both statements are individually authorized, but one cert cannot satisfy the
    // two-distinct-approver requirement.
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

#[test]
fn correct_signature_with_unauthorized_role_fails() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let mut m = candidate();
    // The reviewer signs a *system-owner* approval — a valid signature, but the
    // reviewer's fingerprint is not authorized for that role.
    add_approval(
        &mut m,
        "system-owner",
        &reviewer.cert_der,
        &reviewer.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    let policy = pinned_policy(&reviewer, &owner, &release);
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

#[test]
fn approval_for_different_candidate_fails() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let mut m = candidate();
    add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();

    // Hand-craft a reviewer approval bound to the WRONG candidate digest, correctly signed.
    let cert = SignerCert::parse(&reviewer.cert_der).unwrap();
    let base = format!("security-reviewer.{}", reviewer.fingerprint);
    let sig_path = format!("signatures/{base}.sig");
    let stmt = ApprovalStatement {
        candidate_digest: "0".repeat(64),
        approval_role: "security-reviewer".into(),
        signer_certificate_fingerprint: reviewer.fingerprint.clone(),
        signer_subject: cert.subject().to_string(),
        approved_at: Utc::now(),
        decision: Decision::Approve,
        optional_comment: None,
        signature_algorithm: "ed25519".into(),
        signature_path: sig_path.clone(),
    };
    let bytes = stmt.to_json_bytes().unwrap();
    let sig = sign_pkcs8(&reviewer.key_pkcs8, &bytes).unwrap();
    m.insert(format!("approvals/{base}.approval.json"), bytes);
    m.insert(sig_path, sig);
    m.insert(format!("signatures/{base}.crt"), reviewer.cert_der.clone());

    let policy = pinned_policy(&reviewer, &owner, &release);
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

#[test]
fn modified_approval_statement_fails() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let mut m = candidate();
    add_approval(
        &mut m,
        "security-reviewer",
        &reviewer.cert_der,
        &reviewer.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    // Flip a byte in the reviewer's approval statement — signature no longer matches.
    let key = m
        .keys()
        .find(|k| k.starts_with("approvals/security-reviewer."))
        .unwrap()
        .clone();
    m.get_mut(&key).unwrap()[5] ^= 0x01;
    let policy = pinned_policy(&reviewer, &owner, &release);
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

#[test]
fn untrusted_approver_fails() {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let stranger = self_signed("stranger");
    let mut m = candidate();
    // The stranger validly signs a reviewer approval, but is not in the policy.
    add_approval(
        &mut m,
        "security-reviewer",
        &stranger.cert_der,
        &stranger.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        Utc::now(),
    )
    .unwrap();
    let policy = pinned_policy(&reviewer, &owner, &release);
    assert!(check(&m, &policy, &release.fingerprint).is_err());
}

/// An approval signed with a key that is not its certificate's is refused when it is made,
/// and the candidate is left as it was. Before, `add_approval` wrote a statement in the
/// certificate holder's name with a signature nobody could verify, and only the later
/// finalize noticed.
#[test]
fn an_approval_signed_with_another_signers_key_is_refused_and_records_nothing() {
    let mut m = candidate();
    let owner = self_signed("owner");
    let other = self_signed("other");
    let before = m.clone();
    let r = add_approval(
        &mut m,
        "system-owner",
        &owner.cert_der,
        &other.key_pkcs8,
        None,
        Utc::now(),
    );
    assert!(
        matches!(r, Err(crate::BundleError::InvalidApprovalSignature { .. })),
        "a key that is not the certificate's must be refused: {r:?}"
    );
    assert_eq!(m, before, "a refused approval must not touch the candidate");
}