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 | |
| 28 | use crate::{ |
| 29 | daticle::{ |
| 30 | Dat, |
| 31 | Daticle, |
| 32 | }, |
| 33 | map::DaticleMap, |
| 34 | }; |
| 35 | |
| 36 | use oxedyne_fe2o3_core::prelude::*; |
| 37 | |
| 38 | |
| 39 | /// The largest integer a JavaScript number holds exactly, `2^53 - 1`. |
| 40 | const JS_SAFE_INTEGER: i128 = 9_007_199_254_740_991; |
| 41 | |
| 42 | impl 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`. |
| 75 | fn 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. |
| 116 | fn 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. |
| 143 | fn 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. |
| 155 | fn 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. |
| 176 | fn utf16_units(s: &str) -> Vec<u16> { |
| 177 | s.encode_utf16().collect() |
| 178 | } |
| 179 | |
| 180 | |
| 181 | #[cfg(test)] |
| 182 | mod 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 | } |