confium_privacy/
jsonld_signing.rs1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SignedJsonLd {
9 pub document: Value,
11 pub proof: Proof,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Proof {
18 #[serde(rename = "type")]
20 pub proof_type: String,
21 pub created: chrono::DateTime<chrono::Utc>,
23 pub verification_method: String,
25 pub proof_purpose: String,
27 pub proof_value: String,
29}
30
31pub fn canonicalize(document: &Value) -> Vec<u8> {
34 let mut canonical = canonicalize_value(document);
37 canonical.sort();
38 let mut result = Vec::new();
39 for entry in canonical {
40 result.extend_from_slice(entry.as_bytes());
41 result.push(b'\n');
42 }
43 result
44}
45
46fn canonicalize_value(value: &Value) -> Vec<String> {
47 let mut entries = Vec::new();
48 match value {
49 Value::Object(map) => {
50 let mut keys: Vec<&String> = map.keys().collect();
51 keys.sort();
52 for key in keys {
53 if key == "proof" {
54 continue;
55 }
56 let val = &map[key];
57 match val {
58 Value::String(s) => {
59 entries.push(format!("{key}:{s}"));
60 }
61 Value::Number(n) => {
62 entries.push(format!("{key}:{n}"));
63 }
64 Value::Bool(b) => {
65 entries.push(format!("{key}:{b}"));
66 }
67 Value::Null => {
68 entries.push(format!("{key}:null"));
69 }
70 _ => {
71 let sub = canonicalize_value(val);
72 for s in sub {
73 entries.push(format!("{key}.{s}"));
74 }
75 }
76 }
77 }
78 }
79 Value::Array(arr) => {
80 for (i, item) in arr.iter().enumerate() {
81 let sub = canonicalize_value(item);
82 for s in sub {
83 entries.push(format!("[{i}].{s}"));
84 }
85 }
86 }
87 Value::String(s) => entries.push(s.clone()),
88 Value::Number(n) => entries.push(n.to_string()),
89 Value::Bool(b) => entries.push(b.to_string()),
90 Value::Null => entries.push("null".into()),
91 }
92 entries
93}
94
95pub fn sign_document(
97 document: Value,
98 algorithm: &str,
99 verification_method: &str,
100 signature_hex: &str,
101) -> SignedJsonLd {
102 SignedJsonLd {
103 document,
104 proof: Proof {
105 proof_type: algorithm.into(),
106 created: chrono::Utc::now(),
107 verification_method: verification_method.into(),
108 proof_purpose: "assertionMethod".into(),
109 proof_value: signature_hex.into(),
110 },
111 }
112}
113
114pub fn verify_canonical(signed: &SignedJsonLd) -> Vec<u8> {
116 canonicalize(&signed.document)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use serde_json::json;
123
124 #[test]
125 fn sign_adds_proof() {
126 let doc = json!({"name": "test", "value": 42});
127 let signed = sign_document(doc, "Ed25519Signature2020", "key-1", "abc123");
128 assert_eq!(signed.proof.proof_type, "Ed25519Signature2020");
129 assert_eq!(signed.proof.verification_method, "key-1");
130 assert_eq!(signed.proof.proof_value, "abc123");
131 }
132
133 #[test]
134 fn canonicalize_is_deterministic() {
135 let doc1 = json!({"b": 2, "a": 1});
136 let doc2 = json!({"a": 1, "b": 2});
137 assert_eq!(canonicalize(&doc1), canonicalize(&doc2));
138 }
139
140 #[test]
141 fn canonicalize_excludes_proof() {
142 let doc = json!({"data": "x", "proof": {"value": "sig"}});
143 let canonical = canonicalize(&doc);
144 let text = String::from_utf8(canonical).unwrap();
145 assert!(!text.contains("proof"));
146 assert!(text.contains("data:x"));
147 }
148
149 #[test]
150 fn canonicalize_nested() {
151 let doc = json!({"outer": {"inner": "val"}});
152 let canonical = canonicalize(&doc);
153 let text = String::from_utf8(canonical).unwrap();
154 assert!(text.contains("outer.inner:val"));
155 }
156
157 #[test]
158 fn canonicalize_array() {
159 let doc = json!({"items": ["a", "b"]});
160 let canonical = canonicalize(&doc);
161 let text = String::from_utf8(canonical).unwrap();
162 assert!(text.contains("[0].a"));
163 assert!(text.contains("[1].b"));
164 }
165
166 #[test]
167 fn signed_document_serializes() {
168 let doc = json!({"hello": "world"});
169 let signed = sign_document(doc, "Test", "k1", "sig");
170 let json_str = serde_json::to_string(&signed).unwrap();
171 assert!(json_str.contains("proof"));
172 assert!(json_str.contains("assertionMethod"));
173 }
174
175 #[test]
176 fn verify_canonical_matches() {
177 let doc = json!({"a": 1});
178 let signed = sign_document(doc, "Ed", "k", "s");
179 let canonical = verify_canonical(&signed);
180 assert!(!canonical.is_empty());
181 }
182
183 #[test]
184 fn different_documents_different_canonical() {
185 let doc1 = json!({"a": 1});
186 let doc2 = json!({"a": 2});
187 assert_ne!(canonicalize(&doc1), canonicalize(&doc2));
188 }
189
190 #[test]
191 fn proof_has_timestamp() {
192 let signed = sign_document(json!({}), "T", "k", "s");
193 assert!(signed.proof.created.timestamp() > 0);
194 }
195}