Skip to main content

ratify_protocol/
canonical.rs

1//! Canonical JSON serialization per Ratify Protocol SPEC ยง6.
2//!
3//! Every implementation MUST produce byte-identical output for the same
4//! input or signatures will not verify across languages.
5//!
6//! Rules:
7//!   - Object members in lex order (byte order on UTF-8), RECURSIVELY.
8//!   - No whitespace between tokens. No trailing newline.
9//!   - UTF-8 encoding.
10//!   - Integers as shortest decimal.
11//!   - Byte arrays as base64-standard strings with padding.
12//!   - '<', '>', '&' pass through unmodified.
13//!   - U+2028 / U+2029 escape as \\u2028 / \\u2029 (matches Go behavior).
14//!   - Minimum string escaping per RFC 8259.
15
16#[cfg(not(feature = "std"))]
17use alloc::{format, string::String, string::ToString, vec::Vec};
18
19use base64::{engine::general_purpose::STANDARD, Engine as _};
20
21/// Canonical JSON-encode a serde_json::Value.
22///
23/// Available with the `std` feature only (requires `serde_json`).
24#[cfg(feature = "std")]
25pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
26    let mut out = String::new();
27    encode_value(value, &mut out);
28    out.into_bytes()
29}
30
31#[cfg(feature = "std")]
32fn encode_value(v: &serde_json::Value, out: &mut String) {
33    use serde_json::Value;
34    match v {
35        Value::Null => out.push_str("null"),
36        Value::Bool(true) => out.push_str("true"),
37        Value::Bool(false) => out.push_str("false"),
38        Value::Number(n) => out.push_str(&encode_number(n)),
39        Value::String(s) => encode_string(s, out),
40        Value::Array(arr) => {
41            out.push('[');
42            for (i, item) in arr.iter().enumerate() {
43                if i > 0 {
44                    out.push(',');
45                }
46                encode_value(item, out);
47            }
48            out.push(']');
49        }
50        Value::Object(obj) => {
51            // Collect keys and sort lex-order.
52            let mut keys: Vec<&String> = obj.keys().collect();
53            keys.sort();
54            out.push('{');
55            let mut first = true;
56            for k in keys {
57                let val = &obj[k];
58                // Skip nulls โ€” matches Go's omitempty for optional fields.
59                if val.is_null() {
60                    continue;
61                }
62                if !first {
63                    out.push(',');
64                }
65                encode_string(k, out);
66                out.push(':');
67                encode_value(val, out);
68                first = false;
69            }
70            out.push('}');
71        }
72    }
73}
74
75// encode_number reproduces the Go / TS / Python policy: integers and
76// integer-valued floats serialize as shortest decimal integer (no "500.0"),
77// non-integer floats serialize via their default textual representation.
78// Without this, serde_json's f64 default emits "500.0" where the other
79// implementations emit "500", breaking cross-SDK byte identicality.
80#[cfg(feature = "std")]
81fn encode_number(n: &serde_json::Number) -> String {
82    if let Some(i) = n.as_i64() {
83        return i.to_string();
84    }
85    if let Some(u) = n.as_u64() {
86        return u.to_string();
87    }
88    if let Some(f) = n.as_f64() {
89        if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
90            // Whole-number f64 โ†’ integer form.
91            return (f as i64).to_string();
92        }
93        return f.to_string();
94    }
95    n.to_string()
96}
97
98fn encode_string(s: &str, out: &mut String) {
99    out.push('"');
100    for c in s.chars() {
101        match c {
102            '"' => out.push_str("\\\""),
103            '\\' => out.push_str("\\\\"),
104            '\u{0008}' => out.push_str("\\b"),
105            '\u{0009}' => out.push_str("\\t"),
106            '\u{000A}' => out.push_str("\\n"),
107            '\u{000C}' => out.push_str("\\f"),
108            '\u{000D}' => out.push_str("\\r"),
109            '\u{2028}' => out.push_str("\\u2028"),
110            '\u{2029}' => out.push_str("\\u2029"),
111            c if (c as u32) < 0x20 => {
112                out.push_str(&format!("\\u{:04x}", c as u32));
113            }
114            // Everything else passes through unmodified (NO HTML escape).
115            c => out.push(c),
116        }
117    }
118    out.push('"');
119}
120
121/// Standard base64 encode with padding.
122pub fn base64_std_encode(data: &[u8]) -> String {
123    STANDARD.encode(data)
124}
125
126/// Standard base64 decode.
127pub fn base64_std_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
128    STANDARD.decode(s)
129}
130
131/// Lowercase hex.
132pub fn hex_encode(data: &[u8]) -> String {
133    hex::encode(data)
134}
135
136/// Lower- or upper-case hex.
137pub fn hex_decode(s: &str) -> Result<Vec<u8>, hex::FromHexError> {
138    hex::decode(s)
139}
140
141// ---------------------------------------------------------------------------
142// Low-level canonical-JSON helpers (no serde_json::Value intermediary)
143//
144// These write directly into a &mut String and are the building blocks for
145// the no_std signing-bytes functions in crypto.rs and receipts.rs.
146// ---------------------------------------------------------------------------
147
148/// Write a canonical-JSON string (with escaping) into `out`.
149pub fn encode_str(s: &str, out: &mut String) {
150    encode_string(s, out);
151}
152
153/// Write a canonical-JSON i64 (shortest decimal) into `out`.
154pub fn encode_i64(n: i64, out: &mut String) {
155    out.push_str(&n.to_string());
156}
157
158/// Write a canonical-JSON i32 into `out`.
159pub fn encode_i32(n: i32, out: &mut String) {
160    out.push_str(&n.to_string());
161}
162
163/// Write a canonical-JSON f64 following the Ratify integer-valued rule:
164/// whole-number f64s emit as shortest decimal integer; others as-is.
165pub fn encode_f64(n: f64, out: &mut String) {
166    if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e15 {
167        out.push_str(&(n as i64).to_string());
168    } else {
169        out.push_str(&n.to_string());
170    }
171}
172
173/// Write a canonical-JSON bool into `out`.
174pub fn encode_bool(b: bool, out: &mut String) {
175    out.push_str(if b { "true" } else { "false" });
176}
177
178/// Write a base64-standard-encoded byte slice as a canonical-JSON string.
179pub fn encode_bytes_b64(b: &[u8], out: &mut String) {
180    let s = base64_std_encode(b);
181    encode_string(&s, out);
182}
183
184/// Write a canonical-JSON array of strings into `out`.
185pub fn encode_str_array(arr: &[String], out: &mut String) {
186    out.push('[');
187    for (i, s) in arr.iter().enumerate() {
188        if i > 0 {
189            out.push(',');
190        }
191        encode_string(s, out);
192    }
193    out.push(']');
194}
195
196/// Write a canonical-JSON array of [f64; 2] pairs (geo polygon points).
197pub fn encode_points_array(pts: &[[f64; 2]], out: &mut String) {
198    out.push('[');
199    for (i, pt) in pts.iter().enumerate() {
200        if i > 0 {
201            out.push(',');
202        }
203        out.push('[');
204        encode_f64(pt[0], out);
205        out.push(',');
206        encode_f64(pt[1], out);
207        out.push(']');
208    }
209    out.push(']');
210}
211
212/// Write a canonical Constraint object into `out`, matching the per-kind
213/// shape emitted by Constraint's Serialize impl (alphabetical keys).
214pub fn encode_constraint(c: &crate::types::Constraint, out: &mut String) {
215    match c.kind.as_str() {
216        "geo_circle" => {
217            // keys: lat, lon, radius_m, type
218            out.push('{');
219            out.push_str("\"lat\":");  encode_f64(c.lat, out);
220            out.push_str(",\"lon\":");  encode_f64(c.lon, out);
221            out.push_str(",\"radius_m\":");  encode_f64(c.radius_m, out);
222            out.push_str(",\"type\":");  encode_string(&c.kind, out);
223            out.push('}');
224        }
225        "geo_polygon" => {
226            // keys: points, type
227            out.push('{');
228            out.push_str("\"points\":");  encode_points_array(&c.points, out);
229            out.push_str(",\"type\":");  encode_string(&c.kind, out);
230            out.push('}');
231        }
232        "geo_bbox" => {
233            // base keys: max_lat, max_lon, min_lat, min_lon, type
234            // optional altitude keys (alphabetical insert): max_alt_m < max_lat, min_alt_m < min_lat
235            out.push('{');
236            let has_alt = c.min_alt_m != 0.0 || c.max_alt_m != 0.0;
237            if has_alt {
238                out.push_str("\"max_alt_m\":");  encode_f64(c.max_alt_m, out);
239                out.push(',');
240            }
241            out.push_str("\"max_lat\":");  encode_f64(c.max_lat, out);
242            out.push_str(",\"max_lon\":");  encode_f64(c.max_lon, out);
243            if has_alt {
244                out.push_str(",\"min_alt_m\":");  encode_f64(c.min_alt_m, out);
245            }
246            out.push_str(",\"min_lat\":");  encode_f64(c.min_lat, out);
247            out.push_str(",\"min_lon\":");  encode_f64(c.min_lon, out);
248            out.push_str(",\"type\":");  encode_string(&c.kind, out);
249            out.push('}');
250        }
251        "time_window" => {
252            // keys: end, start, type, tz
253            out.push('{');
254            out.push_str("\"end\":");  encode_string(&c.end, out);
255            out.push_str(",\"start\":");  encode_string(&c.start, out);
256            out.push_str(",\"type\":");  encode_string(&c.kind, out);
257            out.push_str(",\"tz\":");  encode_string(&c.tz, out);
258            out.push('}');
259        }
260        "max_speed_mps" => {
261            // keys: max_mps, type
262            out.push('{');
263            out.push_str("\"max_mps\":");  encode_f64(c.max_mps, out);
264            out.push_str(",\"type\":");  encode_string(&c.kind, out);
265            out.push('}');
266        }
267        "max_amount" => {
268            // keys: currency, max_amount, type
269            out.push('{');
270            out.push_str("\"currency\":");  encode_string(&c.currency, out);
271            out.push_str(",\"max_amount\":");  encode_f64(c.max_amount, out);
272            out.push_str(",\"type\":");  encode_string(&c.kind, out);
273            out.push('}');
274        }
275        "max_rate" => {
276            // keys: count, type, window_s
277            out.push('{');
278            out.push_str("\"count\":");  encode_i64(c.count, out);
279            out.push_str(",\"type\":");  encode_string(&c.kind, out);
280            out.push_str(",\"window_s\":");  encode_i64(c.window_s, out);
281            out.push('}');
282        }
283        // Unknown kind: emit only the type tag, matching the Serialize impl.
284        _ => {
285            out.push('{');
286            out.push_str("\"type\":");  encode_string(&c.kind, out);
287            out.push('}');
288        }
289    }
290}
291
292/// Write a canonical-JSON array of Constraint objects.
293pub fn encode_constraints(cs: &[crate::types::Constraint], out: &mut String) {
294    out.push('[');
295    for (i, c) in cs.iter().enumerate() {
296        if i > 0 {
297            out.push(',');
298        }
299        encode_constraint(c, out);
300    }
301    out.push(']');
302}
303
304/// Write a canonical HybridPublicKey object: `{"ed25519":"...","ml_dsa_65":"..."}`.
305/// Keys are already in lex order: "ed25519" < "ml_dsa_65".
306pub fn encode_hybrid_pub_key(pk: &crate::types::HybridPublicKey, out: &mut String) {
307    out.push('{');
308    out.push_str("\"ed25519\":");  encode_bytes_b64(&pk.ed25519, out);
309    out.push_str(",\"ml_dsa_65\":");  encode_bytes_b64(&pk.ml_dsa_65, out);
310    out.push('}');
311}
312
313/// Write a canonical HybridSignature object: `{"ed25519":"...","ml_dsa_65":"..."}`.
314pub fn encode_hybrid_sig(sig: &crate::types::HybridSignature, out: &mut String) {
315    out.push('{');
316    out.push_str("\"ed25519\":");  encode_bytes_b64(&sig.ed25519, out);
317    out.push_str(",\"ml_dsa_65\":");  encode_bytes_b64(&sig.ml_dsa_65, out);
318    out.push('}');
319}
320
321/// serde helper to (de)serialize Vec<u8> as base64-standard strings.
322pub mod base64_bytes {
323    #[cfg(not(feature = "std"))]
324    use alloc::{format, string::String, vec::Vec};
325    use super::{base64_std_decode, base64_std_encode};
326    use serde::{de::Error, Deserialize, Deserializer, Serializer};
327
328    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
329    where
330        S: Serializer,
331    {
332        serializer.serialize_str(&base64_std_encode(bytes))
333    }
334
335    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
336    where
337        D: Deserializer<'de>,
338    {
339        let s = String::deserialize(deserializer)?;
340        base64_std_decode(&s).map_err(|e| D::Error::custom(format!("base64 decode: {e}")))
341    }
342}