Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_jdat/src/string/canon.rs

13.3 KiB, 15 runs

created by r1870400018:20206, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Canonical JSON, for daticles a signature has to cover.
2//!
3//! A signature over JSON is a signature over bytes, so the signer and the verifier must agree on
4//! the bytes exactly. [`Dat::json`] does not settle that: it puts a space after each colon and each
5//! comma, which is readable and is not what a browser's `JSON.stringify` produces. Verifying a
6//! browser's signature therefore needs a second, narrower encoding whose output is fixed.
7//!
8//! [`Dat::json_canonical`] is that encoding, following RFC 8785 (JSON Canonicalisation Scheme):
9//!
10//! - No whitespace anywhere outside string literals.
11//! - Object members sorted by their keys' UTF-16 code units.
12//! - Strings escaped as RFC 8785 §3.2.2.2 requires -- the two mandatory escapes, the five
13//! short escapes, `\u00xx` in lowercase hex for the remaining C0 controls, and every other
14//! character emitted as itself in UTF-8.
15//!
16//! What it refuses matters as much as what it writes, because a caller who cannot get the same
17//! bytes from the other end should be told rather than handed bytes that only look canonical:
18//!
19//! - A float has no canonical form here. RFC 8785 §3.2.2.3 defers to ECMAScript's number
20//! serialisation, which this encoder does not implement; carry the value as a string instead.
21//! - An integer beyond 2^53 - 1 is refused, because a JavaScript signer cannot hold it exactly and
22//! so cannot have signed the digits we would write.
23//! - Bytes, tuples, vectors, user kinds and the rest of the daticle catalogue have no JSON form at
24//! all, and each is named in the error rather than silently coerced.
25//!
26//! Object keys must be strings, which JSON requires and JDAT does not.
27
28use crate::{
29 daticle::{
30 Dat,
31 Daticle,
32 },
33 map::DaticleMap,
34};
35
36use oxedyne_fe2o3_core::prelude::*;
37
38
39/// The largest integer a JavaScript number holds exactly, `2^53 - 1`.
40const JS_SAFE_INTEGER: i128 = 9_007_199_254_740_991;
41
42impl Dat {
43 /// Encode this daticle as canonical JSON, per RFC 8785.
44 ///
45 /// The output carries no whitespace outside string literals and orders object members by their
46 /// keys, so two ends that agree on the value agree on the bytes. See the module documentation
47 /// for the daticle kinds this refuses and why.
48 pub fn json_canonical(&self) -> Outcome<String> {
49 let mut out = String::new();
50 res!(write_canonical(self, &mut out));
51 Ok(out)
52 }
53
54 /// Encode this object as canonical JSON with the named top-level members left out.
55 ///
56 /// These are the bytes a signature covers when the signature itself, and any envelope around
57 /// the signed body, travel inside the object they sign. A name that is absent is no fault, so
58 /// the bare body and the whole envelope give the same bytes.
59 pub fn json_canonical_without(&self, omit: &[&str]) -> Outcome<String> {
60 match self {
61 Dat::Map(m) => {
62 let mut out = String::new();
63 res!(write_map(m, omit, &mut out));
64 Ok(out)
65 },
66 other => Err(err!(
67 "Only an object has members to leave out of its canonical JSON, and this \
68 daticle is of kind {:?}.", other.kind();
69 Invalid, Input, Mismatch)),
70 }
71 }
72}
73
74/// Append the canonical JSON encoding of `dat` to `out`.
75fn write_canonical(dat: &Dat, out: &mut String) -> Outcome<()> {
76 match dat {
77 Dat::Empty => out.push_str("null"),
78 Dat::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
79 Dat::Str(s) => write_string(s, out),
80 Dat::Opt(boxopt) => match &**boxopt {
81 None => out.push_str("null"),
82 Some(d) => res!(write_canonical(d, out)),
83 },
84 Dat::Box(d) => res!(write_canonical(d, out)),
85 Dat::U8(n) => res!(write_integer(*n as i128, out)),
86 Dat::U16(n) => res!(write_integer(*n as i128, out)),
87 Dat::U32(n) => res!(write_integer(*n as i128, out)),
88 Dat::U64(n) => res!(write_integer(*n as i128, out)),
89 Dat::U128(n) => res!(write_integer(*n as i128, out)),
90 Dat::I8(n) => res!(write_integer(*n as i128, out)),
91 Dat::I16(n) => res!(write_integer(*n as i128, out)),
92 Dat::I32(n) => res!(write_integer(*n as i128, out)),
93 Dat::I64(n) => res!(write_integer(*n as i128, out)),
94 Dat::I128(n) => res!(write_integer(*n, out)),
95 Dat::List(items) => {
96 out.push('[');
97 for (i, item) in items.iter().enumerate() {
98 if i > 0 {
99 out.push(',');
100 }
101 res!(write_canonical(item, out));
102 }
103 out.push(']');
104 },
105 Dat::Map(m) => res!(write_map(m, &[], out)),
106 other => return Err(err!(
107 "A daticle of kind {:?} has no canonical JSON form. If a signature must cover \
108 it, carry it as a string.", other.kind();
109 Invalid, Input, Unimplemented)),
110 }
111 Ok(())
112}
113
114/// Append an object less the members named in `omit`, its members ordered by their keys' UTF-16
115/// code units.
116fn write_map(m: &DaticleMap, omit: &[&str], out: &mut String) -> Outcome<()> {
117 let mut entries: Vec<(&String, &Dat)> = Vec::with_capacity(m.len());
118 for (k, v) in m.iter() {
119 match k {
120 Dat::Str(s) if omit.contains(&s.as_str()) => (),
121 Dat::Str(s) => entries.push((s, v)),
122 other => return Err(err!(
123 "A JSON object key must be a string, and this one is of kind {:?}.",
124 other.kind();
125 Invalid, Input, Mismatch)),
126 }
127 }
128 entries.sort_by(|(a, _), (b, _)| utf16_units(a).cmp(&utf16_units(b)));
129 out.push('{');
130 for (i, (k, v)) in entries.iter().enumerate() {
131 if i > 0 {
132 out.push(',');
133 }
134 write_string(k, out);
135 out.push(':');
136 res!(write_canonical(v, out));
137 }
138 out.push('}');
139 Ok(())
140}
141
142/// Append an integer, refusing one a JavaScript signer could not have held exactly.
143fn write_integer(n: i128, out: &mut String) -> Outcome<()> {
144 if n > JS_SAFE_INTEGER || n < -JS_SAFE_INTEGER {
145 return Err(err!(
146 "The integer {} is beyond 2^53 - 1, so a JavaScript signer cannot hold it \
147 exactly and cannot have signed these digits. Carry it as a string.", n;
148 Invalid, Input, TooBig));
149 }
150 out.push_str(&fmt!("{}", n));
151 Ok(())
152}
153
154/// Append a JSON string literal, escaped as RFC 8785 §3.2.2.2 requires.
155fn write_string(s: &str, out: &mut String) {
156 out.push('"');
157 for c in s.chars() {
158 match c {
159 '"' => out.push_str("\\\""),
160 '\\' => out.push_str("\\\\"),
161 '\u{08}' => out.push_str("\\b"),
162 '\u{0c}' => out.push_str("\\f"),
163 '\n' => out.push_str("\\n"),
164 '\r' => out.push_str("\\r"),
165 '\t' => out.push_str("\\t"),
166 c if (c as u32) < 0x20 => out.push_str(&fmt!("\\u{:04x}", c as u32)),
167 c => out.push(c),
168 }
169 }
170 out.push('"');
171}
172
173/// The UTF-16 code units of a string, which is the order RFC 8785 §3.2.3 sorts keys by. It differs
174/// from Rust's own string ordering only above the basic multilingual plane, where a surrogate pair
175/// sorts below the unpaired code points that follow it.
176fn utf16_units(s: &str) -> Vec<u16> {
177 s.encode_utf16().collect()
178}
179
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 use crate::prelude::*;
186
187 /// The members of RFC 8785 §3.2.3's ordering example, arriving in the order the RFC lists them
188 /// (its repeated member dropped, since this crate's decoder refuses a duplicate key). The
189 /// expected order is the one the RFC mandates -- ascending UTF-16 code units, so the vertical
190 /// tab at U+000B sorts between the newline and the carriage return, and the digit sorts after
191 /// all three -- which is not the order they were written in, nor the order a byte-wise sort of
192 /// the escaped forms would give.
193 #[test]
194 fn test_json_canonical_rfc8785_ordering_00() -> Outcome<()> {
195 let src = r#"{"\u20ac":"Euro Sign","\r":"Carriage Return","\u000a":"Newline","1":"One","\u0080":"Control","\u00f6":"Latin Small Letter O With Diaeresis","\u000b":"Vertical Tab"}"#;
196 let dat = res!(Dat::decode_string(src));
197 assert_eq!(
198 res!(dat.json_canonical()),
199 "{\"\\n\":\"Newline\",\"\\u000b\":\"Vertical Tab\",\"\\r\":\"Carriage Return\",\
200 \"1\":\"One\",\"\u{80}\":\"Control\",\"\u{f6}\":\"Latin Small Letter O With \
201 Diaeresis\",\"\u{20ac}\":\"Euro Sign\"}",
202 );
203 Ok(())
204 }
205
206 /// What a browser signs, read back and re-encoded, must be the bytes the browser signed. This
207 /// is the shape a ceremony attestation takes: nested objects, a boolean, an empty string and a
208 /// null.
209 #[test]
210 fn test_json_canonical_round_trips_a_signed_object_00() -> Outcome<()> {
211 // As JSON.stringify emits it once the keys are sorted: no spaces, null for an absent value.
212 let signed = "{\"age_band\":\"adult\",\"age_belief\":true,\"captures\":\
213 {\"face\":\"aG91c2U\",\"hands\":\"simulated\"},\"evidence\":\"\",\
214 \"place\":{\"cell\":null,\"how\":\"device\"}}";
215 // Arriving with the members in a different order, as a client is free to send them.
216 let arrived = "{\"place\":{\"how\":\"device\",\"cell\":null},\"evidence\":\"\",\
217 \"captures\":{\"hands\":\"simulated\",\"face\":\"aG91c2U\"},\
218 \"age_belief\":true,\"age_band\":\"adult\"}";
219 let dat = res!(Dat::decode_string(arrived));
220 assert_eq!(res!(dat.json_canonical()), signed);
221 Ok(())
222 }
223
224 /// The five short escapes, the two mandatory ones, and a control character with no short form.
225 #[test]
226 fn test_json_canonical_escapes_00() -> Outcome<()> {
227 let dat = dat!("a\"b\\c\nd\re\tf\u{08}g\u{0c}h\u{1f}i");
228 assert_eq!(
229 res!(dat.json_canonical()),
230 "\"a\\\"b\\\\c\\nd\\re\\tf\\bg\\fh\\u001fi\"",
231 );
232 Ok(())
233 }
234
235 /// A character outside ASCII is written as itself, as `JSON.stringify` writes it. Escaping it
236 /// would produce bytes no browser signed.
237 #[test]
238 fn test_json_canonical_leaves_non_ascii_literal_00() -> Outcome<()> {
239 let dat = dat!("Cœur — 日本");
240 assert_eq!(res!(dat.json_canonical()), "\"Cœur — 日本\"");
241 Ok(())
242 }
243
244 /// Lists keep their order, which is data, unlike object member order, which is not.
245 #[test]
246 fn test_json_canonical_list_order_kept_00() -> Outcome<()> {
247 let dat = listdat!["z", "a", 1u8, true, Dat::Empty];
248 assert_eq!(res!(dat.json_canonical()), "[\"z\",\"a\",1,true,null]");
249 Ok(())
250 }
251
252 /// A float is refused rather than written in a form the other end may not reproduce.
253 #[test]
254 fn test_json_canonical_refuses_a_float_00() -> Outcome<()> {
255 assert!(dat!(1.5f64).json_canonical().is_err(),
256 "a float has no canonical form here and must be refused");
257 Ok(())
258 }
259
260 /// An integer a JavaScript number cannot hold exactly is refused: the digits we would write are
261 /// not the digits the other end signed.
262 #[test]
263 fn test_json_canonical_refuses_an_unsafe_integer_00() -> Outcome<()> {
264 assert!(dat!(9_007_199_254_740_991u64).json_canonical().is_ok(),
265 "2^53 - 1 is exactly representable and must be accepted");
266 assert!(dat!(9_007_199_254_740_992u64).json_canonical().is_err(),
267 "2^53 must be refused");
268 Ok(())
269 }
270
271 /// Bytes are not JSON, and the error must name the kind so the caller knows what to change.
272 #[test]
273 fn test_json_canonical_refuses_bytes_00() -> Outcome<()> {
274 assert!(Dat::BU8(vec![1, 2, 3]).json_canonical().is_err(),
275 "a byte string has no JSON form");
276 Ok(())
277 }
278
279 /// A non-string object key is not JSON either.
280 #[test]
281 fn test_json_canonical_refuses_a_non_string_key_00() -> Outcome<()> {
282 let mut m = DaticleMap::new();
283 m.insert(dat!(1u8), dat!("one"));
284 assert!(Dat::Map(m).json_canonical().is_err(),
285 "an integer object key has no JSON form");
286 Ok(())
287 }
288
289 /// The named members are left out at the top level only, a nested member of the same name
290 /// stays, and the bare body and the whole envelope give the same bytes.
291 #[test]
292 fn test_json_canonical_without_leaves_out_top_level_members_00() -> Outcome<()> {
293 let envelope = res!(Dat::decode_string(
294 "{\"id\":7,\"kind\":\"Claim\",\"body\":{\"sig\":\"kept\",\"a\":1},\"b\":true,\"sig\":\"xyz\"}"));
295 let bare = res!(Dat::decode_string("{\"b\":true,\"body\":{\"a\":1,\"sig\":\"kept\"}}"));
296 let want = "{\"b\":true,\"body\":{\"a\":1,\"sig\":\"kept\"}}";
297 assert_eq!(res!(envelope.json_canonical_without(&["id", "kind", "sig"])), want);
298 assert_eq!(res!(bare.json_canonical_without(&["id", "kind", "sig"])), want);
299 assert_eq!(res!(bare.json_canonical_without(&[])), res!(bare.json_canonical()));
300 Ok(())
301 }
302
303 /// Only an object has members to leave out.
304 #[test]
305 fn test_json_canonical_without_refuses_a_non_object_00() -> Outcome<()> {
306 assert!(listdat!["sig"].json_canonical_without(&["sig"]).is_err(),
307 "a list has no members to leave out");
308 Ok(())
309 }
310}