use std::path::{Path, PathBuf};
use std::process::Command;
use chrono::{DateTime, Duration, Utc};
use cortex_verifier::{
AuthorityDomain, IndependentWitness, WitnessClass, WitnessPayload, WitnessSignature,
WitnessTier,
};
use ed25519_dalek::{Signer, SigningKey};
use serde_json::{json, Value};
fn cortex_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}
fn run(args: &[&str]) -> std::process::Output {
Command::new(cortex_bin())
.env_remove("RUST_LOG")
.args(args)
.output()
.expect("spawn cortex")
}
fn authority_grade_input() -> String {
json!({
"evidence_kind": "remote_ci",
"runtime_mode": "authority_grade",
"authority_class": "operator",
"proof_state": "full_chain_verified",
"claim_ceiling": "authority_grade",
"policy_outcome": "allow",
"source_refs": ["signed://fixture/release-readiness"]
})
.to_string()
}
fn blake3_hex(input: &str) -> String {
blake3::hash(input.as_bytes()).to_hex().to_string()
}
fn signing_key(seed: u8) -> SigningKey {
let mut bytes = [0u8; 32];
bytes[0] = seed;
bytes[31] = seed.wrapping_add(0x5A);
SigningKey::from_bytes(&bytes)
}
fn sample_payload(class: WitnessClass) -> WitnessPayload {
let stamp =
DateTime::<Utc>::from_timestamp(1_715_500_000, 0).expect("constant fits in chrono range");
match class {
WitnessClass::SignedLedgerChainHead => WitnessPayload::SignedLedgerChainHead {
chain_head_hash: blake3::hash(b"chain-head").to_hex().to_string(),
event_count: 42,
},
WitnessClass::ExternalAnchorCrossing => WitnessPayload::ExternalAnchorCrossing {
chain_head_hash: blake3::hash(b"chain-head").to_hex().to_string(),
event_count: 42,
sink_kind: "branch_protection".to_string(),
},
WitnessClass::RemoteCiConclusion => WitnessPayload::RemoteCiConclusion {
workflow_run_id: "ci-run-1".to_string(),
commit_sha: "0".repeat(40),
conclusion_timestamp: stamp,
},
WitnessClass::ReproducibleBuildProvenance => WitnessPayload::ReproducibleBuildProvenance {
builder_id: "https://slsa-builder.example/v1".to_string(),
source_digest: blake3::hash(b"source").to_hex().to_string(),
artifact_digest: blake3::hash(b"artifact").to_hex().to_string(),
},
}
}
fn build_witness(
seed: u8,
class: WitnessClass,
tier: WitnessTier,
subject_digest: &str,
asserted_at: DateTime<Utc>,
) -> IndependentWitness {
let key = signing_key(seed);
let domain = class.required_authority_domain();
let payload = sample_payload(class);
let preimage = payload.canonical_preimage(domain, subject_digest, asserted_at);
let signature = key.sign(&preimage);
IndependentWitness {
class,
authority_domain: domain,
tier,
asserted_at,
asserted_subject_blake3: subject_digest.to_string(),
signature: WitnessSignature::Ed25519 {
public_key_bytes: key.verifying_key().to_bytes(),
signature_bytes: signature.to_bytes(),
signer_id: Some(format!("test-{}", class.wire_str())),
},
payload,
}
}
fn override_domain(witness: &mut IndependentWitness, seed: u8, domain: AuthorityDomain) {
witness.authority_domain = domain;
let key = signing_key(seed);
let preimage = witness.payload.canonical_preimage(
witness.authority_domain,
&witness.asserted_subject_blake3,
witness.asserted_at,
);
let signature = key.sign(&preimage);
match &mut witness.signature {
WitnessSignature::Ed25519 {
public_key_bytes,
signature_bytes,
..
} => {
*public_key_bytes = key.verifying_key().to_bytes();
*signature_bytes = signature.to_bytes();
}
other => panic!("override_domain: expected Ed25519, got {other:?}"),
}
}
fn full_witness_set(subject_digest: &str, asserted_at: DateTime<Utc>) -> Vec<IndependentWitness> {
vec![
build_witness(
1,
WitnessClass::SignedLedgerChainHead,
WitnessTier::OperatorOwned,
subject_digest,
asserted_at,
),
build_witness(
2,
WitnessClass::ExternalAnchorCrossing,
WitnessTier::OperatorOwned,
subject_digest,
asserted_at,
),
build_witness(
3,
WitnessClass::RemoteCiConclusion,
WitnessTier::ThirdParty,
subject_digest,
asserted_at,
),
build_witness(
4,
WitnessClass::ReproducibleBuildProvenance,
WitnessTier::ThirdParty,
subject_digest,
asserted_at,
),
]
}
fn witnesses_to_json(witnesses: &[IndependentWitness]) -> Vec<String> {
witnesses
.iter()
.map(|w| serde_json::to_string(w).expect("witness serializes"))
.collect()
}
fn assert_exit(out: &std::process::Output, expected: i32) {
let code = out.status.code().expect("process exited via signal");
assert_eq!(
code,
expected,
"expected exit {expected}, got {code}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn all_four_witnesses_promote_to_full_chain_verified_and_exit_ok() {
let input = authority_grade_input();
let digest = blake3_hex(&input);
let witnesses = full_witness_set(&digest, Utc::now());
let json_witnesses = witnesses_to_json(&witnesses);
let mut cli = vec![
"release",
"readiness",
"--evidence-json",
&input,
"--evidence-blake3",
&digest,
];
for w in &json_witnesses {
cli.push("--witness-json");
cli.push(w.as_str());
}
let out = run(&cli);
assert_exit(&out, 0);
let report: Value =
serde_json::from_slice(&out.stderr).expect("stderr is JSON preflight report");
assert_eq!(report["command"], "release.readiness");
assert_eq!(report["trusted_artifact_emitted"], true);
assert_eq!(report["trusted_stdout_artifact"], true);
assert_eq!(report["independent_verification"], true);
assert_eq!(report["claim_allowed"], true);
assert_eq!(report["verifier"]["invoked"], true);
assert_eq!(report["verifier"]["state"], "full_chain_verified");
assert_eq!(report["verifier"]["ceiling"], "authority_grade");
assert_eq!(
report["verifier"]["witnesses"]
.as_array()
.map(Vec::len)
.unwrap_or(0),
4
);
}
#[test]
fn three_of_four_witnesses_yield_partial_and_advisory_exit() {
let input = authority_grade_input();
let digest = blake3_hex(&input);
let mut witnesses = full_witness_set(&digest, Utc::now());
witnesses.pop();
let json_witnesses = witnesses_to_json(&witnesses);
let mut cli = vec![
"release",
"readiness",
"--evidence-json",
&input,
"--evidence-blake3",
&digest,
];
for w in &json_witnesses {
cli.push("--witness-json");
cli.push(w.as_str());
}
let out = run(&cli);
assert_exit(&out, 7);
let report: Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(report["trusted_artifact_emitted"], false);
assert_eq!(report["verifier"]["state"], "partial");
let reasons = report["verifier"]["reasons"]
.as_array()
.expect("verifier.reasons is array");
assert!(reasons.iter().any(|r| r
.as_str()
.unwrap_or("")
.contains("reproducible_build_provenance")));
assert_eq!(
report["evidence_input"]["failed_rule"],
"release.evidence_input.independent_verification_partial"
);
}
#[test]
fn two_witnesses_sharing_authority_domain_break_with_overlap_invariant() {
let input = authority_grade_input();
let digest = blake3_hex(&input);
let now = Utc::now();
let mut witnesses = full_witness_set(&digest, now);
override_domain(&mut witnesses[1], 2, AuthorityDomain::LocalSignedLedger);
let json_witnesses = witnesses_to_json(&witnesses);
let mut cli = vec![
"release",
"readiness",
"--evidence-json",
&input,
"--evidence-blake3",
&digest,
];
for w in &json_witnesses {
cli.push("--witness-json");
cli.push(w.as_str());
}
let out = run(&cli);
assert_exit(&out, 7);
let report: Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(report["trusted_artifact_emitted"], false);
assert_eq!(report["verifier"]["state"], "broken");
assert_eq!(
report["verifier"]["broken_edge"]["invariant"],
"verifier.witness.authority_overlap"
);
assert_eq!(
report["evidence_input"]["failed_rule"],
"release.evidence_input.independent_verification_broken"
);
}
#[test]
fn stale_witness_is_rejected_with_witness_stale_invariant() {
let input = authority_grade_input();
let digest = blake3_hex(&input);
let asserted_at = Utc::now() - Duration::days(7);
let witnesses = full_witness_set(&digest, asserted_at);
let json_witnesses = witnesses_to_json(&witnesses);
let mut cli = vec![
"release",
"readiness",
"--evidence-json",
&input,
"--evidence-blake3",
&digest,
];
for w in &json_witnesses {
cli.push("--witness-json");
cli.push(w.as_str());
}
let out = run(&cli);
assert_exit(&out, 7);
let report: Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(report["trusted_artifact_emitted"], false);
assert_eq!(report["verifier"]["state"], "broken");
assert_eq!(
report["verifier"]["broken_edge"]["invariant"],
"verifier.witness.stale"
);
}
#[test]
fn evidence_digest_mismatch_witness_is_rejected_with_disagreement_invariant() {
let input = authority_grade_input();
let digest = blake3_hex(&input);
let now = Utc::now();
let other_digest = "1".repeat(64);
let witnesses = full_witness_set(&other_digest, now);
let json_witnesses = witnesses_to_json(&witnesses);
let mut cli = vec![
"release",
"readiness",
"--evidence-json",
&input,
"--evidence-blake3",
&digest,
];
for w in &json_witnesses {
cli.push("--witness-json");
cli.push(w.as_str());
}
let out = run(&cli);
assert_exit(&out, 7);
let report: Value = serde_json::from_slice(&out.stderr).expect("stderr is JSON");
assert_eq!(report["trusted_artifact_emitted"], false);
assert_eq!(report["verifier"]["state"], "broken");
assert_eq!(
report["verifier"]["broken_edge"]["invariant"],
"verifier.witness.disagreement"
);
}
const VERIFIER_BINDING_FIXTURE_DIR: &str = "tests/fixtures/verifier-binding";
const EVIDENCE_FIXTURE_NAME: &str = "evidence-d1.json";
const WITNESS_FIXTURE_NAMES: [&str; 4] = [
"witness-signed-ledger-d2.json",
"witness-external-anchor-d2.json",
"witness-remote-ci-d2.json",
"witness-reproducible-build-d2.json",
];
const FALSE_SUBJECT_BLAKE3: &str =
"1111111111111111111111111111111111111111111111111111111111111111";
fn verifier_binding_fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(VERIFIER_BINDING_FIXTURE_DIR)
}
fn read_fixture_bytes(name: &str) -> Vec<u8> {
let path = verifier_binding_fixture_dir().join(name);
std::fs::read(&path).unwrap_or_else(|err| {
panic!(
"fixture {} is missing or unreadable: {err}. \
Regenerate with: cargo test -p cortex --test cli_release_readiness -- \
--ignored regenerate_verifier_binding_fixtures",
path.display()
)
})
}
#[test]
fn release_readiness_rejects_witnesses_whose_subject_blake3_does_not_match_evidence_input() {
let fixture_dir = verifier_binding_fixture_dir();
let evidence_path = fixture_dir.join(EVIDENCE_FIXTURE_NAME);
let evidence_bytes = read_fixture_bytes(EVIDENCE_FIXTURE_NAME);
let d1 = blake3::hash(&evidence_bytes).to_hex().to_string();
assert_ne!(
d1, FALSE_SUBJECT_BLAKE3,
"fixture invariant violated: D1 must differ from D2 for the subject-binding test to be meaningful"
);
for witness_name in WITNESS_FIXTURE_NAMES {
let witness_bytes = read_fixture_bytes(witness_name);
let parsed: Value = serde_json::from_slice(&witness_bytes)
.unwrap_or_else(|err| panic!("witness fixture {witness_name} is not JSON: {err}"));
assert_eq!(
parsed["asserted_subject_blake3"].as_str(),
Some(FALSE_SUBJECT_BLAKE3),
"witness fixture {witness_name} must declare the false subject D2 \
(regenerate via --ignored regenerate_verifier_binding_fixtures)"
);
}
let witness_paths: Vec<PathBuf> = WITNESS_FIXTURE_NAMES
.iter()
.map(|name| fixture_dir.join(name))
.collect();
let mut cli: Vec<String> = vec![
"release".into(),
"readiness".into(),
"--evidence-file".into(),
evidence_path.to_string_lossy().into_owned(),
"--evidence-blake3".into(),
d1.clone(),
];
for witness_path in &witness_paths {
cli.push("--witness-file".into());
cli.push(witness_path.to_string_lossy().into_owned());
}
let cli_refs: Vec<&str> = cli.iter().map(String::as_str).collect();
let out = run(&cli_refs);
assert_exit(&out, 7);
let stderr = String::from_utf8(out.stderr.clone()).expect("stderr is UTF-8");
assert!(
stderr.contains("verifier.witness.disagreement"),
"stderr MUST contain stable invariant `verifier.witness.disagreement` verbatim; got: {stderr}"
);
let report: Value = serde_json::from_str(&stderr).unwrap_or_else(|err| {
panic!("stderr is not a JSON preflight report: {err}\nstderr: {stderr}")
});
assert_eq!(report["command"], "release.readiness");
assert_eq!(
report["trusted_artifact_emitted"], false,
"subject-binding mismatch MUST NOT emit a trusted artifact"
);
assert_eq!(
report["independent_verification"], false,
"subject-binding mismatch MUST report independent_verification=false"
);
assert_eq!(report["trusted_stdout_artifact"], false);
assert_eq!(report["artifact_emitted"], false);
assert_eq!(report["verifier"]["invoked"], true);
assert_eq!(report["verifier"]["state"], "broken");
assert_eq!(
report["verifier"]["broken_edge"]["invariant"],
"verifier.witness.disagreement"
);
let detail = report["verifier"]["broken_edge"]["detail"]
.as_str()
.expect("broken_edge.detail is a string");
assert!(
detail.contains(FALSE_SUBJECT_BLAKE3),
"broken_edge.detail MUST quote the false subject D2; got: {detail}"
);
assert!(
detail.contains(&d1),
"broken_edge.detail MUST quote the producer-supplied subject D1; got: {detail}"
);
assert_eq!(
report["evidence_input"]["failed_rule"],
"release.evidence_input.independent_verification_broken"
);
assert_eq!(report["evidence_input"]["accepted"], false);
assert_eq!(report["evidence_input"]["trusted_artifact_emitted"], false);
}
fn write_fixture_atomic(path: &Path, bytes: &[u8]) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.unwrap_or_else(|err| panic!("create_dir_all({}) failed: {err}", parent.display()));
}
std::fs::write(path, bytes)
.unwrap_or_else(|err| panic!("write fixture {} failed: {err}", path.display()));
}
#[test]
#[ignore = "regenerator: writes committed fixtures; run explicitly via --ignored"]
fn regenerate_verifier_binding_fixtures() {
let dir = verifier_binding_fixture_dir();
let evidence_input = json!({
"evidence_kind": "remote_ci",
"runtime_mode": "authority_grade",
"authority_class": "operator",
"proof_state": "full_chain_verified",
"claim_ceiling": "authority_grade",
"policy_outcome": "allow",
"source_refs": ["signed://fixture/verifier-binding/d1-evidence"]
});
let evidence_bytes =
serde_json::to_vec(&evidence_input).expect("evidence input serializes to JSON");
write_fixture_atomic(&dir.join(EVIDENCE_FIXTURE_NAME), &evidence_bytes);
let asserted_at =
DateTime::<Utc>::from_timestamp(1_715_500_000, 0).expect("constant fits in chrono range");
let witnesses: [(u8, WitnessClass, WitnessTier, &str); 4] = [
(
21,
WitnessClass::SignedLedgerChainHead,
WitnessTier::OperatorOwned,
WITNESS_FIXTURE_NAMES[0],
),
(
22,
WitnessClass::ExternalAnchorCrossing,
WitnessTier::OperatorOwned,
WITNESS_FIXTURE_NAMES[1],
),
(
23,
WitnessClass::RemoteCiConclusion,
WitnessTier::ThirdParty,
WITNESS_FIXTURE_NAMES[2],
),
(
24,
WitnessClass::ReproducibleBuildProvenance,
WitnessTier::ThirdParty,
WITNESS_FIXTURE_NAMES[3],
),
];
for (seed, class, tier, name) in witnesses {
let witness = build_witness(seed, class, tier, FALSE_SUBJECT_BLAKE3, asserted_at);
let bytes = serde_json::to_vec_pretty(&witness).expect("witness serializes to JSON");
write_fixture_atomic(&dir.join(name), &bytes);
}
}