Skip to main content

tenzro_identity/
document.rs

1//! W3C DID Document types
2//!
3//! Defines the DID Document structure following the W3C DID Core specification,
4//! used as the external representation format for Tenzro identities.
5
6use serde::{Deserialize, Serialize};
7
8/// A W3C DID Document
9///
10/// See: <https://www.w3.org/TR/did-core/>
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct DidDocument {
14    /// JSON-LD context
15    #[serde(rename = "@context")]
16    pub context: Vec<String>,
17    /// The DID subject
18    pub id: String,
19    /// Optional controller DID
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub controller: Option<String>,
22    /// Verification methods (public keys)
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub verification_method: Vec<VerificationMethod>,
25    /// Authentication verification relationships
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub authentication: Vec<String>,
28    /// Assertion method verification relationships
29    #[serde(default, skip_serializing_if = "Vec::is_empty")]
30    pub assertion_method: Vec<String>,
31    /// Key agreement verification relationships
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub key_agreement: Vec<String>,
34    /// Service endpoints
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub service: Vec<DidService>,
37}
38
39impl DidDocument {
40    /// Creates a new DID Document with default Tenzro context
41    pub fn new(id: impl Into<String>) -> Self {
42        Self {
43            context: vec![
44                "https://www.w3.org/ns/did/v1".to_string(),
45                "https://w3id.org/security/suites/ed25519-2020/v1".to_string(),
46                "https://ns.tenzro.network/v1".to_string(),
47            ],
48            id: id.into(),
49            controller: None,
50            verification_method: Vec::new(),
51            authentication: Vec::new(),
52            assertion_method: Vec::new(),
53            key_agreement: Vec::new(),
54            service: Vec::new(),
55        }
56    }
57
58    /// Sets the controller
59    pub fn with_controller(mut self, controller: impl Into<String>) -> Self {
60        self.controller = Some(controller.into());
61        self
62    }
63
64    /// Adds a verification method
65    pub fn add_verification_method(&mut self, method: VerificationMethod) {
66        let method_id = method.id.clone();
67        for purpose in &method.purposes {
68            match purpose {
69                VerificationPurpose::Authentication => {
70                    self.authentication.push(method_id.clone());
71                }
72                VerificationPurpose::AssertionMethod => {
73                    self.assertion_method.push(method_id.clone());
74                }
75                VerificationPurpose::KeyAgreement => {
76                    self.key_agreement.push(method_id.clone());
77                }
78            }
79        }
80        self.verification_method.push(method);
81    }
82
83    /// Adds a service endpoint
84    pub fn add_service(&mut self, service: DidService) {
85        self.service.push(service);
86    }
87}
88
89/// A verification method in a DID Document
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct VerificationMethod {
92    /// Verification method ID (e.g., "did:tenzro:human:abc#key-1")
93    pub id: String,
94    /// The DID this method belongs to
95    pub controller: String,
96    /// Method type (e.g., "Ed25519VerificationKey2020")
97    #[serde(rename = "type")]
98    pub method_type: String,
99    /// Public key in multibase encoding
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub public_key_multibase: Option<String>,
102    /// Public key in JWK format
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub public_key_jwk: Option<serde_json::Value>,
105    /// Purposes this key serves (not part of W3C spec, used internally)
106    #[serde(skip)]
107    pub purposes: Vec<VerificationPurpose>,
108}
109
110/// Purpose of a verification method
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum VerificationPurpose {
113    /// Used for authentication
114    Authentication,
115    /// Used for making assertions (signing credentials)
116    AssertionMethod,
117    /// Used for key agreement (encryption)
118    KeyAgreement,
119}
120
121/// A service endpoint in a DID Document
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct DidService {
124    /// Service ID
125    pub id: String,
126    /// Service type
127    #[serde(rename = "type")]
128    pub service_type: String,
129    /// Service endpoint URL
130    #[serde(rename = "serviceEndpoint")]
131    pub service_endpoint: String,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_create_did_document() {
140        let doc = DidDocument::new("did:tenzro:human:alice-uuid");
141        assert_eq!(doc.id, "did:tenzro:human:alice-uuid");
142        assert!(doc.context.contains(&"https://www.w3.org/ns/did/v1".to_string()));
143        assert!(doc.controller.is_none());
144    }
145
146    #[test]
147    fn test_did_document_with_controller() {
148        let doc = DidDocument::new("did:tenzro:machine:ctrl:bot1")
149            .with_controller("did:tenzro:human:ctrl");
150        assert_eq!(doc.controller, Some("did:tenzro:human:ctrl".to_string()));
151    }
152
153    #[test]
154    fn test_add_verification_method() {
155        let mut doc = DidDocument::new("did:tenzro:human:alice");
156
157        let method = VerificationMethod {
158            id: "did:tenzro:human:alice#key-1".to_string(),
159            controller: "did:tenzro:human:alice".to_string(),
160            method_type: "Ed25519VerificationKey2020".to_string(),
161            public_key_multibase: Some("z6Mk...".to_string()),
162            public_key_jwk: None,
163            purposes: vec![
164                VerificationPurpose::Authentication,
165                VerificationPurpose::AssertionMethod,
166            ],
167        };
168
169        doc.add_verification_method(method);
170
171        assert_eq!(doc.verification_method.len(), 1);
172        assert_eq!(doc.authentication.len(), 1);
173        assert_eq!(doc.assertion_method.len(), 1);
174        assert_eq!(doc.key_agreement.len(), 0);
175    }
176
177    #[test]
178    fn test_add_service() {
179        let mut doc = DidDocument::new("did:tenzro:human:alice");
180
181        doc.add_service(DidService {
182            id: "did:tenzro:human:alice#inference".to_string(),
183            service_type: "InferenceEndpoint".to_string(),
184            service_endpoint: "https://node.tenzro.com/inference".to_string(),
185        });
186
187        assert_eq!(doc.service.len(), 1);
188        assert_eq!(doc.service[0].service_type, "InferenceEndpoint");
189    }
190
191    #[test]
192    fn test_serialize_did_document() {
193        let doc = DidDocument::new("did:tenzro:human:alice");
194        let json = serde_json::to_string_pretty(&doc).unwrap();
195        assert!(json.contains("@context"));
196        assert!(json.contains("did:tenzro:human:alice"));
197    }
198
199    #[test]
200    fn test_w3c_did_document_json_ld_compliance() {
201        let mut doc = DidDocument::new("did:tenzro:human:alice-uuid");
202
203        // Add a verification method
204        doc.add_verification_method(VerificationMethod {
205            id: "did:tenzro:human:alice-uuid#key-1".to_string(),
206            controller: "did:tenzro:human:alice-uuid".to_string(),
207            method_type: "Ed25519VerificationKey2020".to_string(),
208            public_key_multibase: Some("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string()),
209            public_key_jwk: None,
210            purposes: vec![VerificationPurpose::Authentication],
211        });
212
213        // Serialize to JSON
214        let json = serde_json::to_string_pretty(&doc).unwrap();
215
216        // Verify W3C required fields are present
217        assert!(json.contains("@context"), "Missing @context field");
218        assert!(json.contains("https://www.w3.org/ns/did/v1"), "Missing W3C DID context");
219        assert!(json.contains("\"id\""), "Missing id field");
220        assert!(json.contains("verificationMethod"), "Missing verificationMethod");
221        assert!(json.contains("authentication"), "Missing authentication relationship");
222
223        // Deserialize and verify
224        let parsed: DidDocument = serde_json::from_str(&json).unwrap();
225        assert_eq!(parsed.id, "did:tenzro:human:alice-uuid");
226        assert_eq!(parsed.context.len(), 3);
227        assert_eq!(parsed.verification_method.len(), 1);
228        assert_eq!(parsed.authentication.len(), 1);
229    }
230
231    #[test]
232    fn test_did_document_roundtrip_with_all_fields() {
233        let mut doc = DidDocument::new("did:tenzro:machine:ctrl:bot")
234            .with_controller("did:tenzro:human:ctrl");
235
236        doc.add_verification_method(VerificationMethod {
237            id: "did:tenzro:machine:ctrl:bot#key-1".to_string(),
238            controller: "did:tenzro:machine:ctrl:bot".to_string(),
239            method_type: "Ed25519VerificationKey2020".to_string(),
240            public_key_multibase: Some("z6Mk...".to_string()),
241            public_key_jwk: None,
242            purposes: vec![
243                VerificationPurpose::Authentication,
244                VerificationPurpose::AssertionMethod,
245                VerificationPurpose::KeyAgreement,
246            ],
247        });
248
249        doc.add_service(DidService {
250            id: "did:tenzro:machine:ctrl:bot#inference".to_string(),
251            service_type: "InferenceEndpoint".to_string(),
252            service_endpoint: "https://node.example.com/inference".to_string(),
253        });
254
255        // Serialize
256        let json = serde_json::to_string(&doc).unwrap();
257
258        // Deserialize
259        let parsed: DidDocument = serde_json::from_str(&json).unwrap();
260
261        // Verify all fields match
262        assert_eq!(parsed.id, doc.id);
263        assert_eq!(parsed.controller, doc.controller);
264        assert_eq!(parsed.verification_method.len(), 1);
265        assert_eq!(parsed.authentication.len(), 1);
266        assert_eq!(parsed.assertion_method.len(), 1);
267        assert_eq!(parsed.key_agreement.len(), 1);
268        assert_eq!(parsed.service.len(), 1);
269    }
270}