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");
}