Skip to main content

wire/
canonical.rs

1//! Canonical wire-byte form for events + cards.
2//!
3//! Rules (v0.1):
4//!   1. Object keys serialize in lexicographic byte order.
5//!   2. No whitespace anywhere (`,` and `:` separators only).
6//!   3. UTF-8 throughout — non-ASCII is NOT \uXXXX-escaped.
7//!   4. The top-level fields `signature` and `public_key_id` are stripped
8//!      before serialization (they are computed *over* the canonical bytes,
9//!      so they cannot be inside them).
10//!   5. The top-level field `event_id` is stripped iff `strict = true` —
11//!      `compute_event_id` uses strict-mode bytes; `verify_message_v31`
12//!      uses non-strict because the wire copy carries `event_id` already.
13//!
14//! Implementation note — `serde_json::Map` uses `BTreeMap` internally when
15//! the `preserve_order` cargo feature is OFF (which is the default). This
16//! gives us free lexicographic key ordering at every nesting level. If a
17//! downstream crate ever enables `preserve_order` we'll need to walk and
18//! re-sort manually; for now the default is sufficient.
19
20use serde_json::Value;
21
22/// Strip metadata fields from a top-level object before canonicalization.
23///
24/// Always removes `signature` and `public_key_id`. Removes `event_id` iff
25/// `strict` is true.
26fn strip_meta(value: &Value, strict: bool) -> Value {
27    match value {
28        Value::Object(map) => {
29            let mut out = serde_json::Map::new();
30            for (k, v) in map {
31                if k == "signature" || k == "public_key_id" {
32                    continue;
33                }
34                if strict && k == "event_id" {
35                    continue;
36                }
37                out.insert(k.clone(), v.clone());
38            }
39            Value::Object(out)
40        }
41        other => other.clone(),
42    }
43}
44
45/// Canonical bytes for a JSON value.
46///
47/// `strict = true` excludes `event_id` (use when *computing* event_id).
48/// `strict = false` keeps `event_id` (use for transport/storage).
49pub fn canonical(value: &Value, strict: bool) -> Vec<u8> {
50    let stripped = strip_meta(value, strict);
51    serde_json::to_vec(&stripped).expect("canonical serialization is infallible for Value")
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use serde_json::json;
58
59    #[test]
60    fn excludes_signature_and_public_key_id() {
61        let v = json!({"a": 1, "signature": "sig", "public_key_id": "id"});
62        let out = canonical(&v, false);
63        assert!(!std::str::from_utf8(&out).unwrap().contains("signature"));
64        assert!(!std::str::from_utf8(&out).unwrap().contains("public_key_id"));
65    }
66
67    #[test]
68    fn strict_excludes_event_id() {
69        let v = json!({"a": 1, "event_id": "deadbeef"});
70        assert!(
71            !std::str::from_utf8(&canonical(&v, true))
72                .unwrap()
73                .contains("event_id")
74        );
75        assert!(
76            std::str::from_utf8(&canonical(&v, false))
77                .unwrap()
78                .contains("event_id")
79        );
80    }
81
82    #[test]
83    fn keys_are_sorted_lexicographically() {
84        let a = json!({"b": 1, "a": 2, "c": 3});
85        let b = json!({"c": 3, "a": 2, "b": 1});
86        assert_eq!(canonical(&a, false), canonical(&b, false));
87        let s = String::from_utf8(canonical(&a, false)).unwrap();
88        assert_eq!(s, r#"{"a":2,"b":1,"c":3}"#);
89    }
90
91    #[test]
92    fn no_whitespace_in_output() {
93        let v = json!({"x": [1, 2, 3], "y": {"z": "w"}});
94        let s = String::from_utf8(canonical(&v, false)).unwrap();
95        assert!(!s.contains(' '));
96        assert!(!s.contains('\n'));
97    }
98
99    #[test]
100    fn nested_objects_also_sorted() {
101        let v = json!({"outer": {"b": 1, "a": 2}});
102        let s = String::from_utf8(canonical(&v, false)).unwrap();
103        assert_eq!(s, r#"{"outer":{"a":2,"b":1}}"#);
104    }
105
106    #[test]
107    fn non_ascii_passes_through_unescaped() {
108        let v = json!({"name": "Pål"});
109        let s = String::from_utf8(canonical(&v, false)).unwrap();
110        assert!(s.contains("Pål"), "got {s}");
111    }
112}