use std::collections::HashSet;
use chrono::{DateTime, Duration, Utc};
use cortex_core::{effective_ceiling, ClaimCeiling, PolicyDecision, PolicyOutcome};
use cortex_runtime::RuntimeClaimKind;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use crate::input::{EvidenceInput, EvidenceKind};
use crate::invariant::{
COMPOSITION_CEILING_BELOW_REQUIRED, COMPOSITION_POLICY_FAIL_CLOSED, WITNESS_AUTHORITY_OVERLAP,
WITNESS_DISAGREEMENT, WITNESS_MISSING, WITNESS_SIGNATURE_INVALID, WITNESS_STALE,
WITNESS_TIER_INSUFFICIENT,
};
use crate::state::{BrokenEdge, VerifiedTrustState};
use crate::witness::{
AuthorityDomain, IndependentWitness, SelfSignedAlgorithm, SelfSignedKeyRegistry, WitnessClass,
WitnessSignature, WitnessSummary, WitnessTier,
};
#[must_use]
pub fn verify(
input: &EvidenceInput,
witnesses: &[IndependentWitness],
now: DateTime<Utc>,
max_age: Duration,
) -> VerifiedTrustState {
verify_with_policy(input, witnesses, now, max_age, None)
}
#[derive(Debug)]
pub struct VerifyOptions<'a> {
pub policy: Option<&'a cortex_core::PolicyDecision>,
pub self_signed_keys: Option<&'a SelfSignedKeyRegistry>,
}
#[must_use]
pub fn verify_with_options(
input: &EvidenceInput,
witnesses: &[IndependentWitness],
now: DateTime<Utc>,
max_age: Duration,
opts: VerifyOptions<'_>,
) -> VerifiedTrustState {
let summaries: Vec<WitnessSummary> =
witnesses.iter().map(WitnessSummary::from_witness).collect();
if let Some(decision) = opts.policy {
if matches!(
decision.final_outcome,
cortex_core::PolicyOutcome::Reject | cortex_core::PolicyOutcome::Quarantine
) {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
COMPOSITION_POLICY_FAIL_CLOSED,
format!(
"policy outcome {:?} fails closed for {:?}",
decision.final_outcome, input.kind
),
),
witnesses: summaries,
};
}
}
verify_inner(
input,
witnesses,
now,
max_age,
opts.self_signed_keys,
summaries,
)
}
#[must_use]
pub fn verify_with_policy(
input: &EvidenceInput,
witnesses: &[IndependentWitness],
now: DateTime<Utc>,
max_age: Duration,
policy: Option<&PolicyDecision>,
) -> VerifiedTrustState {
let summaries: Vec<WitnessSummary> =
witnesses.iter().map(WitnessSummary::from_witness).collect();
if let Some(decision) = policy {
if matches!(
decision.final_outcome,
PolicyOutcome::Reject | PolicyOutcome::Quarantine
) {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
COMPOSITION_POLICY_FAIL_CLOSED,
format!(
"policy outcome {:?} fails closed for {:?}",
decision.final_outcome, input.kind
),
),
witnesses: summaries,
};
}
}
verify_inner(input, witnesses, now, max_age, None, summaries)
}
fn verify_inner(
input: &EvidenceInput,
witnesses: &[IndependentWitness],
now: DateTime<Utc>,
max_age: Duration,
registry: Option<&SelfSignedKeyRegistry>,
summaries: Vec<WitnessSummary>,
) -> VerifiedTrustState {
if input.is_advisory_only() {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_TIER_INSUFFICIENT,
format!(
"evidence input is advisory-only and cannot be promoted by witnesses (runtime_mode={:?}, advisory_only=true)",
input.runtime_mode
),
),
witnesses: summaries,
};
}
for witness in witnesses {
if witness.asserted_subject_blake3 != input.evidence_blake3 {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_DISAGREEMENT,
format!(
"witness class={} asserted subject {} but input declares {}",
witness.class.wire_str(),
witness.asserted_subject_blake3,
input.evidence_blake3,
),
),
witnesses: summaries,
};
}
}
for witness in witnesses {
let expected = witness.class.required_authority_domain();
if witness.authority_domain != expected {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_AUTHORITY_OVERLAP,
format!(
"witness class={} declared authority_domain={} but class requires {}",
witness.class.wire_str(),
witness.authority_domain.wire_str(),
expected.wire_str(),
),
),
witnesses: summaries,
};
}
}
let mut seen_domains: HashSet<AuthorityDomain> = HashSet::new();
for witness in witnesses {
if !seen_domains.insert(witness.authority_domain) {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_AUTHORITY_OVERLAP,
format!(
"two witnesses share authority_domain={}",
witness.authority_domain.wire_str()
),
),
witnesses: summaries,
};
}
}
for witness in witnesses {
let age = now - witness.asserted_at;
if age > max_age {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_STALE,
format!(
"witness class={} is stale: age {}s exceeds max_age {}s",
witness.class.wire_str(),
age.num_seconds(),
max_age.num_seconds(),
),
),
witnesses: summaries,
};
}
}
for witness in witnesses {
if requires_third_party(input.kind, witness.class)
&& witness.tier != WitnessTier::ThirdParty
{
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_TIER_INSUFFICIENT,
format!(
"witness class={} requires tier=third_party for {}, got {}",
witness.class.wire_str(),
input.kind.wire_str(),
witness.tier.wire_str(),
),
),
witnesses: summaries,
};
}
}
for witness in witnesses {
if let Err(detail) = verify_witness_signature(witness, registry) {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
WITNESS_SIGNATURE_INVALID,
format!(
"witness class={} signature did not verify: {}",
witness.class.wire_str(),
detail
),
),
witnesses: summaries,
};
}
}
let required_classes = required_classes_for(input.kind);
let present_classes: HashSet<WitnessClass> = witnesses.iter().map(|w| w.class).collect();
let missing: Vec<WitnessClass> = required_classes
.iter()
.copied()
.filter(|class| !present_classes.contains(class))
.collect();
if !missing.is_empty() {
let reasons: Vec<String> = missing
.iter()
.map(|class| {
format!(
"{}: required witness class {} is missing",
WITNESS_MISSING,
class.wire_str()
)
})
.collect();
return VerifiedTrustState::Partial {
reasons,
witnesses: summaries,
};
}
let runtime_kind = runtime_claim_kind_for(input.kind);
let required_ceiling = runtime_kind.required_ceiling();
let effective = effective_ceiling(
input.runtime_mode,
input.authority_class,
input.proof_state,
input.requested_ceiling,
);
if effective < required_ceiling {
return VerifiedTrustState::Broken {
edge: BrokenEdge::new(
COMPOSITION_CEILING_BELOW_REQUIRED,
format!(
"effective ceiling {effective:?} is below required ceiling {required_ceiling:?} for {runtime_kind:?}"
),
),
witnesses: summaries,
};
}
VerifiedTrustState::FullChainVerified {
ceiling: effective,
witnesses: summaries,
}
}
fn required_classes_for(kind: EvidenceKind) -> &'static [WitnessClass] {
match kind {
EvidenceKind::ReleaseReadiness | EvidenceKind::ComplianceEvidence => &[
WitnessClass::SignedLedgerChainHead,
WitnessClass::ExternalAnchorCrossing,
WitnessClass::RemoteCiConclusion,
WitnessClass::ReproducibleBuildProvenance,
],
}
}
const fn runtime_claim_kind_for(kind: EvidenceKind) -> RuntimeClaimKind {
match kind {
EvidenceKind::ReleaseReadiness => RuntimeClaimKind::ReleaseReadiness,
EvidenceKind::ComplianceEvidence => RuntimeClaimKind::ComplianceEvidence,
}
}
fn requires_third_party(kind: EvidenceKind, class: WitnessClass) -> bool {
matches!(
(kind, class),
(
EvidenceKind::ReleaseReadiness | EvidenceKind::ComplianceEvidence,
WitnessClass::RemoteCiConclusion | WitnessClass::ReproducibleBuildProvenance,
)
)
}
fn verify_witness_signature(
witness: &IndependentWitness,
registry: Option<&SelfSignedKeyRegistry>,
) -> Result<(), String> {
if witness.payload.class() != witness.class {
return Err(format!(
"payload class {} does not match declared class {}",
witness.payload.class().wire_str(),
witness.class.wire_str()
));
}
match &witness.signature {
WitnessSignature::Ed25519 {
public_key_bytes,
signature_bytes,
..
} => {
let key = VerifyingKey::from_bytes(public_key_bytes)
.map_err(|err| format!("public_key_bytes is not a valid Ed25519 point: {err}"))?;
let sig = Signature::from_bytes(signature_bytes);
let preimage = witness.canonical_preimage();
key.verify(&preimage, &sig)
.map_err(|err| format!("Ed25519 verification failed: {err}"))?;
Ok(())
}
WitnessSignature::EcdsaP256 { .. } => {
Err("UnsupportedAlgorithm: EcdsaP256 verification not yet implemented at the verifier layer".to_string())
}
WitnessSignature::SelfSigned {
key_id,
signature_bytes,
} => {
let reg = registry.ok_or_else(|| {
format!(
"SelfSigned key_id={key_id}: no SelfSignedKeyRegistry supplied \
(pass --witness-key-registry to provide one)"
)
})?;
let entry = reg.get(key_id).ok_or_else(|| {
format!(
"SelfSigned key_id={key_id} is not in SelfSignedKeyRegistry \
(registry contains {} entries)",
reg.len()
)
})?;
let key_bytes = entry.key_bytes().map_err(|e| {
format!("SelfSigned key_id={key_id}: malformed key_bytes_hex in registry: {e}")
})?;
let preimage = witness.canonical_preimage();
match entry.algorithm {
SelfSignedAlgorithm::Ed25519 => {
let raw: [u8; 32] = key_bytes.as_slice().try_into().map_err(|_| {
format!(
"SelfSigned key_id={key_id}: Ed25519 key must be 32 bytes, got {}",
key_bytes.len()
)
})?;
let key = VerifyingKey::from_bytes(&raw).map_err(|e| {
format!("SelfSigned key_id={key_id}: invalid Ed25519 public key: {e}")
})?;
let sig_raw: [u8; 64] =
signature_bytes.as_slice().try_into().map_err(|_| {
format!(
"SelfSigned key_id={key_id}: Ed25519 signature must be 64 bytes, \
got {}",
signature_bytes.len()
)
})?;
let sig = Signature::from_bytes(&sig_raw);
key.verify(&preimage, &sig).map_err(|e| {
format!("SelfSigned key_id={key_id}: Ed25519 verification failed: {e}")
})
}
SelfSignedAlgorithm::EcdsaP256 => {
Err(format!(
"SelfSigned key_id={key_id}: \
UnsupportedAlgorithm: EcdsaP256 SelfSigned verification \
not yet implemented at the verifier layer"
))
}
}
}
}
}
#[must_use]
pub fn ceiling_from_state(state: &VerifiedTrustState) -> ClaimCeiling {
match state {
VerifiedTrustState::FullChainVerified { ceiling, .. } => *ceiling,
VerifiedTrustState::Partial { .. } | VerifiedTrustState::Broken { .. } => {
ClaimCeiling::DevOnly
}
}
}