aitp_session_bundle/types.rs
1//! Wire types for Session Trust Bundle (RFC-AITP-0010 §3).
2
3use aitp_core::{Aid, ExtensionsMap, Timestamp};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Full set of top-level members `aitp-session-bundle.schema.json`
8/// declares for the inner `session_bundle` object, including the
9/// `extensions` namespace key itself. Used by
10/// [`crate::wire::parse_session_bundle_wire`]'s body-level member-set
11/// check (RFC-AITP-0001 §7, RFC-AITP-0010 §5) — a violation here is
12/// [`crate::error::SessionBundleError::UnknownField`], distinct from a
13/// wrapper-level defect (RFC-AITP-0010 §3), which is
14/// [`crate::error::SessionBundleError::WireFormInvalid`] instead.
15///
16/// Anchored to the vendored schema by
17/// `crates/aitp-session-bundle/tests/schema.rs`, which asserts this list
18/// equals `properties.session_bundle.properties`'s keys — so this cannot
19/// silently drift from the spec.
20pub const SESSION_BUNDLE_MEMBERS: &[&str] = &[
21 "version",
22 "session_id",
23 "coordinator",
24 "issued_at",
25 "expires_at",
26 "participants",
27 "extensions",
28 "signature",
29];
30
31/// Coordinator-attested session membership artifact (RFC-AITP-0010 §3).
32///
33/// The schema is `additionalProperties: false`, with an explicit
34/// `extensions` slot reserved per RFC-AITP-0001 §7.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(deny_unknown_fields)]
37pub struct SessionTrustBundle {
38 /// MUST be `"aitp/0.3"` for this RFC.
39 pub version: String,
40 /// UUID v4 unique to this session. Used as a replay-binding scope.
41 pub session_id: Uuid,
42 /// Coordinator's AID. MUST match the `issuer` of every embedded TCT.
43 pub coordinator: Aid,
44 /// When this bundle was signed.
45 pub issued_at: Timestamp,
46 /// When the bundle MUST NOT be used after. MUST equal
47 /// `min(participants[*].tct.expires_at)` (RFC-AITP-0010 §6).
48 pub expires_at: Timestamp,
49 /// One entry per session participant.
50 pub participants: Vec<ParticipantEntry>,
51 /// Forward-compatible extension namespace (RFC-AITP-0001 §7).
52 ///
53 /// **Presence-sensitive**, deliberately modeled as `Option<ExtensionsMap>`
54 /// rather than a defaulted empty map with `skip_serializing_if =
55 /// "is_empty"`. Under RFC 8785 canonicalization, absent (`None`) emits
56 /// no `extensions` key at all, while present-but-empty
57 /// (`Some(ExtensionsMap::new())`) emits `"extensions":{}` — different
58 /// bytes, different digest, different signature. Silently normalizing
59 /// one into the other would change the signing input and break
60 /// verification against a conformant peer (RFC-AITP-0001 §5.4.1).
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub extensions: Option<ExtensionsMap>,
63 /// Coordinator's signature over the canonical bundle JSON
64 /// excluding `signature`. JCS rules per RFC-AITP-0001 §5.4.1.
65 pub signature: String,
66}
67
68/// One participant in a [`SessionTrustBundle`]: their AID + the
69/// coordinator-issued TCT they received during their bilateral
70/// handshake.
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(deny_unknown_fields)]
73pub struct ParticipantEntry {
74 /// Participant's AID. MUST equal the embedded TCT's `aud` claim.
75 pub aid: Aid,
76 /// Coordinator → participant TCT as an **opaque compact JWS
77 /// string** (`typ: aitp-tct+jwt`, RFC-AITP-0001 §5.4.5), carried
78 /// verbatim — the outer bundle signature covers it byte-for-byte.
79 pub tct: String,
80}
81
82/// HTTP/transport-wrapped form (the `{"session_bundle": {...}}` shape
83/// that matches the JSON Schema `$id`).
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(deny_unknown_fields)]
86pub struct SessionBundleEnvelope {
87 /// The signed inner bundle.
88 pub session_bundle: SessionTrustBundle,
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use serde_json::json;
95
96 #[test]
97 fn rejects_unknown_top_field() {
98 let v = json!({
99 "version": "aitp/0.3",
100 "session_id": "00000000-0000-4000-8000-000000000000",
101 "coordinator": "aid:pubkey:O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
102 "issued_at": 1_700_000_000,
103 "expires_at": 1_700_010_000,
104 "participants": [],
105 "signature": "A".repeat(86),
106 "rogue": 1,
107 });
108 assert!(serde_json::from_value::<SessionTrustBundle>(v).is_err());
109 }
110}