1use alloc::{string::String, vec, vec::Vec};
9use coset::cbor::Value;
10use ed25519_dalek::{Signature, VerifyingKey};
11use sha2::{Digest, Sha256};
12
13use crate::{
14 BindingProvenance, InspectionFinding as Finding, InspectionLimits, InspectionStatus as Status,
15 ProofVerification, ReceiptVerificationPolicy, TrustInputOrigin, TsPublicKey, TsTrustContext,
16 UnauthenticatedReceiptClaims, VerifyingKeyEvidence, canonicalize_json, derive_candidate_entry,
17 receipt_cbor::{Budget, Role},
18 receipt_inspection::check_unique_maps,
19 verify_scitt_receipt,
20};
21
22pub const SOFTWARE_SITE_CONTENT_TYPE: &str = "application/json; profile=pask71-software-site/1";
24
25fn f(status: Status, code: &'static str) -> Finding {
26 Finding {
27 status,
28 code,
29 detail: "Only the named dimension under the recorded local policy; not a general acceptance claim.",
30 evidence_refs: vec!["exact_statement_and_explicit_caller_inputs"],
31 }
32}
33fn pass(code: &'static str) -> Finding {
34 f(Status::Passed, code)
35}
36fn fail(code: &'static str) -> Finding {
37 f(Status::Failed, code)
38}
39fn unset(code: &'static str) -> Finding {
40 f(Status::Unestablished, code)
41}
42fn skip() -> Finding {
43 f(Status::NotEvaluated, "prerequisite_not_established")
44}
45fn ok(x: &Finding) -> bool {
46 x.status == Status::Passed
47}
48fn integer(v: &Value) -> Option<i128> {
49 if let Value::Integer(i) = v {
50 Some((*i).into())
51 } else {
52 None
53 }
54}
55fn get(m: &[(Value, Value)], key: i128) -> Option<&Value> {
56 m.iter()
57 .find(|(k, _)| integer(k) == Some(key))
58 .map(|(_, v)| v)
59}
60fn text(v: Option<&Value>) -> Option<&str> {
61 if let Some(Value::Text(s)) = v {
62 Some(s)
63 } else {
64 None
65 }
66}
67fn encode(v: &Value) -> Vec<u8> {
68 let mut out = Vec::new();
69 coset::cbor::ser::into_writer(v, &mut out).expect("CBOR Vec writer");
71 out
72}
73fn decode(bytes: &[u8]) -> Option<Value> {
74 let mut b = bytes;
75 let v = coset::cbor::de::from_reader(&mut b).ok()?;
76 b.is_empty().then_some(v)
77}
78fn bounded_text(s: &str) -> bool {
79 !s.is_empty() && s.len() <= 8192
80}
81fn string_or_uri(s: &str) -> bool {
82 bounded_text(s) && (!s.contains(':') || fluent_uri::Uri::parse(s).is_ok())
83}
84fn authenticated(p: BindingProvenance<'_>) -> bool {
85 matches!(p, BindingProvenance::CallerAuthenticated { authority, evidence_ref }
86 if bounded_text(authority) && bounded_text(evidence_ref))
87}
88fn bounded_provenance(p: BindingProvenance<'_>) -> bool {
89 match p {
90 BindingProvenance::Missing => true,
91 BindingProvenance::Unauthenticated { origin } => origin.len() <= 8192,
92 BindingProvenance::CallerAuthenticated {
93 authority,
94 evidence_ref,
95 } => authority.len() <= 8192 && evidence_ref.len() <= 8192,
96 }
97}
98
99#[derive(Debug, Clone)]
102pub struct TransparentStatementPolicy {
103 pub strict_cross_map: bool,
104 pub max_statement_bytes: usize,
105 pub max_receipts: usize,
106 pub receipt: ReceiptVerificationPolicy,
107}
108impl Default for TransparentStatementPolicy {
109 fn default() -> Self {
110 Self {
111 strict_cross_map: false,
112 max_statement_bytes: 1_048_576,
113 max_receipts: 8,
114 receipt: ReceiptVerificationPolicy::default(),
115 }
116 }
117}
118impl TransparentStatementPolicy {
119 pub const ID: &'static str = "pask71-recipient-offline/1";
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ReceiptContainerState {
125 NotExamined,
126 Absent,
127 Malformed,
128 Present,
129}
130
131#[derive(Debug, Clone)]
133pub struct OuterStatementReport<'a> {
134 pub encoded_statement: &'a [u8],
135 pub policy: TransparentStatementPolicy,
136 pub protected_bytes: Option<Vec<u8>>,
137 pub payload_bytes: Option<Vec<u8>>,
138 pub signature_bytes: Option<Vec<u8>>,
139 pub effective_headers: Vec<(Value, Value)>,
140 pub claims: Option<UnauthenticatedReceiptClaims>,
141 pub algorithm: Option<i128>,
142 pub protected_content_type: Option<String>,
143 pub structure: Finding,
144 pub required_claims: Finding,
145 pub support: Finding,
146 pub selected_policy: Finding,
147 pub container: ReceiptContainerState,
148 pub receipts: Vec<Vec<u8>>,
149}
150
151#[must_use]
155pub fn inspect_transparent_statement<'a>(
156 bytes: &'a [u8],
157 policy: &TransparentStatementPolicy,
158) -> OuterStatementReport<'a> {
159 let mut r = OuterStatementReport {
160 encoded_statement: bytes,
161 policy: policy.clone(),
162 protected_bytes: None,
163 payload_bytes: None,
164 signature_bytes: None,
165 effective_headers: Vec::new(),
166 claims: None,
167 algorithm: None,
168 protected_content_type: None,
169 structure: skip(),
170 required_claims: skip(),
171 support: skip(),
172 selected_policy: pass("bounded_outer_policy"),
173 container: ReceiptContainerState::NotExamined,
174 receipts: Vec::new(),
175 };
176 if let Err(code) = inspect_outer(&mut r) {
177 r.structure = fail(code);
178 }
179 r
180}
181fn inspect_outer(r: &mut OuterStatementReport<'_>) -> Result<(), &'static str> {
182 let p = &r.policy;
183 if p.max_statement_bytes == 0
184 || p.max_statement_bytes > 1_048_576
185 || p.max_receipts == 0
186 || p.max_receipts > 8
187 {
188 r.selected_policy = fail("invalid_outer_limits");
189 return Err("outer_policy_limit");
190 }
191 if r.encoded_statement.len() > p.max_statement_bytes {
192 return Err("outer_byte_limit");
193 }
194 let limits = InspectionLimits {
195 max_receipt_bytes: p.max_statement_bytes,
196 ..InspectionLimits::default()
197 };
198 let mut budget = Budget {
199 limits: &limits,
200 items: 0,
201 unprotected_x5t_invalid: false,
202 };
203 let role = if r.encoded_statement.first().is_some_and(|b| b >> 5 == 6) {
204 Role::Envelope
205 } else {
206 Role::Sign1
207 };
208 budget
209 .scan(r.encoded_statement, 0, role, "outer_trailing")
210 .map_err(|_| "outer_cbor_preflight")?;
211 let value = decode(r.encoded_statement).ok_or("outer_cbor")?;
212 check_unique_maps(&value)?;
213 let value = match value {
214 Value::Tag(18, inner) => *inner,
215 v => v,
216 };
217 let Value::Array(items) = value else {
218 return Err("outer_shape");
219 };
220 let [
221 Value::Bytes(protected),
222 Value::Map(u),
223 Value::Bytes(payload),
224 Value::Bytes(signature),
225 ] = items.as_slice()
226 else {
227 return Err("outer_shape");
228 };
229 budget
230 .scan(protected, 2, Role::ProtectedMap, "protected_trailing")
231 .map_err(|_| "protected_cbor_preflight")?;
232 let protected_map = decode(protected).ok_or("protected_cbor")?;
233 check_unique_maps(&protected_map)?;
234 let Value::Map(p) = &protected_map else {
235 return Err("protected_not_map");
236 };
237 for m in [p, u] {
238 if m.iter()
239 .any(|(k, _)| !matches!(k, Value::Integer(_) | Value::Text(_)))
240 {
241 return Err("header_label_type");
242 }
243 }
244 if get(p, 15).is_some() && get(u, 15).is_some() {
245 return Err("label15_single_occurrence");
246 }
247 if get(u, 2).is_some() {
248 return Err("crit_location");
249 }
250 if r.policy.strict_cross_map && p.iter().any(|(k, _)| u.iter().any(|(q, _)| q == k)) {
251 r.selected_policy = fail("cross_map_overlap");
252 }
253 r.protected_bytes = Some(protected.clone());
254 r.payload_bytes = Some(payload.clone());
255 r.signature_bytes = Some(signature.clone());
256 r.effective_headers = p.clone();
257 r.effective_headers.extend(
258 u.iter()
259 .filter(|(k, _)| !p.iter().any(|(q, _)| q == k))
260 .cloned(),
261 );
262 r.algorithm = get(p, 1).and_then(integer);
263 r.protected_content_type = text(get(p, 3)).map(String::from);
264 r.support = match r.algorithm {
265 Some(-8) => pass("ed25519_issuer_supported"),
266 Some(_) => f(Status::Unsupported, "issuer_algorithm"),
267 None => fail("protected_algorithm_required"),
268 };
269 if let Some(crit) = get(p, 2) {
270 let Value::Array(labels) = crit else {
271 return Err("crit_type");
272 };
273 if labels.is_empty() {
274 return Err("crit_empty");
275 }
276 for (i, label) in labels.iter().enumerate() {
277 if !matches!(label, Value::Integer(_) | Value::Text(_))
278 || labels[..i].contains(label)
279 || integer(label) == Some(2)
280 {
281 return Err("crit_label");
282 }
283 if !p.iter().any(|(k, _)| k == label) {
284 return Err("crit_reference_absent");
285 }
286 if !matches!(integer(label), Some(1 | 15 | 394)) {
288 r.support = f(Status::Unsupported, "outer_critical_semantics");
289 }
290 }
291 }
292 r.required_claims = fail("protected_text_claims_required");
293 if let Some(Value::Map(c)) = get(p, 15)
294 && let (Some(iss), Some(sub)) = (text(get(c, 1)), text(get(c, 2)))
295 && string_or_uri(iss)
296 && string_or_uri(sub)
297 {
298 r.claims = Some(UnauthenticatedReceiptClaims {
299 issuer: iss.into(),
300 subject: sub.into(),
301 authenticated: false,
302 });
303 r.required_claims = pass("text_claim_types_not_semantic_binding");
304 }
305 if r.algorithm.is_none() {
307 r.required_claims = fail("protected_algorithm_required");
308 }
309 match get(&r.effective_headers, 394) {
310 None => r.container = ReceiptContainerState::Absent,
311 Some(Value::Array(a))
312 if !a.is_empty()
313 && a.len() <= r.policy.max_receipts
314 && a.iter().all(|x| matches!(x, Value::Bytes(_))) =>
315 {
316 r.container = ReceiptContainerState::Present;
317 r.receipts = a
318 .iter()
319 .map(|x| {
320 if let Value::Bytes(b) = x {
321 b.clone()
322 } else {
323 unreachable!()
324 }
325 })
326 .collect();
327 }
328 Some(_) => {
329 r.container = ReceiptContainerState::Malformed;
330 return Err("receipt_container_malformed_or_limit");
331 }
332 }
333 r.structure = pass("outer_structure_inspected");
334 Ok(())
335}
336
337#[derive(Debug, Clone)]
340pub struct IssuerKeyInput<'a> {
341 pub public_key: TsPublicKey<'a>,
342 pub algorithm: i64,
343 pub issuer: &'a str,
344 pub provenance: BindingProvenance<'a>,
345 pub origin: TrustInputOrigin,
346 pub evaluation_time: Option<i64>,
347 pub valid_from: Option<i64>,
348 pub valid_until: Option<i64>,
349 pub explicitly_distrusted: bool,
350}
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum DigestTarget {
353 CandidateEntrySha256,
354 PayloadSha256,
355}
356#[derive(Debug, Clone)]
358pub struct ExpectedDigest<'a> {
359 pub target: DigestTarget,
360 pub sha256: [u8; 32],
361 pub provenance: BindingProvenance<'a>,
362 pub origin: TrustInputOrigin,
363}
364#[derive(Debug, Clone)]
366pub struct SubjectMapping<'a> {
367 pub service_identity: &'a str,
368 pub receipt_subject: &'a str,
369 pub statement_subject: &'a str,
370 pub site_id: &'a str,
371}
372#[derive(Debug, Clone, Default)]
373pub enum SubjectPolicy<'a> {
374 #[default]
375 None,
376 SharedJsonSiteV1,
378 AuthenticatedMappingV1 {
380 rows: &'a [SubjectMapping<'a>],
381 provenance: BindingProvenance<'a>,
382 origin: TrustInputOrigin,
383 },
384}
385#[derive(Debug, Clone, Default)]
387pub enum StatementApplicationPolicy {
388 #[default]
389 None,
390 SignedJsonSiteV1 {
391 require_subject: bool,
392 require_all_receipts: bool,
393 require_authenticated_digest: bool,
394 },
395}
396#[derive(Debug)]
397pub struct StatementVerificationInputs<'a> {
398 pub issuer: Option<&'a IssuerKeyInput<'a>>,
399 pub services: &'a TsTrustContext<'a>,
400 pub expected_digest: Option<&'a ExpectedDigest<'a>>,
401 pub subject: SubjectPolicy<'a>,
402 pub application: StatementApplicationPolicy,
403}
404
405#[derive(Debug, Clone)]
408pub struct StatementReceiptOutcome {
409 pub index: usize,
410 pub encoded_receipt: Vec<u8>,
411 pub structure: Finding,
412 pub required_claims: Finding,
413 pub support: Finding,
414 pub selected_policy: Finding,
415 pub claims: Option<UnauthenticatedReceiptClaims>,
416 pub candidate_derivation: Finding,
417 pub key_configuration: Vec<Finding>,
418 pub candidate_keys: Vec<VerifyingKeyEvidence>,
419 pub proofs: Vec<ProofVerification>,
420 pub signature_attempts: usize,
421 pub ts_signature: Finding,
422 pub inclusion: Finding,
423 pub ts_key_association: Finding,
424 pub ts_identity_trust: Finding,
425 pub acceptable_for_registration: Finding,
426 pub subject_policy: Finding,
427}
428#[derive(Debug)]
429pub struct TransparentStatementReport<'a> {
430 pub outer: OuterStatementReport<'a>,
431 pub inputs: &'a StatementVerificationInputs<'a>,
432 pub policy_id: &'static str,
433 pub candidate_entry: Option<Vec<u8>>,
434 pub candidate_derivation: Finding,
435 pub digest_equality: Finding,
436 pub digest_origin: Finding,
437 pub issuer_signature: Finding,
438 pub actual_issuer_key: Option<[u8; 32]>,
439 pub issuer_key_association: Finding,
440 pub issuer_identity_trust: Finding,
441 pub payload_context: Finding,
442 pub application_profile: Finding,
443 pub included_site_id: Option<String>,
444 pub receipts: Vec<StatementReceiptOutcome>,
445 pub acceptable_receipt_indices: Vec<usize>,
446 pub registration_evidence: Finding,
447 pub subject_policy: Finding,
448 pub application_policy: Finding,
449 pub hardware_appraisal: Finding,
450 pub overall_profile: Finding,
451}
452
453#[must_use]
459pub fn verify_transparent_statement<'a>(
460 bytes: &'a [u8],
461 inputs: &'a StatementVerificationInputs<'a>,
462 policy: &TransparentStatementPolicy,
463) -> TransparentStatementReport<'a> {
464 let mut r = TransparentStatementReport {
465 outer: inspect_transparent_statement(bytes, policy),
466 inputs,
467 policy_id: TransparentStatementPolicy::ID,
468 candidate_entry: None,
469 candidate_derivation: skip(),
470 digest_equality: unset("expected_digest_absent"),
471 digest_origin: unset("expected_digest_origin_absent"),
472 issuer_signature: skip(),
473 actual_issuer_key: None,
474 issuer_key_association: unset("issuer_key_absent"),
475 issuer_identity_trust: unset("issuer_trust_absent"),
476 payload_context: skip(),
477 application_profile: unset("application_policy_absent"),
478 included_site_id: None,
479 receipts: Vec::new(),
480 acceptable_receipt_indices: Vec::new(),
481 registration_evidence: skip(),
482 subject_policy: unset("subject_convention_absent"),
483 application_policy: unset("application_policy_absent"),
484 hardware_appraisal: f(Status::NotEvaluated, "hardware_not_implemented"),
485 overall_profile: unset("full_pser_conformance_not_established"),
486 };
487 if !ok(&r.outer.structure) || !ok(&r.outer.selected_policy) {
488 return r;
489 }
490 let Ok(candidate) = derive_candidate_entry(bytes) else {
491 r.candidate_derivation = fail("candidate_derivation");
492 return r;
493 };
494 r.candidate_entry = Some(candidate);
495 r.candidate_derivation = pass("exact_candidate_derived");
496 check_digest(&mut r);
497 check_issuer(&mut r);
498 let payload = r.outer.payload_bytes.as_ref().expect("inspected payload");
502 r.payload_context = fail("canonical_json_site_context_required");
503 if canonicalize_json(payload).is_ok_and(|b| b == *payload)
504 && let Ok(v) = serde_json::from_slice::<serde_json::Value>(payload)
505 && let Some(site) = v
506 .get("site")
507 .and_then(|v| v.get("id"))
508 .and_then(|v| v.as_str())
509 .filter(|s| bounded_text(s))
510 {
511 r.included_site_id = Some(site.into());
512 r.payload_context = pass("included_json_site_context_not_pser_validation");
513 }
514 for (index, encoded) in r.outer.receipts.iter().enumerate() {
515 let result = verify_scitt_receipt(encoded, bytes, inputs.services, &policy.receipt);
516 let subject = subject_finding(&r, result.envelope.unauthenticated_claims.as_ref());
517 let selected_policy = if !ok(&result.envelope.selected_policy) {
518 result.envelope.selected_policy.clone()
519 } else {
520 result.selected_policy.clone()
521 };
522 r.receipts.push(StatementReceiptOutcome {
523 index,
524 encoded_receipt: encoded.clone(),
525 structure: result.envelope.structure,
526 required_claims: result.envelope.required_claims,
527 support: result.envelope.support,
528 selected_policy,
529 claims: result.envelope.unauthenticated_claims,
530 candidate_derivation: result.candidate_derivation,
531 key_configuration: result.key_configuration,
532 candidate_keys: result.candidate_keys,
533 proofs: result.proofs,
534 signature_attempts: result.signature_attempts,
535 ts_signature: result.ts_signature,
536 inclusion: result.inclusion,
537 ts_key_association: result.ts_key_association,
538 ts_identity_trust: result.ts_identity_trust,
539 acceptable_for_registration: result.acceptable_for_registration,
540 subject_policy: subject,
541 });
542 }
543 r.acceptable_receipt_indices = r
544 .receipts
545 .iter()
546 .filter(|x| ok(&x.acceptable_for_registration))
547 .map(|x| x.index)
548 .collect();
549 r.registration_evidence = if r.acceptable_receipt_indices.is_empty() {
550 unset("no_acceptable_receipt")
551 } else {
552 pass("at_least_one_acceptable_receipt_caller_trust")
553 };
554 if !matches!(inputs.subject, SubjectPolicy::None) {
555 r.subject_policy = if r
556 .receipts
557 .iter()
558 .any(|x| ok(&x.acceptable_for_registration) && ok(&x.subject_policy))
559 {
560 pass("acceptable_receipt_has_subject_binding")
561 } else if r.receipts.iter().any(|x| {
562 ok(&x.acceptable_for_registration) && x.subject_policy.status == Status::Failed
563 }) {
564 fail("acceptable_receipt_subject_contradiction")
565 } else {
566 unset("subject_binding_not_established_for_acceptable_receipt")
567 };
568 }
569 if let StatementApplicationPolicy::SignedJsonSiteV1 {
570 require_subject,
571 require_all_receipts,
572 require_authenticated_digest,
573 } = inputs.application
574 {
575 r.application_profile = match r.outer.protected_content_type.as_deref() {
576 Some(SOFTWARE_SITE_CONTENT_TYPE) => pass("declared_software_site_v1"),
577 Some(_) => f(Status::Unsupported, "application_declared_profile"),
578 None => fail("application_protected_content_type_required"),
579 };
580 let required = [
581 &r.outer.required_claims,
582 &r.outer.support,
583 &r.payload_context,
584 &r.issuer_signature,
585 &r.issuer_key_association,
586 &r.issuer_identity_trust,
587 &r.registration_evidence,
588 &r.application_profile,
589 ];
590 r.application_policy = if required.iter().any(|x| x.status == Status::Failed) {
591 fail("software_application_prerequisite_failed")
592 } else if required.iter().any(|x| !ok(x)) {
593 unset("software_application_prerequisite_unestablished")
594 } else if require_all_receipts
595 && r.receipts.iter().any(|x| {
596 !ok(&x.acceptable_for_registration) || (require_subject && !ok(&x.subject_policy))
597 })
598 {
599 fail("explicit_all_receipts_application_policy")
600 } else if require_subject && !ok(&r.subject_policy) {
601 r.subject_policy.clone()
602 } else if require_authenticated_digest && (!ok(&r.digest_equality) || !ok(&r.digest_origin))
603 {
604 if r.digest_equality.status == Status::Failed {
605 fail("expected_digest_mismatch")
606 } else {
607 unset("authenticated_digest_not_established")
608 }
609 } else {
610 pass("signed_json_site_v1_only_not_pser_or_hardware")
611 };
612 }
613 r
614}
615fn check_digest(r: &mut TransparentStatementReport<'_>) {
616 let Some(d) = r.inputs.expected_digest else {
617 return;
618 };
619 let bytes = match d.target {
620 DigestTarget::CandidateEntrySha256 => r.candidate_entry.as_ref().unwrap(),
621 DigestTarget::PayloadSha256 => r.outer.payload_bytes.as_ref().unwrap(),
622 };
623 let actual: [u8; 32] = Sha256::digest(bytes).into();
624 r.digest_equality = if actual == d.sha256 {
625 pass("explicit_expected_digest_equal")
626 } else {
627 fail("expected_digest_mismatch")
628 };
629 r.digest_origin = if authenticated(d.provenance)
630 && d.origin == TrustInputOrigin::CallerAuthenticatedExternal
631 {
632 pass("digest_origin_caller_assertion")
633 } else {
634 unset("digest_equality_not_authenticated_origin")
635 };
636}
637fn check_issuer(r: &mut TransparentStatementReport<'_>) {
638 let Some(k) = r.inputs.issuer else {
639 r.issuer_signature = unset("issuer_key_absent");
640 return;
641 };
642 if !bounded_text(k.issuer) || !bounded_provenance(k.provenance) {
643 r.issuer_key_association = fail("issuer_input_limit");
644 return;
645 }
646 if !ok(&r.outer.support) {
647 r.issuer_signature = r.outer.support.clone();
648 return;
649 }
650 if r.outer.algorithm != Some(i128::from(k.algorithm)) {
651 r.issuer_signature = fail("issuer_algorithm_key_mismatch");
652 return;
653 }
654 let TsPublicKey::Ed25519(bytes) = k.public_key else {
655 r.issuer_signature = f(Status::Unsupported, "issuer_key_type");
656 return;
657 };
658 let signed = encode(&Value::Array(vec![
659 Value::Text("Signature1".into()),
660 Value::Bytes(r.outer.protected_bytes.clone().unwrap()),
661 Value::Bytes(vec![]),
662 Value::Bytes(r.outer.payload_bytes.clone().unwrap()),
663 ]));
664 let valid = VerifyingKey::from_bytes(&bytes)
665 .ok()
666 .zip(Signature::from_slice(r.outer.signature_bytes.as_ref().unwrap()).ok())
667 .is_some_and(|(key, sig)| key.verify_strict(&signed, &sig).is_ok());
668 r.issuer_signature = if valid {
669 pass("issuer_signature_exact_bytes")
670 } else {
671 fail("issuer_signature_invalid")
672 };
673 if !valid {
674 return;
675 }
676 r.actual_issuer_key = Some(bytes);
677 r.issuer_key_association = if r
678 .outer
679 .claims
680 .as_ref()
681 .is_some_and(|c| c.issuer == k.issuer)
682 && authenticated(k.provenance)
683 {
684 pass("actual_issuer_key_caller_authenticated_binding")
685 } else {
686 unset("issuer_binding_provenance_missing_or_mismatched")
687 };
688 r.issuer_identity_trust = if k.explicitly_distrusted {
689 fail("issuer_key_explicitly_distrusted")
690 } else if !ok(&r.issuer_key_association) {
691 unset("issuer_binding_unestablished")
692 } else if !matches!((k.evaluation_time, k.valid_from, k.valid_until), (Some(now), Some(from), Some(until)) if from < until && now >= from && now < until)
693 {
694 unset("issuer_validity_not_established")
695 } else if k.origin == TrustInputOrigin::LocalSimulation {
696 unset("issuer_local_simulation_only")
697 } else {
698 pass("issuer_trust_conditional_on_caller_origin")
699 };
700}
701fn subject_finding(
702 r: &TransparentStatementReport<'_>,
703 receipt: Option<&UnauthenticatedReceiptClaims>,
704) -> Finding {
705 if matches!(r.inputs.subject, SubjectPolicy::None) {
706 return unset("subject_convention_absent");
707 }
708 let (Some(statement), Some(receipt), Some(site)) =
709 (&r.outer.claims, receipt, &r.included_site_id)
710 else {
711 return unset("typed_subject_context_missing");
712 };
713 match &r.inputs.subject {
714 SubjectPolicy::None => unreachable!(),
715 SubjectPolicy::SharedJsonSiteV1 => {
716 if receipt.subject == statement.subject && statement.subject == *site {
717 pass("shared_json_site_v1_exact_correspondence")
718 } else {
719 fail("shared_json_site_v1_contradiction")
720 }
721 }
722 SubjectPolicy::AuthenticatedMappingV1 {
723 rows,
724 provenance,
725 origin,
726 } => {
727 if rows.len() > 64
728 || rows.iter().any(|x| {
729 [
730 x.service_identity,
731 x.receipt_subject,
732 x.statement_subject,
733 x.site_id,
734 ]
735 .iter()
736 .any(|s| !bounded_text(s))
737 })
738 {
739 return fail("subject_mapping_limit");
740 }
741 if rows.is_empty()
742 || !authenticated(*provenance)
743 || *origin != TrustInputOrigin::CallerAuthenticatedExternal
744 {
745 return unset("authenticated_mapping_missing");
746 }
747 let mut matching = rows.iter().filter(|x| {
749 x.service_identity == receipt.issuer && x.receipt_subject == receipt.subject
750 });
751 let Some(first) = matching.next() else {
752 return unset("authenticated_mapping_missing");
753 };
754 if matching.any(|x| {
755 x.statement_subject != first.statement_subject || x.site_id != first.site_id
756 }) {
757 return fail("ambiguous_subject_mapping");
758 }
759 if first.statement_subject == statement.subject && first.site_id == site {
760 pass("authenticated_mapping_v1_exact_correspondence")
761 } else {
762 fail("mapping_contradiction")
763 }
764 }
765 }
766}