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::TctClaims;
11use serde::Serialize;
12use sha2::{Digest, Sha256};
13use uuid::Uuid;
14
15pub const DEFAULT_BUNDLE_VERSION: &str = "aitp/0.2";
17
18pub(crate) fn peek_tct_claims(token: &str) -> Result<TctClaims, SessionBundleError> {
23 let payload = aitp_crypto::jws::decode_payload_unverified(token)
24 .map_err(|e| SessionBundleError::Canonicalization(format!("participant tct: {e}")))?;
25 serde_json::from_slice(&payload)
26 .map_err(|e| SessionBundleError::Canonicalization(format!("participant tct claims: {e}")))
27}
28
29pub struct SessionBundleBuilder<'a> {
32 coordinator_key: &'a AitpSigningKey,
33 session_id: Option<Uuid>,
34 participants: Vec<ParticipantEntry>,
35 issued_at: Option<Timestamp>,
36}
37
38impl<'a> SessionBundleBuilder<'a> {
39 pub fn new(coordinator_key: &'a AitpSigningKey) -> Self {
41 Self {
42 coordinator_key,
43 session_id: None,
44 participants: Vec::new(),
45 issued_at: None,
46 }
47 }
48
49 pub fn session_id(mut self, id: Uuid) -> Self {
52 self.session_id = Some(id);
53 self
54 }
55
56 pub fn issued_at(mut self, ts: Timestamp) -> Self {
58 self.issued_at = Some(ts);
59 self
60 }
61
62 pub fn participant(mut self, aid: Aid, tct: String) -> Self {
66 self.participants.push(ParticipantEntry { aid, tct });
67 self
68 }
69
70 pub fn build(self) -> Result<SessionTrustBundle, SessionBundleError> {
72 if self.participants.is_empty() {
73 return Err(SessionBundleError::EmptyParticipants);
74 }
75
76 let coordinator = self.coordinator_key.aid().clone();
77 let session_id = self.session_id.unwrap_or_else(Uuid::new_v4);
78 let issued_at = self.issued_at.unwrap_or_else(Timestamp::now);
79
80 let mut min_exp: Option<Timestamp> = None;
85 for entry in &self.participants {
86 let claims = peek_tct_claims(&entry.tct)?;
87 if claims.iss != coordinator {
88 return Err(SessionBundleError::CoordinatorIssuerMismatch);
89 }
90 if claims.aud != entry.aid {
91 return Err(SessionBundleError::AudienceMismatch);
92 }
93 min_exp = Some(match min_exp {
94 Some(m) if m.0 <= claims.exp.0 => m,
95 _ => claims.exp,
96 });
97 }
98
99 let expires_at = min_exp.ok_or(SessionBundleError::EmptyParticipants)?;
101
102 let view = BundleSigningView {
103 session_bundle: BundleSigningBody {
104 version: DEFAULT_BUNDLE_VERSION,
105 session_id: &session_id,
106 coordinator: &coordinator,
107 issued_at: &issued_at,
108 expires_at: &expires_at,
109 participants: &self.participants,
110 },
111 };
112 let canonical = jcs::canonicalize_serializable(&view)
113 .map_err(|e| SessionBundleError::Canonicalization(e.to_string()))?;
114 let digest = Sha256::digest(&canonical);
115 let signature = self.coordinator_key.sign(&digest);
116
117 Ok(SessionTrustBundle {
118 version: DEFAULT_BUNDLE_VERSION.to_string(),
119 session_id,
120 coordinator,
121 issued_at,
122 expires_at,
123 participants: self.participants,
124 signature: signature.into_string(),
125 })
126 }
127}
128
129#[derive(Serialize)]
133pub(crate) struct BundleSigningView<'a> {
134 pub session_bundle: BundleSigningBody<'a>,
135}
136
137#[derive(Serialize)]
140pub(crate) struct BundleSigningBody<'a> {
141 pub version: &'a str,
142 pub session_id: &'a Uuid,
143 pub coordinator: &'a Aid,
144 pub issued_at: &'a Timestamp,
145 pub expires_at: &'a Timestamp,
146 pub participants: &'a [ParticipantEntry],
147}