Skip to main content

ev/
canonical.rs

1//! Canonical-JSON (RFC 8785 / JCS) + content-addressed identity.
2//! For our string-only payload, serde_json's default output (sorted BTreeMap
3//! keys, compact separators, raw non-ASCII, no `/`-escape) IS JCS โ€” verified by
4//! the golden vectors. Liveness arrays are sorted+deduped here so set-valued
5//! fields are order-insensitive; grounds[] keeps authored order.
6use crate::tick::{Check, Tick};
7use serde_json::{json, Map, Value};
8use sha2::{Digest, Sha256};
9
10fn sorted_set(v: &[String]) -> Vec<String> {
11    let mut s: Vec<String> = v.to_vec();
12    s.sort(); // byte order == codepoint == UTF-16 order for the ASCII tokens we use
13    s.dedup();
14    s
15}
16
17fn check_value(c: &Check) -> Value {
18    match c {
19        Check::Person { reference } => json!({ "by": "person", "ref": reference }),
20        Check::Test {
21            reference,
22            verified_at_sha,
23            counter_test,
24            liveness,
25        } => json!({
26            "by": "test",
27            "ref": reference,
28            "verified_at_sha": verified_at_sha,
29            "counter_test": counter_test,
30            "liveness": {
31                "platforms":    sorted_set(&liveness.platforms),
32                "triggered_by": sorted_set(&liveness.triggered_by),
33                "surfaces":     sorted_set(&liveness.surfaces),
34            }
35        }),
36    }
37}
38
39/// The Value containing ONLY the hashed fields (decision, observe, grounds, parent_id).
40pub fn hashed_value(t: &Tick) -> Value {
41    let grounds: Vec<Value> = t
42        .grounds
43        .iter()
44        .map(|g| {
45            // Built as a manual Map (not json!) so an absent check OMITS the key
46            // entirely โ€” never serializes as null (design ยง4.8).
47            let mut o = Map::new();
48            o.insert("claim".into(), Value::String(g.claim.clone()));
49            o.insert("supports".into(), Value::String(g.supports.clone()));
50            if let Some(c) = &g.check {
51                o.insert("check".into(), check_value(c));
52            }
53            Value::Object(o)
54        })
55        .collect();
56    json!({
57        "decision": t.decision,
58        "observe": t.observe,
59        "grounds": grounds,
60        "parent_id": t.parent_id,
61    })
62}
63
64/// Canonical bytes for our **string-only** hashed `Value`. serde_json's compact
65/// output over a sorted-key `Value` equals RFC-8785/JCS *only* because the payload
66/// contains no numbers/bools/nulls (JCS number canonicalization is not applied).
67/// Do not reuse on a number-bearing `Value` (see module header).
68pub fn canonical_json(v: &Value) -> String {
69    serde_json::to_string(v).expect("Value is serializable")
70}
71
72/// id = first 12 hex of SHA-256 over the canonical-JSON of the hashed fields.
73pub fn compute_id(t: &Tick) -> String {
74    let canon = canonical_json(&hashed_value(t));
75    let full = hex::encode(Sha256::digest(canon.as_bytes()));
76    full[..12].to_string()
77}