aitp_session_bundle/
verifier.rs1use crate::builder::{peek_tct_claims, 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, TctError, 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 mut computed_min: Option<Timestamp> = None;
86 for p in &bundle.participants {
87 let exp = peek_tct_claims(&p.tct)?.exp;
88 computed_min = Some(match computed_min {
89 Some(m) if m.0 <= exp.0 => m,
90 _ => exp,
91 });
92 }
93 let computed_min = computed_min.ok_or(SessionBundleError::EmptyParticipants)?;
94 if computed_min.0 != bundle.expires_at.0 {
95 return Err(SessionBundleError::ExpiryWindowInvariant);
96 }
97
98 let verifier_present = bundle
100 .participants
101 .iter()
102 .any(|p| &p.aid == ctx.verifier_aid);
103 if !verifier_present {
104 return Err(SessionBundleError::NotMember);
105 }
106
107 let coord_key = AitpVerifyingKey::from_aid(&bundle.coordinator)?;
109 let view = BundleSigningView {
110 session_bundle: crate::builder::BundleSigningBody {
111 version: &bundle.version,
112 session_id: &bundle.session_id,
113 coordinator: &bundle.coordinator,
114 issued_at: &bundle.issued_at,
115 expires_at: &bundle.expires_at,
116 participants: &bundle.participants,
117 },
118 };
119 let canonical = jcs::canonicalize_serializable(&view)
120 .map_err(|e| SessionBundleError::Canonicalization(e.to_string()))?;
121 let digest = Sha256::digest(&canonical);
122 let outer_sig =
123 Signature::parse(&bundle.signature).map_err(|_| SessionBundleError::InvalidSignature)?;
124 coord_key
125 .verify(&digest, &outer_sig)
126 .map_err(|_| SessionBundleError::InvalidSignature)?;
127
128 let mut active = Vec::with_capacity(bundle.participants.len());
131 let mut dropped = Vec::new();
132
133 for entry in &bundle.participants {
134 let tct_ctx = TctVerifyContext {
139 expected_audience: &entry.aid,
140 issuer: &bundle.coordinator,
141 now: ctx.now,
142 issuer_manifest_expires_at: None,
143 revocation_check: None, };
145 let verified = match verify_tct(&entry.tct, &tct_ctx) {
146 Ok(v) => v,
147 Err(TctError::IssuerMismatch) => {
148 return Err(SessionBundleError::CoordinatorIssuerMismatch)
149 }
150 Err(TctError::AudienceMismatch) => return Err(SessionBundleError::AudienceMismatch),
151 Err(e) => return Err(SessionBundleError::TctVerification(e)),
152 };
153
154 let is_revoked = ctx
157 .revocation_check
158 .map(|check| check(&verified.claims.jti))
159 .unwrap_or(false);
160 if is_revoked {
161 dropped.push(entry.aid.clone());
162 } else {
163 active.push(entry.aid.clone());
164 }
165 }
166
167 if dropped.is_empty() {
168 Ok(BundleOutcome::Clear {
169 active_aids: active,
170 })
171 } else {
172 Ok(BundleOutcome::DegradedSubset {
173 active_aids: active,
174 dropped_aids: dropped,
175 })
176 }
177}