forked from
smokesignal.events/atproto-plc
Rust and WASM did-method-plc tools and structures
1//! DID document structures and parsing
2
3use crate::did::Did;
4use crate::error::{PlcError, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Maximum number of verification methods allowed
9pub const MAX_VERIFICATION_METHODS: usize = 10;
10
11/// Internal PLC state format
12///
13/// This represents the internal state of a did:plc document as stored
14/// in the PLC directory.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct PlcState {
17 /// Rotation keys (1-5 did:key strings)
18 #[serde(rename = "rotationKeys")]
19 pub rotation_keys: Vec<String>,
20
21 /// Verification methods (max 10 entries)
22 #[serde(rename = "verificationMethods")]
23 pub verification_methods: HashMap<String, String>,
24
25 /// Also-known-as URIs
26 #[serde(rename = "alsoKnownAs")]
27 pub also_known_as: Vec<String>,
28
29 /// Service endpoints
30 pub services: HashMap<String, ServiceEndpoint>,
31}
32
33impl PlcState {
34 /// Create a new empty PLC state
35 pub fn new() -> Self {
36 Self {
37 rotation_keys: Vec::new(),
38 verification_methods: HashMap::new(),
39 also_known_as: Vec::new(),
40 services: HashMap::new(),
41 }
42 }
43
44 /// Validate this PLC state according to the specification
45 ///
46 /// # Errors
47 ///
48 /// Returns errors if:
49 /// - Rotation keys count is not 1-5
50 /// - Rotation keys contain duplicates
51 /// - Verification methods exceed 10 entries
52 pub fn validate(&self) -> Result<()> {
53 // Validate rotation keys (1-5 required, no duplicates)
54 if self.rotation_keys.is_empty() {
55 return Err(PlcError::InvalidRotationKeys(
56 "At least one rotation key is required".to_string(),
57 ));
58 }
59
60 if self.rotation_keys.len() > 5 {
61 return Err(PlcError::TooManyEntries {
62 field: "rotation_keys".to_string(),
63 max: 5,
64 actual: self.rotation_keys.len(),
65 });
66 }
67
68 // Check for duplicate rotation keys
69 let mut seen = std::collections::HashSet::new();
70 for key in &self.rotation_keys {
71 if !seen.insert(key) {
72 return Err(PlcError::DuplicateEntry {
73 field: "rotation_keys".to_string(),
74 value: key.clone(),
75 });
76 }
77 }
78
79 // Validate all rotation keys are valid did:key format
80 for key in &self.rotation_keys {
81 if !key.starts_with("did:key:") {
82 return Err(PlcError::InvalidRotationKeys(format!(
83 "Rotation key must be in did:key format: {}",
84 key
85 )));
86 }
87 }
88
89 // Validate verification methods (max 10)
90 if self.verification_methods.len() > MAX_VERIFICATION_METHODS {
91 return Err(PlcError::TooManyEntries {
92 field: "verification_methods".to_string(),
93 max: MAX_VERIFICATION_METHODS,
94 actual: self.verification_methods.len(),
95 });
96 }
97
98 // Validate all verification methods are valid did:key format
99 for (name, key) in &self.verification_methods {
100 if !key.starts_with("did:key:") {
101 return Err(PlcError::InvalidVerificationMethods(format!(
102 "Verification method '{}' must be in did:key format: {}",
103 name, key
104 )));
105 }
106 }
107
108 Ok(())
109 }
110
111 /// Convert this PLC state to a W3C DID document
112 pub fn to_did_document(&self, did: &Did) -> DidDocument {
113 DidDocument::from_plc_state(did.clone(), self.clone())
114 }
115}
116
117impl Default for PlcState {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123/// Service endpoint definition
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct ServiceEndpoint {
126 /// Service type (e.g., "AtprotoPersonalDataServer")
127 #[serde(rename = "type")]
128 pub service_type: String,
129
130 /// Service endpoint URL
131 pub endpoint: String,
132}
133
134impl ServiceEndpoint {
135 /// Create a new service endpoint
136 pub fn new(service_type: String, endpoint: String) -> Self {
137 Self {
138 service_type,
139 endpoint,
140 }
141 }
142
143 /// Validate this service endpoint
144 pub fn validate(&self) -> Result<()> {
145 if self.service_type.is_empty() {
146 return Err(PlcError::InvalidService(
147 "Service type cannot be empty".to_string(),
148 ));
149 }
150
151 if self.endpoint.is_empty() {
152 return Err(PlcError::InvalidService(
153 "Service endpoint cannot be empty".to_string(),
154 ));
155 }
156
157 Ok(())
158 }
159}
160
161/// W3C DID Document format
162///
163/// This represents a DID document in the W3C standard format.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct DidDocument {
166 /// The DID this document describes
167 pub id: Did,
168
169 /// JSON-LD context
170 #[serde(rename = "@context")]
171 pub context: Vec<String>,
172
173 /// Verification methods
174 #[serde(rename = "verificationMethod")]
175 pub verification_method: Vec<VerificationMethod>,
176
177 /// Also-known-as URIs
178 #[serde(rename = "alsoKnownAs", skip_serializing_if = "Vec::is_empty", default)]
179 pub also_known_as: Vec<String>,
180
181 /// Services
182 #[serde(skip_serializing_if = "Vec::is_empty", default)]
183 pub service: Vec<Service>,
184}
185
186impl DidDocument {
187 /// Create a DID document from PLC state
188 pub fn from_plc_state(did: Did, state: PlcState) -> Self {
189 let mut verification_methods = Vec::new();
190
191 // Add verification methods
192 for (id, controller) in &state.verification_methods {
193 verification_methods.push(VerificationMethod {
194 id: format!("{}#{}", did, id),
195 method_type: "Multikey".to_string(),
196 controller: did.to_string(),
197 public_key_multibase: controller.clone(),
198 });
199 }
200
201 // Add services
202 let services: Vec<Service> = state
203 .services
204 .iter()
205 .map(|(id, endpoint)| Service {
206 id: format!("{}#{}", did, id),
207 service_type: endpoint.service_type.clone(),
208 service_endpoint: endpoint.endpoint.clone(),
209 })
210 .collect();
211
212 Self {
213 id: did,
214 context: vec![
215 "https://www.w3.org/ns/did/v1".to_string(),
216 "https://w3id.org/security/multikey/v1".to_string(),
217 ],
218 verification_method: verification_methods,
219 also_known_as: state.also_known_as.clone(),
220 service: services,
221 }
222 }
223
224 /// Validate this DID document
225 pub fn validate(&self) -> Result<()> {
226 // Convert to PLC state and validate
227 let plc_state = self.to_plc_state()?;
228 plc_state.validate()
229 }
230
231 /// Convert this DID document to PLC state
232 pub fn to_plc_state(&self) -> Result<PlcState> {
233 let mut verification_methods = HashMap::new();
234
235 for vm in &self.verification_method {
236 // Extract the fragment ID (after '#')
237 let id = vm
238 .id
239 .rsplit('#')
240 .next()
241 .ok_or_else(|| {
242 PlcError::InvalidVerificationMethods(format!(
243 "Invalid verification method ID: {}",
244 vm.id
245 ))
246 })?
247 .to_string();
248
249 verification_methods.insert(id, vm.public_key_multibase.clone());
250 }
251
252 let mut services = HashMap::new();
253 for svc in &self.service {
254 let id = svc
255 .id
256 .rsplit('#')
257 .next()
258 .ok_or_else(|| PlcError::InvalidService(format!("Invalid service ID: {}", svc.id)))?
259 .to_string();
260
261 services.insert(
262 id,
263 ServiceEndpoint {
264 service_type: svc.service_type.clone(),
265 endpoint: svc.service_endpoint.clone(),
266 },
267 );
268 }
269
270 Ok(PlcState {
271 rotation_keys: Vec::new(), // Not stored in DID document
272 verification_methods,
273 also_known_as: self.also_known_as.clone(),
274 services,
275 })
276 }
277}
278
279/// Verification method in W3C format
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct VerificationMethod {
282 /// Verification method ID (e.g., "did:plc:xyz#atproto")
283 pub id: String,
284
285 /// Method type (e.g., "Multikey")
286 #[serde(rename = "type")]
287 pub method_type: String,
288
289 /// Controller DID
290 pub controller: String,
291
292 /// Public key in multibase format
293 #[serde(rename = "publicKeyMultibase")]
294 pub public_key_multibase: String,
295}
296
297/// Service in W3C format
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299pub struct Service {
300 /// Service ID (e.g., "did:plc:xyz#atproto_pds")
301 pub id: String,
302
303 /// Service type
304 #[serde(rename = "type")]
305 pub service_type: String,
306
307 /// Service endpoint URL
308 #[serde(rename = "serviceEndpoint")]
309 pub service_endpoint: String,
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_plc_state_validation() {
318 let mut state = PlcState::new();
319
320 // Empty state should fail (no rotation keys)
321 assert!(state.validate().is_err());
322
323 // Add a rotation key
324 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string());
325 assert!(state.validate().is_ok());
326
327 // Add duplicate rotation key
328 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string());
329 assert!(state.validate().is_err());
330 }
331
332 #[test]
333 fn test_service_endpoint() {
334 let endpoint = ServiceEndpoint::new(
335 "AtprotoPersonalDataServer".to_string(),
336 "https://pds.example.com".to_string(),
337 );
338 assert!(endpoint.validate().is_ok());
339
340 let empty_type = ServiceEndpoint::new(String::new(), "https://example.com".to_string());
341 assert!(empty_type.validate().is_err());
342 }
343
344 #[test]
345 fn test_did_document_conversion() {
346 let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap();
347 let mut state = PlcState::new();
348 state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string());
349 state.verification_methods.insert(
350 "atproto".to_string(),
351 "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169".to_string(),
352 );
353
354 let doc = state.to_did_document(&did);
355 assert_eq!(doc.id, did);
356 assert_eq!(doc.verification_method.len(), 1);
357 assert_eq!(doc.verification_method[0].id, "did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto");
358 }
359}