Skip to main content

confium_privacy/
jsonld_signing.rs

1//! JSON-LD document signing.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// A signed JSON-LD document.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SignedJsonLd {
9    /// The original document content.
10    pub document: Value,
11    /// The proof (signature) node.
12    pub proof: Proof,
13}
14
15/// A Linked Data proof.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Proof {
18    /// Proof type (e.g., "Ed25519Signature2020").
19    #[serde(rename = "type")]
20    pub proof_type: String,
21    /// When the proof was created.
22    pub created: chrono::DateTime<chrono::Utc>,
23    /// Verification method (key ID).
24    pub verification_method: String,
25    /// Proof purpose (e.g., "assertionMethod").
26    pub proof_purpose: String,
27    /// Signature value (hex or base58).
28    pub proof_value: String,
29}
30
31/// Canonicalize a JSON-LD document for signing (URDNA2015 simplified).
32/// Produces a deterministic byte representation.
33pub fn canonicalize(document: &Value) -> Vec<u8> {
34    // Simplified canonicalization: sort keys recursively, serialize
35    // Full implementation would use JSON-LD framing + URDNA2015.
36    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
95/// Create a signed JSON-LD document.
96pub 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
114/// Verify a signed document by recomputing the canonical form.
115pub 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}