use crate::builder::{peek_tct_claims, DEFAULT_BUNDLE_VERSION};
use crate::error::SessionBundleError;
use crate::types::SessionTrustBundle;
use aitp_core::{Aid, Timestamp};
use aitp_crypto::{AitpVerifyingKey, Signature};
use aitp_manifest::{verify_manifest, Manifest, VerifyManifestContext};
use aitp_tct::{verify_tct, TctError, TctVerifyContext};
use uuid::Uuid;
#[derive(Clone, Copy)]
pub enum CoordinatorKeySource<'a> {
Manifest(&'a Manifest),
UnverifiedAidDangerous,
}
pub struct VerifySessionBundleContext<'a> {
pub verifier_aid: &'a Aid,
pub now: Timestamp,
pub revocation_check: Option<&'a dyn Fn(&Uuid) -> bool>,
pub coordinator_key: CoordinatorKeySource<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BundleOutcome {
Clear {
active_aids: Vec<Aid>,
},
DegradedSubset {
active_aids: Vec<Aid>,
dropped_aids: Vec<Aid>,
},
}
pub fn verify_session_bundle(
bundle: &SessionTrustBundle,
ctx: &VerifySessionBundleContext<'_>,
) -> Result<BundleOutcome, SessionBundleError> {
if bundle.version != DEFAULT_BUNDLE_VERSION {
return Err(SessionBundleError::VersionMismatch);
}
if bundle.expires_at.is_in_the_past(ctx.now) {
return Err(SessionBundleError::Expired);
}
let mut computed_min: Option<Timestamp> = None;
for p in &bundle.participants {
let exp = peek_tct_claims(&p.tct)?.exp;
computed_min = Some(match computed_min {
Some(m) if m.0 <= exp.0 => m,
_ => exp,
});
}
if let Some(computed_min) = computed_min {
if computed_min.0 != bundle.expires_at.0 {
return Err(SessionBundleError::ExpiryWindowInvariant);
}
}
if bundle.participants.is_empty() {
return Err(SessionBundleError::EmptyParticipants);
}
let (coord_key, coordinator_manifest_expires_at) = match ctx.coordinator_key {
CoordinatorKeySource::Manifest(manifest) => {
verify_manifest(manifest, &VerifyManifestContext { now: ctx.now })
.map_err(SessionBundleError::CoordinatorManifest)?;
if manifest.aid != bundle.coordinator {
return Err(SessionBundleError::CoordinatorManifestMismatch);
}
(
AitpVerifyingKey::from_aid(&manifest.aid)?,
Some(manifest.expires_at),
)
}
CoordinatorKeySource::UnverifiedAidDangerous => {
(AitpVerifyingKey::from_aid(&bundle.coordinator)?, None)
}
};
let canonical = crate::builder::bundle_signing_bytes(&crate::builder::BundleSigningBody {
version: &bundle.version,
session_id: &bundle.session_id,
coordinator: &bundle.coordinator,
issued_at: &bundle.issued_at,
expires_at: &bundle.expires_at,
participants: &bundle.participants,
extensions: bundle.extensions.as_ref(),
})?;
let digest = aitp_crypto::Digest32::sha256(&canonical);
let outer_sig =
Signature::parse(&bundle.signature).map_err(|_| SessionBundleError::InvalidSignature)?;
coord_key
.verify_jcs(&digest, &outer_sig)
.map_err(|_| SessionBundleError::InvalidSignature)?;
let mut active = Vec::with_capacity(bundle.participants.len());
let mut dropped = Vec::new();
for entry in &bundle.participants {
let tct_ctx = TctVerifyContext::builder(&entry.aid, &bundle.coordinator, ctx.now)
.accept_unchecked_revocation_dangerous();
let tct_ctx = match coordinator_manifest_expires_at {
Some(expires_at) => tct_ctx.issuer_manifest_expires_at(expires_at),
None => tct_ctx.skip_manifest_expiry_cap_dangerous(),
}
.build()
.expect("both verify decisions are made above");
let verified = match verify_tct(&entry.tct, &tct_ctx) {
Ok(v) => v,
Err(TctError::IssuerMismatch) => {
return Err(SessionBundleError::CoordinatorIssuerMismatch)
}
Err(TctError::AudienceMismatch) => return Err(SessionBundleError::AudienceMismatch),
Err(e) => return Err(SessionBundleError::TctVerification(e)),
};
let is_revoked = ctx
.revocation_check
.map(|check| check(&verified.claims.jti))
.unwrap_or(false);
if is_revoked {
dropped.push(entry.aid.clone());
} else {
active.push(entry.aid.clone());
}
}
let verifier_present = bundle
.participants
.iter()
.any(|p| &p.aid == ctx.verifier_aid);
if !verifier_present {
return Err(SessionBundleError::NotMember);
}
if dropped.is_empty() {
Ok(BundleOutcome::Clear {
active_aids: active,
})
} else {
Ok(BundleOutcome::DegradedSubset {
active_aids: active,
dropped_aids: dropped,
})
}
}