Skip to main content

aitp_session_bundle/
builder.rs

1//! [`SessionTrustBundle`] builder (RFC-AITP-0010 §4).
2//!
3//! Coordinator-side: collect each participant's coordinator-issued TCT
4//! (one per bilateral handshake), assemble the bundle body, JCS-sign.
5
6use 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
15/// `version` constant for v0.1 bundles.
16pub const DEFAULT_BUNDLE_VERSION: &str = "aitp/0.1";
17
18/// Fluent builder for issuing a [`SessionTrustBundle`] as the
19/// coordinator.
20pub 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    /// Begin a new bundle, signed by `coordinator_key`.
29    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    /// Set the session ID (UUIDv4). If unset, a fresh one is generated
39    /// at `build()` time.
40    pub fn session_id(mut self, id: Uuid) -> Self {
41        self.session_id = Some(id);
42        self
43    }
44
45    /// Override `issued_at`. Tests / fixtures only.
46    pub fn issued_at(mut self, ts: Timestamp) -> Self {
47        self.issued_at = Some(ts);
48        self
49    }
50
51    /// Add a participant. The TCT MUST be coordinator-issued
52    /// (`tct.issuer == coordinator_key.aid()`) with `audience == aid`.
53    /// These invariants are checked in `build()`.
54    pub fn participant(mut self, aid: Aid, tct: Tct) -> Self {
55        self.participants.push(ParticipantEntry { aid, tct });
56        self
57    }
58
59    /// Construct, sign, and return the bundle.
60    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        // Validate every participant entry up front so build() returns
70        // a structurally-correct bundle.
71        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        // expires_at = min(participant TCT expiries) per RFC §6.
81        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/// Serialization view of [`SessionTrustBundle`] without `signature`.
114#[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}