Skip to main content

pask_wire/
transparent_statement.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Wilder Management Inc. (d/b/a Wilder Robotics) <rob@wilder-robotics.com>
3// See LICENSING.md in the workspace root.
4
5//! Bounded offline recipient coordination. No I/O, live clock or authenticated
6//! provisioning transport. Software-only application policies are not PSER,
7//! hardware appraisal or physical-event truth. Existing #70 helpers are unchanged.
8use 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
22/// Private software-fixture application convention, not a core PSER version.
23pub 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    // Writing into Vec is infallible for the supported CBOR values.
70    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/// Tightenable local ceilings. Aggregate receipt work is at most eight times
100/// the unchanged Phase 2 hard ceiling (256 signature attempts per Receipt).
101#[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/// Absence, malformed enclosing container and malformed encoded Receipt differ.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ReceiptContainerState {
125    NotExamined,
126    Absent,
127    Malformed,
128    Present,
129}
130
131/// Exact signed byte contents plus unauthenticated decoded conveniences.
132#[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/// Inspect the transmitted object before deriving a candidate or doing crypto.
152/// Accepts one tag-18 wrapper or legacy untagged local producer form. Payload
153/// must be attached. Receipts must be byte strings, never repaired legacy arrays.
154#[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            // Only implemented semantics; content-type and key discovery are not understood critical extensions here.
287            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    // Location is not repaired by reading the unprotected equivalent.
306    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/// A single explicit issuer verification key, not a receipt-controlled kid lookup.
338/// Time, permitted algorithm, exact issuer binding and origin are caller inputs.
339#[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/// Expected value is never inferred from a producer file or from the statement.
357#[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/// Exact mapping row, including the TS identity. No normalization or wildcard.
365#[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    /// Local named convention, not a universal SCITT requirement.
377    SharedJsonSiteV1,
378    /// Caller asserts the mapping is independently authenticated.
379    AuthenticatedMappingV1 {
380        rows: &'a [SubjectMapping<'a>],
381        provenance: BindingProvenance<'a>,
382        origin: TrustInputOrigin,
383    },
384}
385/// Named software-only JSON-site policy. It never establishes PSER conformance.
386#[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/// Owned per-Receipt findings, preserving every encoded input and proof outcome.
406/// No borrowed self-reference and no collapsing a bad attachment into absence.
407#[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/// Coordinate exact transmitted bytes. A good Receipt never erases the findings
454/// for others. Only malformed outer structure/policy stops all dependent work.
455/// Passed origin/trust means conditional on explicit caller assertions, not an
456/// authenticated transport observed by this library. LocalSimulation cannot pass
457/// registration or application acceptance. No application policy means unestablished.
458#[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    // The local context convention is canonical JSON with a nonempty site.id.
499    // Comparing canonical output to ORIGINAL bytes rejects duplicate keys and
500    // normalization; it never replaces bytes used for signatures or inclusion.
501    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            // One exact source identity+subject must have one deterministic target.
748            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}