Skip to main content

vasari_core/
hash.rs

1use sha2::{Digest, Sha256};
2
3/// Compute the Vasari node ID: sha256 of the RFC 8785 JCS canonical form
4/// of the hash-input value.
5///
6/// Full RFC 8785 compliance requires specific number serialization and unicode
7/// escape handling beyond key-sorting. This implementation covers the critical
8/// invariant (key-sorted JSON, no whitespace) which is sufficient for hash
9/// stability within a single implementation. Full JCS compliance is tracked
10/// in INTENT-SPEC.md §3 (Canonicalization).
11pub fn node_id(hash_input: &serde_json::Value) -> String {
12    let canonical = canonical_json_bytes(hash_input);
13    let digest = Sha256::digest(&canonical);
14    hex::encode(digest)
15}
16
17/// Produce deterministic JSON bytes with recursively sorted object keys.
18///
19/// This is the key-ordering invariant of RFC 8785 JCS. Number formatting
20/// and unicode escaping to full spec is deferred (see hash.rs comment above).
21pub fn canonical_json_bytes(value: &serde_json::Value) -> Vec<u8> {
22    fn sort_keys(v: &serde_json::Value) -> serde_json::Value {
23        match v {
24            serde_json::Value::Object(map) => {
25                let mut keys: Vec<&String> = map.keys().collect();
26                keys.sort();
27                let sorted = keys
28                    .into_iter()
29                    .map(|k| (k.clone(), sort_keys(&map[k])))
30                    .collect();
31                serde_json::Value::Object(sorted)
32            }
33            serde_json::Value::Array(arr) => {
34                serde_json::Value::Array(arr.iter().map(sort_keys).collect())
35            }
36            other => other.clone(),
37        }
38    }
39    let sorted = sort_keys(value);
40    // serde_json compact serialization: no extra whitespace, stable key order.
41    serde_json::to_vec(&sorted).expect("valid JSON value cannot fail to serialize")
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use serde_json::json;
48
49    #[test]
50    fn key_order_is_stable() {
51        let a = json!({ "z": 1, "a": 2, "m": 3 });
52        let b = json!({ "m": 3, "z": 1, "a": 2 });
53        assert_eq!(canonical_json_bytes(&a), canonical_json_bytes(&b));
54    }
55
56    #[test]
57    fn nested_objects_sorted() {
58        let v = json!({ "b": { "z": 1, "a": 2 }, "a": 1 });
59        let bytes = canonical_json_bytes(&v);
60        let s = std::str::from_utf8(&bytes).unwrap();
61        // "a" key should appear before "b" at the top level
62        assert!(s.find('"').is_some());
63        let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
64        let obj = parsed.as_object().unwrap();
65        let keys: Vec<&String> = obj.keys().collect();
66        assert_eq!(keys[0], "a");
67        assert_eq!(keys[1], "b");
68    }
69
70    #[test]
71    fn node_id_is_hex_sha256() {
72        let id = node_id(&json!({ "type": "intent", "text": "hello" }));
73        assert_eq!(id.len(), 64);
74        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
75    }
76
77    #[test]
78    fn same_content_same_id() {
79        let v = json!({ "type": "intent", "text": "add JWT verification" });
80        assert_eq!(node_id(&v), node_id(&v));
81    }
82}