Skip to main content

aitp_session_bundle/
wire.rs

1//! Wire-form parsing for the Session Trust Bundle transport envelope
2//! (RFC-AITP-0010 §3, §5).
3
4use crate::error::SessionBundleError;
5use crate::types::{SessionBundleEnvelope, SessionTrustBundle, SESSION_BUNDLE_MEMBERS};
6use aitp_core::{check_members, from_serde_error};
7use serde_json::Value;
8
9/// Full member set of the `{"session_bundle": {...}}` transport
10/// wrapper. Per RFC-AITP-0010 §3 the wrapper is `additionalProperties:
11/// false`: no member besides `session_bundle` is ever permitted, not
12/// even in a future schema revision, since the wrapper's sole purpose
13/// is to carry the signed body.
14const SESSION_BUNDLE_ENVELOPE_MEMBERS: &[&str] = &["session_bundle"];
15
16/// Parse a session bundle from its on-the-wire JSON.
17///
18/// Accepts the spec transport envelope `{"session_bundle": {<body incl.
19/// `signature`>}}` and, for internal callers that never wrap, a bare
20/// body. This function is where RFC-AITP-0010's two DISTINCT failure
21/// classes are told apart, by WHERE a violation occurs:
22///
23/// - **Wrapper-level** (§3): when a `{"session_bundle": …}` wrapper is
24///   present, its member set MUST be exactly `{session_bundle}`. Any
25///   sibling member — notably the pre-erratum shape where `signature`
26///   sat OUTSIDE the wrapped body — is a wire-FORM defect:
27///   [`SessionBundleError::WireFormInvalid`], which the error-code
28///   mapping surfaces as `SESSION_BUNDLE_INVALID`.
29/// - **Body-level** (§5): once the wrapper shape (if any) is confirmed
30///   valid, the inner body's member set is checked against
31///   [`SESSION_BUNDLE_MEMBERS`]. A member outside that set is not a wire
32///   defect — the wrapper/no-wrapper shape was correct — but an unknown
33///   member of the signed artifact itself:
34///   [`SessionBundleError::UnknownField`], the **core** `UNKNOWN_FIELD`
35///   code (RFC-AITP-0001 §7), even though the session-bundle artifact
36///   is itself draft. This check runs before any cryptographic work,
37///   including the coordinator key resolution used to verify the outer
38///   signature (RFC-AITP-0010 §5 step 5) — a bundle with an unknown body
39///   member is rejected without ever deriving that key.
40/// - Once both member-set checks pass, the value is typed-deserialized.
41///   A residual `#[serde(deny_unknown_fields)]` error at that point can
42///   only come from a NESTED closed object (a `participants[]` entry);
43///   [`from_serde_error`] recovers the offending field name so it, too,
44///   reports [`SessionBundleError::UnknownField`] rather than a generic
45///   parse failure.
46///
47/// The bare-body path (no wrapper, used by internal callers) applies the
48/// SAME body-level member-set check as the wrapped form, so the two
49/// entry points never disagree about what a valid body looks like.
50pub fn parse_session_bundle_wire(value: &Value) -> Result<SessionTrustBundle, SessionBundleError> {
51    if value.get("session_bundle").is_some() {
52        check_members(
53            "SessionBundleEnvelope",
54            value,
55            SESSION_BUNDLE_ENVELOPE_MEMBERS,
56        )
57        .map_err(|e| {
58            SessionBundleError::WireFormInvalid(format!(
59                "transport envelope: unknown field `{}` (RFC-AITP-0010 §3 places \
60                 `signature` inside the signed body, never beside the wrapper)",
61                e.field
62            ))
63        })?;
64
65        let body = value.get("session_bundle").expect("checked above");
66        check_members("SessionTrustBundle", body, SESSION_BUNDLE_MEMBERS)
67            .map_err(|e| SessionBundleError::UnknownField(e.field))?;
68
69        let env: SessionBundleEnvelope = serde_json::from_value(value.clone()).map_err(|e| {
70            if let Some(field) = from_serde_error(&e) {
71                SessionBundleError::UnknownField(field)
72            } else {
73                SessionBundleError::WireFormInvalid(format!("transport envelope: {e}"))
74            }
75        })?;
76        Ok(env.session_bundle)
77    } else {
78        check_members("SessionTrustBundle", value, SESSION_BUNDLE_MEMBERS)
79            .map_err(|e| SessionBundleError::UnknownField(e.field))?;
80
81        serde_json::from_value(value.clone()).map_err(|e| {
82            if let Some(field) = from_serde_error(&e) {
83                SessionBundleError::UnknownField(field)
84            } else {
85                SessionBundleError::WireFormInvalid(format!("bundle body: {e}"))
86            }
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use serde_json::json;
95
96    fn valid_body() -> Value {
97        json!({
98            "version": "aitp/0.2",
99            "session_id": "00000000-0000-4000-8000-000000000000",
100            "coordinator": "aid:pubkey:O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
101            "issued_at": 1_700_000_000,
102            "expires_at": 1_700_010_000,
103            "participants": [],
104            "signature": "A".repeat(86),
105        })
106    }
107
108    #[test]
109    fn wrapped_with_signature_inside_parses() {
110        let wire = json!({ "session_bundle": valid_body() });
111        assert!(parse_session_bundle_wire(&wire).is_ok());
112    }
113
114    #[test]
115    fn bare_body_parses() {
116        assert!(parse_session_bundle_wire(&valid_body()).is_ok());
117    }
118
119    #[test]
120    fn sibling_signature_is_rejected() {
121        let mut body = valid_body();
122        let signature = body.as_object_mut().unwrap().remove("signature").unwrap();
123        let wire = json!({ "session_bundle": body, "signature": signature });
124        assert!(matches!(
125            parse_session_bundle_wire(&wire),
126            Err(SessionBundleError::WireFormInvalid(_))
127        ));
128    }
129
130    #[test]
131    fn unexpected_sibling_key_is_rejected() {
132        let wire = json!({ "session_bundle": valid_body(), "extra": "nope" });
133        assert!(matches!(
134            parse_session_bundle_wire(&wire),
135            Err(SessionBundleError::WireFormInvalid(_))
136        ));
137    }
138
139    #[test]
140    fn bare_body_missing_signature_is_rejected() {
141        let mut body = valid_body();
142        body.as_object_mut().unwrap().remove("signature");
143        assert!(matches!(
144            parse_session_bundle_wire(&body),
145            Err(SessionBundleError::WireFormInvalid(_))
146        ));
147    }
148
149    /// `bundle-006-unknown-field-rejected`: an unknown member of the
150    /// INNER body (not inside `extensions`, not a wrapper sibling) is a
151    /// body-level defect, distinct from the wrapper-level
152    /// `WireFormInvalid` cases above.
153    #[test]
154    fn unknown_body_member_is_unknown_field() {
155        let mut body = valid_body();
156        body.as_object_mut()
157            .unwrap()
158            .insert("coordinator_note".into(), json!("primary-region"));
159        let wire = json!({ "session_bundle": body });
160        match parse_session_bundle_wire(&wire) {
161            Err(SessionBundleError::UnknownField(field)) => {
162                assert_eq!(field, "coordinator_note")
163            }
164            other => panic!("expected UnknownField(\"coordinator_note\"), got {other:?}"),
165        }
166    }
167
168    /// Same body-level defect via the bare-body entry point: the two
169    /// entry points must not disagree about what a valid body looks
170    /// like.
171    #[test]
172    fn unknown_bare_body_member_is_unknown_field() {
173        let mut body = valid_body();
174        body.as_object_mut()
175            .unwrap()
176            .insert("rogue".into(), json!(1));
177        match parse_session_bundle_wire(&body) {
178            Err(SessionBundleError::UnknownField(field)) => assert_eq!(field, "rogue"),
179            other => panic!("expected UnknownField(\"rogue\"), got {other:?}"),
180        }
181    }
182
183    /// A member inside `extensions` is never inspected — the MUST-ignore
184    /// half of RFC-AITP-0001 §7 — even though the body-level member-set
185    /// check now runs (`bundle-005-extensions-accepted`'s ignore-half
186    /// guard, at the wire-parsing layer rather than full verification).
187    #[test]
188    fn extensions_contents_are_never_treated_as_unknown_members() {
189        let mut body = valid_body();
190        body.as_object_mut()
191            .unwrap()
192            .insert("extensions".into(), json!({ "tee": { "platform": "sgx" } }));
193        let wire = json!({ "session_bundle": body });
194        assert!(parse_session_bundle_wire(&wire).is_ok());
195    }
196
197    /// A nested closed object (`participants[]` entry) with an unknown
198    /// member is not caught by the top-level [`SESSION_BUNDLE_MEMBERS`]
199    /// check — that only inspects the body's own keys — but the residual
200    /// `deny_unknown_fields` error from `ParticipantEntry` is still
201    /// recovered via `from_serde_error` and reported as `UnknownField`,
202    /// never a generic parse failure.
203    #[test]
204    fn unknown_participant_entry_member_is_unknown_field_via_from_serde_error() {
205        let mut body = valid_body();
206        body["participants"] = json!([{
207            "aid": "aid:pubkey:O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
208            "tct": "a.b.c",
209            "rogue": "nope",
210        }]);
211        let wire = json!({ "session_bundle": body });
212        match parse_session_bundle_wire(&wire) {
213            Err(SessionBundleError::UnknownField(field)) => assert_eq!(field, "rogue"),
214            other => panic!("expected UnknownField(\"rogue\"), got {other:?}"),
215        }
216    }
217
218    /// The security property RFC-AITP-0010 §5 places this check for: even
219    /// a bundle whose `coordinator` is not a resolvable AID at all is
220    /// rejected on its unknown body member BEFORE anything downstream
221    /// would attempt to interpret — let alone resolve — that field. The
222    /// member-set check operates purely on the raw JSON key set, so it
223    /// never reaches, parses, or resolves `coordinator`.
224    #[test]
225    fn unknown_body_member_is_rejected_before_coordinator_key_resolution() {
226        let mut body = valid_body();
227        // Not a well-formed AID at all — if this were ever parsed or a
228        // key derived from it, that would panic or error with something
229        // other than UnknownField. It never gets that far.
230        body["coordinator"] = json!("not-an-aid-and-not-even-attempted");
231        body.as_object_mut()
232            .unwrap()
233            .insert("rogue".into(), json!("nope"));
234        let wire = json!({ "session_bundle": body });
235        match parse_session_bundle_wire(&wire) {
236            Err(SessionBundleError::UnknownField(field)) => assert_eq!(field, "rogue"),
237            other => panic!(
238                "expected the unknown-member check to short-circuit before `coordinator` is \
239                 ever interpreted, got {other:?}"
240            ),
241        }
242    }
243}