aitp_session_bundle/
verifier.rs1use crate::builder::{BundleSigningView, DEFAULT_BUNDLE_VERSION};
4use crate::error::SessionBundleError;
5use crate::types::SessionTrustBundle;
6use aitp_core::{jcs, Aid, Timestamp};
7use aitp_crypto::{AitpVerifyingKey, Signature};
8use aitp_tct::{verify_tct, TctVerifyContext};
9use sha2::{Digest, Sha256};
10use uuid::Uuid;
11
12pub struct VerifySessionBundleContext<'a> {
14 pub verifier_aid: &'a Aid,
18 pub now: Timestamp,
20 pub revocation_check: Option<&'a dyn Fn(&Uuid) -> bool>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum BundleOutcome {
31 Clear {
34 active_aids: Vec<Aid>,
36 },
37 DegradedSubset {
44 active_aids: Vec<Aid>,
46 dropped_aids: Vec<Aid>,
48 },
49}
50
51pub fn verify_session_bundle(
64 bundle: &SessionTrustBundle,
65 ctx: &VerifySessionBundleContext<'_>,
66) -> Result<BundleOutcome, SessionBundleError> {
67 if bundle.version != DEFAULT_BUNDLE_VERSION {
69 return Err(SessionBundleError::VersionMismatch);
70 }
71
72 if bundle.expires_at.is_in_the_past(ctx.now) {
74 return Err(SessionBundleError::Expired);
75 }
76
77 if bundle.participants.is_empty() {
78 return Err(SessionBundleError::EmptyParticipants);
79 }
80
81 let computed_min = bundle
83 .participants
84 .iter()
85 .map(|p| p.tct.expires_at)
86 .min_by_key(|t| t.0)
87 .ok_or(SessionBundleError::EmptyParticipants)?;
88 if computed_min.0 != bundle.expires_at.0 {
89 return Err(SessionBundleError::ExpiryWindowInvariant);
90 }
91
92 let verifier_present = bundle
94 .participants
95 .iter()
96 .any(|p| &p.aid == ctx.verifier_aid);
97 if !verifier_present {
98 return Err(SessionBundleError::NotMember);
99 }
100
101 let coord_key = AitpVerifyingKey::from_aid(&bundle.coordinator)?;
103 let view = BundleSigningView {
104 version: &bundle.version,
105 session_id: &bundle.session_id,
106 coordinator: &bundle.coordinator,
107 issued_at: &bundle.issued_at,
108 expires_at: &bundle.expires_at,
109 participants: &bundle.participants,
110 };
111 let canonical = jcs::canonicalize_serializable(&view)
112 .map_err(|e| SessionBundleError::Canonicalization(e.to_string()))?;
113 let digest = Sha256::digest(&canonical);
114 let outer_sig =
115 Signature::parse(&bundle.signature).map_err(|_| SessionBundleError::InvalidSignature)?;
116 coord_key
117 .verify(&digest, &outer_sig)
118 .map_err(|_| SessionBundleError::InvalidSignature)?;
119
120 let mut active = Vec::with_capacity(bundle.participants.len());
123 let mut dropped = Vec::new();
124
125 for entry in &bundle.participants {
126 if entry.tct.issuer != bundle.coordinator {
128 return Err(SessionBundleError::CoordinatorIssuerMismatch);
129 }
130 if entry.tct.audience != entry.aid {
133 return Err(SessionBundleError::AudienceMismatch);
134 }
135 let tct_ctx = TctVerifyContext {
137 expected_audience: &entry.aid,
138 issuer_pubkey: &coord_key,
139 now: ctx.now,
140 issuer_manifest_expires_at: None,
141 revocation_check: None, };
143 verify_tct(&entry.tct, &tct_ctx).map_err(SessionBundleError::TctVerification)?;
144
145 let is_revoked = ctx
148 .revocation_check
149 .map(|check| check(&entry.tct.jti))
150 .unwrap_or(false);
151 if is_revoked {
152 dropped.push(entry.aid.clone());
153 } else {
154 active.push(entry.aid.clone());
155 }
156 }
157
158 if dropped.is_empty() {
159 Ok(BundleOutcome::Clear {
160 active_aids: active,
161 })
162 } else {
163 Ok(BundleOutcome::DegradedSubset {
164 active_aids: active,
165 dropped_aids: dropped,
166 })
167 }
168}