aitp_session_bundle/
builder.rs1use crate::error::SessionBundleError;
7use crate::types::{ParticipantEntry, SessionTrustBundle};
8use aitp_core::{jcs, Aid, Timestamp};
9use aitp_crypto::AitpSigningKey;
10use aitp_tct::Tct;
11use serde::Serialize;
12use sha2::{Digest, Sha256};
13use uuid::Uuid;
14
15pub const DEFAULT_BUNDLE_VERSION: &str = "aitp/0.1";
17
18pub struct SessionBundleBuilder<'a> {
21 coordinator_key: &'a AitpSigningKey,
22 session_id: Option<Uuid>,
23 participants: Vec<ParticipantEntry>,
24 issued_at: Option<Timestamp>,
25}
26
27impl<'a> SessionBundleBuilder<'a> {
28 pub fn new(coordinator_key: &'a AitpSigningKey) -> Self {
30 Self {
31 coordinator_key,
32 session_id: None,
33 participants: Vec::new(),
34 issued_at: None,
35 }
36 }
37
38 pub fn session_id(mut self, id: Uuid) -> Self {
41 self.session_id = Some(id);
42 self
43 }
44
45 pub fn issued_at(mut self, ts: Timestamp) -> Self {
47 self.issued_at = Some(ts);
48 self
49 }
50
51 pub fn participant(mut self, aid: Aid, tct: Tct) -> Self {
55 self.participants.push(ParticipantEntry { aid, tct });
56 self
57 }
58
59 pub fn build(self) -> Result<SessionTrustBundle, SessionBundleError> {
61 if self.participants.is_empty() {
62 return Err(SessionBundleError::EmptyParticipants);
63 }
64
65 let coordinator = self.coordinator_key.aid().clone();
66 let session_id = self.session_id.unwrap_or_else(Uuid::new_v4);
67 let issued_at = self.issued_at.unwrap_or_else(Timestamp::now);
68
69 for entry in &self.participants {
72 if entry.tct.issuer != coordinator {
73 return Err(SessionBundleError::CoordinatorIssuerMismatch);
74 }
75 if entry.tct.audience != entry.aid {
76 return Err(SessionBundleError::AudienceMismatch);
77 }
78 }
79
80 let expires_at = self
82 .participants
83 .iter()
84 .map(|p| p.tct.expires_at)
85 .min_by_key(|t| t.0)
86 .ok_or(SessionBundleError::EmptyParticipants)?;
87
88 let view = BundleSigningView {
89 version: DEFAULT_BUNDLE_VERSION,
90 session_id: &session_id,
91 coordinator: &coordinator,
92 issued_at: &issued_at,
93 expires_at: &expires_at,
94 participants: &self.participants,
95 };
96 let canonical = jcs::canonicalize_serializable(&view)
97 .map_err(|e| SessionBundleError::Canonicalization(e.to_string()))?;
98 let digest = Sha256::digest(&canonical);
99 let signature = self.coordinator_key.sign(&digest);
100
101 Ok(SessionTrustBundle {
102 version: DEFAULT_BUNDLE_VERSION.to_string(),
103 session_id,
104 coordinator,
105 issued_at,
106 expires_at,
107 participants: self.participants,
108 signature: signature.into_string(),
109 })
110 }
111}
112
113#[derive(Serialize)]
115pub(crate) struct BundleSigningView<'a> {
116 pub version: &'a str,
117 pub session_id: &'a Uuid,
118 pub coordinator: &'a Aid,
119 pub issued_at: &'a Timestamp,
120 pub expires_at: &'a Timestamp,
121 pub participants: &'a [ParticipantEntry],
122}