use crate::error::SessionBundleError;
use crate::types::{ParticipantEntry, SessionTrustBundle};
use aitp_core::{jcs, Aid, Timestamp};
use aitp_crypto::AitpSigningKey;
use aitp_tct::Tct;
use serde::Serialize;
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub const DEFAULT_BUNDLE_VERSION: &str = "aitp/0.1";
pub struct SessionBundleBuilder<'a> {
coordinator_key: &'a AitpSigningKey,
session_id: Option<Uuid>,
participants: Vec<ParticipantEntry>,
issued_at: Option<Timestamp>,
}
impl<'a> SessionBundleBuilder<'a> {
pub fn new(coordinator_key: &'a AitpSigningKey) -> Self {
Self {
coordinator_key,
session_id: None,
participants: Vec::new(),
issued_at: None,
}
}
pub fn session_id(mut self, id: Uuid) -> Self {
self.session_id = Some(id);
self
}
pub fn issued_at(mut self, ts: Timestamp) -> Self {
self.issued_at = Some(ts);
self
}
pub fn participant(mut self, aid: Aid, tct: Tct) -> Self {
self.participants.push(ParticipantEntry { aid, tct });
self
}
pub fn build(self) -> Result<SessionTrustBundle, SessionBundleError> {
if self.participants.is_empty() {
return Err(SessionBundleError::EmptyParticipants);
}
let coordinator = self.coordinator_key.aid().clone();
let session_id = self.session_id.unwrap_or_else(Uuid::new_v4);
let issued_at = self.issued_at.unwrap_or_else(Timestamp::now);
for entry in &self.participants {
if entry.tct.issuer != coordinator {
return Err(SessionBundleError::CoordinatorIssuerMismatch);
}
if entry.tct.audience != entry.aid {
return Err(SessionBundleError::AudienceMismatch);
}
}
let expires_at = self
.participants
.iter()
.map(|p| p.tct.expires_at)
.min_by_key(|t| t.0)
.ok_or(SessionBundleError::EmptyParticipants)?;
let view = BundleSigningView {
version: DEFAULT_BUNDLE_VERSION,
session_id: &session_id,
coordinator: &coordinator,
issued_at: &issued_at,
expires_at: &expires_at,
participants: &self.participants,
};
let canonical = jcs::canonicalize_serializable(&view)
.map_err(|e| SessionBundleError::Canonicalization(e.to_string()))?;
let digest = Sha256::digest(&canonical);
let signature = self.coordinator_key.sign(&digest);
Ok(SessionTrustBundle {
version: DEFAULT_BUNDLE_VERSION.to_string(),
session_id,
coordinator,
issued_at,
expires_at,
participants: self.participants,
signature: signature.into_string(),
})
}
}
#[derive(Serialize)]
pub(crate) struct BundleSigningView<'a> {
pub version: &'a str,
pub session_id: &'a Uuid,
pub coordinator: &'a Aid,
pub issued_at: &'a Timestamp,
pub expires_at: &'a Timestamp,
pub participants: &'a [ParticipantEntry],
}