aitp_session_bundle/types.rs
1//! Wire types for Session Trust Bundle (RFC-AITP-0010 §3).
2
3use aitp_core::{Aid, Timestamp};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Coordinator-attested session membership artifact (RFC-AITP-0010 §3).
8///
9/// The schema is `additionalProperties: false`; v0.1 has no `extensions`
10/// slot.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(deny_unknown_fields)]
13pub struct SessionTrustBundle {
14 /// MUST be `"aitp/0.2"` for this RFC.
15 pub version: String,
16 /// UUID v4 unique to this session. Used as a replay-binding scope.
17 pub session_id: Uuid,
18 /// Coordinator's AID. MUST match the `issuer` of every embedded TCT.
19 pub coordinator: Aid,
20 /// When this bundle was signed.
21 pub issued_at: Timestamp,
22 /// When the bundle MUST NOT be used after. MUST equal
23 /// `min(participants[*].tct.expires_at)` (RFC-AITP-0010 §6).
24 pub expires_at: Timestamp,
25 /// One entry per session participant.
26 pub participants: Vec<ParticipantEntry>,
27 /// Coordinator's signature over the canonical bundle JSON
28 /// excluding `signature`. JCS rules per RFC-AITP-0001 §5.4.1.
29 pub signature: String,
30}
31
32/// One participant in a [`SessionTrustBundle`]: their AID + the
33/// coordinator-issued TCT they received during their bilateral
34/// handshake.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(deny_unknown_fields)]
37pub struct ParticipantEntry {
38 /// Participant's AID. MUST equal the embedded TCT's `aud` claim.
39 pub aid: Aid,
40 /// Coordinator → participant TCT as an **opaque compact JWS
41 /// string** (`typ: aitp-tct+jwt`, RFC-AITP-0001 §5.4.5), carried
42 /// verbatim — the outer bundle signature covers it byte-for-byte.
43 pub tct: String,
44}
45
46/// HTTP/transport-wrapped form (the `{"session_bundle": {...}}` shape
47/// that matches the JSON Schema `$id`).
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(deny_unknown_fields)]
50pub struct SessionBundleEnvelope {
51 /// The signed inner bundle.
52 pub session_bundle: SessionTrustBundle,
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use serde_json::json;
59
60 #[test]
61 fn rejects_unknown_top_field() {
62 let v = json!({
63 "version": "aitp/0.2",
64 "session_id": "00000000-0000-4000-8000-000000000000",
65 "coordinator": "aid:pubkey:O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
66 "issued_at": 1_700_000_000,
67 "expires_at": 1_700_010_000,
68 "participants": [],
69 "signature": "A".repeat(86),
70 "rogue": 1,
71 });
72 assert!(serde_json::from_value::<SessionTrustBundle>(v).is_err());
73 }
74}