1use std::collections::HashSet;
28
29use chrono::{DateTime, Duration, Utc};
30use cortex_core::{effective_ceiling, ClaimCeiling, PolicyDecision, PolicyOutcome};
31use cortex_runtime::RuntimeClaimKind;
32use ed25519_dalek::{Signature, Verifier, VerifyingKey};
33
34use crate::input::{EvidenceInput, EvidenceKind};
35use crate::invariant::{
36 COMPOSITION_CEILING_BELOW_REQUIRED, COMPOSITION_POLICY_FAIL_CLOSED, WITNESS_AUTHORITY_OVERLAP,
37 WITNESS_DISAGREEMENT, WITNESS_MISSING, WITNESS_SIGNATURE_INVALID, WITNESS_STALE,
38 WITNESS_TIER_INSUFFICIENT,
39};
40use crate::state::{BrokenEdge, VerifiedTrustState};
41use crate::witness::{
42 AuthorityDomain, IndependentWitness, SelfSignedAlgorithm, SelfSignedKeyRegistry, WitnessClass,
43 WitnessSignature, WitnessSummary, WitnessTier,
44};
45
46#[must_use]
52pub fn verify(
53 input: &EvidenceInput,
54 witnesses: &[IndependentWitness],
55 now: DateTime<Utc>,
56 max_age: Duration,
57) -> VerifiedTrustState {
58 verify_with_policy(input, witnesses, now, max_age, None)
59}
60
61#[derive(Debug)]
65pub struct VerifyOptions<'a> {
66 pub policy: Option<&'a cortex_core::PolicyDecision>,
70 pub self_signed_keys: Option<&'a SelfSignedKeyRegistry>,
75}
76
77#[must_use]
84pub fn verify_with_options(
85 input: &EvidenceInput,
86 witnesses: &[IndependentWitness],
87 now: DateTime<Utc>,
88 max_age: Duration,
89 opts: VerifyOptions<'_>,
90) -> VerifiedTrustState {
91 let summaries: Vec<WitnessSummary> =
92 witnesses.iter().map(WitnessSummary::from_witness).collect();
93
94 if let Some(decision) = opts.policy {
96 if matches!(
97 decision.final_outcome,
98 cortex_core::PolicyOutcome::Reject | cortex_core::PolicyOutcome::Quarantine
99 ) {
100 return VerifiedTrustState::Broken {
101 edge: BrokenEdge::new(
102 COMPOSITION_POLICY_FAIL_CLOSED,
103 format!(
104 "policy outcome {:?} fails closed for {:?}",
105 decision.final_outcome, input.kind
106 ),
107 ),
108 witnesses: summaries,
109 };
110 }
111 }
112
113 verify_inner(
115 input,
116 witnesses,
117 now,
118 max_age,
119 opts.self_signed_keys,
120 summaries,
121 )
122}
123
124#[must_use]
129pub fn verify_with_policy(
130 input: &EvidenceInput,
131 witnesses: &[IndependentWitness],
132 now: DateTime<Utc>,
133 max_age: Duration,
134 policy: Option<&PolicyDecision>,
135) -> VerifiedTrustState {
136 let summaries: Vec<WitnessSummary> =
137 witnesses.iter().map(WitnessSummary::from_witness).collect();
138
139 if let Some(decision) = policy {
142 if matches!(
143 decision.final_outcome,
144 PolicyOutcome::Reject | PolicyOutcome::Quarantine
145 ) {
146 return VerifiedTrustState::Broken {
147 edge: BrokenEdge::new(
148 COMPOSITION_POLICY_FAIL_CLOSED,
149 format!(
150 "policy outcome {:?} fails closed for {:?}",
151 decision.final_outcome, input.kind
152 ),
153 ),
154 witnesses: summaries,
155 };
156 }
157 }
158
159 verify_inner(input, witnesses, now, max_age, None, summaries)
160}
161
162fn verify_inner(
167 input: &EvidenceInput,
168 witnesses: &[IndependentWitness],
169 now: DateTime<Utc>,
170 max_age: Duration,
171 registry: Option<&SelfSignedKeyRegistry>,
172 summaries: Vec<WitnessSummary>,
173) -> VerifiedTrustState {
174 if input.is_advisory_only() {
178 return VerifiedTrustState::Broken {
179 edge: BrokenEdge::new(
180 WITNESS_TIER_INSUFFICIENT,
181 format!(
182 "evidence input is advisory-only and cannot be promoted by witnesses (runtime_mode={:?}, advisory_only=true)",
183 input.runtime_mode
184 ),
185 ),
186 witnesses: summaries,
187 };
188 }
189
190 for witness in witnesses {
193 if witness.asserted_subject_blake3 != input.evidence_blake3 {
194 return VerifiedTrustState::Broken {
195 edge: BrokenEdge::new(
196 WITNESS_DISAGREEMENT,
197 format!(
198 "witness class={} asserted subject {} but input declares {}",
199 witness.class.wire_str(),
200 witness.asserted_subject_blake3,
201 input.evidence_blake3,
202 ),
203 ),
204 witnesses: summaries,
205 };
206 }
207 }
208
209 for witness in witnesses {
212 let expected = witness.class.required_authority_domain();
213 if witness.authority_domain != expected {
214 return VerifiedTrustState::Broken {
215 edge: BrokenEdge::new(
216 WITNESS_AUTHORITY_OVERLAP,
217 format!(
218 "witness class={} declared authority_domain={} but class requires {}",
219 witness.class.wire_str(),
220 witness.authority_domain.wire_str(),
221 expected.wire_str(),
222 ),
223 ),
224 witnesses: summaries,
225 };
226 }
227 }
228
229 let mut seen_domains: HashSet<AuthorityDomain> = HashSet::new();
231 for witness in witnesses {
232 if !seen_domains.insert(witness.authority_domain) {
233 return VerifiedTrustState::Broken {
234 edge: BrokenEdge::new(
235 WITNESS_AUTHORITY_OVERLAP,
236 format!(
237 "two witnesses share authority_domain={}",
238 witness.authority_domain.wire_str()
239 ),
240 ),
241 witnesses: summaries,
242 };
243 }
244 }
245
246 for witness in witnesses {
248 let age = now - witness.asserted_at;
249 if age > max_age {
250 return VerifiedTrustState::Broken {
251 edge: BrokenEdge::new(
252 WITNESS_STALE,
253 format!(
254 "witness class={} is stale: age {}s exceeds max_age {}s",
255 witness.class.wire_str(),
256 age.num_seconds(),
257 max_age.num_seconds(),
258 ),
259 ),
260 witnesses: summaries,
261 };
262 }
263 }
264
265 for witness in witnesses {
268 if requires_third_party(input.kind, witness.class)
269 && witness.tier != WitnessTier::ThirdParty
270 {
271 return VerifiedTrustState::Broken {
272 edge: BrokenEdge::new(
273 WITNESS_TIER_INSUFFICIENT,
274 format!(
275 "witness class={} requires tier=third_party for {}, got {}",
276 witness.class.wire_str(),
277 input.kind.wire_str(),
278 witness.tier.wire_str(),
279 ),
280 ),
281 witnesses: summaries,
282 };
283 }
284 }
285
286 for witness in witnesses {
288 if let Err(detail) = verify_witness_signature(witness, registry) {
289 return VerifiedTrustState::Broken {
290 edge: BrokenEdge::new(
291 WITNESS_SIGNATURE_INVALID,
292 format!(
293 "witness class={} signature did not verify: {}",
294 witness.class.wire_str(),
295 detail
296 ),
297 ),
298 witnesses: summaries,
299 };
300 }
301 }
302
303 let required_classes = required_classes_for(input.kind);
306 let present_classes: HashSet<WitnessClass> = witnesses.iter().map(|w| w.class).collect();
307 let missing: Vec<WitnessClass> = required_classes
308 .iter()
309 .copied()
310 .filter(|class| !present_classes.contains(class))
311 .collect();
312
313 if !missing.is_empty() {
314 let reasons: Vec<String> = missing
315 .iter()
316 .map(|class| {
317 format!(
318 "{}: required witness class {} is missing",
319 WITNESS_MISSING,
320 class.wire_str()
321 )
322 })
323 .collect();
324 return VerifiedTrustState::Partial {
325 reasons,
326 witnesses: summaries,
327 };
328 }
329
330 let runtime_kind = runtime_claim_kind_for(input.kind);
333 let required_ceiling = runtime_kind.required_ceiling();
334 let effective = effective_ceiling(
335 input.runtime_mode,
336 input.authority_class,
337 input.proof_state,
338 input.requested_ceiling,
339 );
340 if effective < required_ceiling {
341 return VerifiedTrustState::Broken {
342 edge: BrokenEdge::new(
343 COMPOSITION_CEILING_BELOW_REQUIRED,
344 format!(
345 "effective ceiling {effective:?} is below required ceiling {required_ceiling:?} for {runtime_kind:?}"
346 ),
347 ),
348 witnesses: summaries,
349 };
350 }
351
352 VerifiedTrustState::FullChainVerified {
353 ceiling: effective,
354 witnesses: summaries,
355 }
356}
357
358fn required_classes_for(kind: EvidenceKind) -> &'static [WitnessClass] {
362 match kind {
363 EvidenceKind::ReleaseReadiness | EvidenceKind::ComplianceEvidence => &[
364 WitnessClass::SignedLedgerChainHead,
365 WitnessClass::ExternalAnchorCrossing,
366 WitnessClass::RemoteCiConclusion,
367 WitnessClass::ReproducibleBuildProvenance,
368 ],
369 }
370}
371
372const fn runtime_claim_kind_for(kind: EvidenceKind) -> RuntimeClaimKind {
376 match kind {
377 EvidenceKind::ReleaseReadiness => RuntimeClaimKind::ReleaseReadiness,
378 EvidenceKind::ComplianceEvidence => RuntimeClaimKind::ComplianceEvidence,
379 }
380}
381
382fn requires_third_party(kind: EvidenceKind, class: WitnessClass) -> bool {
384 matches!(
385 (kind, class),
386 (
387 EvidenceKind::ReleaseReadiness | EvidenceKind::ComplianceEvidence,
388 WitnessClass::RemoteCiConclusion | WitnessClass::ReproducibleBuildProvenance,
389 )
390 )
391}
392
393fn verify_witness_signature(
404 witness: &IndependentWitness,
405 registry: Option<&SelfSignedKeyRegistry>,
406) -> Result<(), String> {
407 if witness.payload.class() != witness.class {
408 return Err(format!(
409 "payload class {} does not match declared class {}",
410 witness.payload.class().wire_str(),
411 witness.class.wire_str()
412 ));
413 }
414 match &witness.signature {
415 WitnessSignature::Ed25519 {
416 public_key_bytes,
417 signature_bytes,
418 ..
419 } => {
420 let key = VerifyingKey::from_bytes(public_key_bytes)
421 .map_err(|err| format!("public_key_bytes is not a valid Ed25519 point: {err}"))?;
422 let sig = Signature::from_bytes(signature_bytes);
423 let preimage = witness.canonical_preimage();
424 key.verify(&preimage, &sig)
425 .map_err(|err| format!("Ed25519 verification failed: {err}"))?;
426 Ok(())
427 }
428 WitnessSignature::EcdsaP256 { .. } => {
429 Err("UnsupportedAlgorithm: EcdsaP256 verification not yet implemented at the verifier layer".to_string())
434 }
435 WitnessSignature::SelfSigned {
436 key_id,
437 signature_bytes,
438 } => {
439 let reg = registry.ok_or_else(|| {
440 format!(
441 "SelfSigned key_id={key_id}: no SelfSignedKeyRegistry supplied \
442 (pass --witness-key-registry to provide one)"
443 )
444 })?;
445 let entry = reg.get(key_id).ok_or_else(|| {
446 format!(
447 "SelfSigned key_id={key_id} is not in SelfSignedKeyRegistry \
448 (registry contains {} entries)",
449 reg.len()
450 )
451 })?;
452 let key_bytes = entry.key_bytes().map_err(|e| {
453 format!("SelfSigned key_id={key_id}: malformed key_bytes_hex in registry: {e}")
454 })?;
455 let preimage = witness.canonical_preimage();
456 match entry.algorithm {
457 SelfSignedAlgorithm::Ed25519 => {
458 let raw: [u8; 32] = key_bytes.as_slice().try_into().map_err(|_| {
459 format!(
460 "SelfSigned key_id={key_id}: Ed25519 key must be 32 bytes, got {}",
461 key_bytes.len()
462 )
463 })?;
464 let key = VerifyingKey::from_bytes(&raw).map_err(|e| {
465 format!("SelfSigned key_id={key_id}: invalid Ed25519 public key: {e}")
466 })?;
467 let sig_raw: [u8; 64] =
468 signature_bytes.as_slice().try_into().map_err(|_| {
469 format!(
470 "SelfSigned key_id={key_id}: Ed25519 signature must be 64 bytes, \
471 got {}",
472 signature_bytes.len()
473 )
474 })?;
475 let sig = Signature::from_bytes(&sig_raw);
476 key.verify(&preimage, &sig).map_err(|e| {
477 format!("SelfSigned key_id={key_id}: Ed25519 verification failed: {e}")
478 })
479 }
480 SelfSignedAlgorithm::EcdsaP256 => {
481 Err(format!(
484 "SelfSigned key_id={key_id}: \
485 UnsupportedAlgorithm: EcdsaP256 SelfSigned verification \
486 not yet implemented at the verifier layer"
487 ))
488 }
489 }
490 }
491 }
492}
493
494#[must_use]
499pub fn ceiling_from_state(state: &VerifiedTrustState) -> ClaimCeiling {
500 match state {
501 VerifiedTrustState::FullChainVerified { ceiling, .. } => *ceiling,
502 VerifiedTrustState::Partial { .. } | VerifiedTrustState::Broken { .. } => {
503 ClaimCeiling::DevOnly
504 }
505 }
506}