Rust and WASM did-method-plc tools and structures
1//! Builder pattern for creating did:plc identifiers
2
3use crate::crypto::SigningKey;
4use crate::did::Did;
5use crate::document::ServiceEndpoint;
6use crate::encoding::{base32_encode, dag_cbor_encode, sha256};
7use crate::error::{PlcError, Result};
8use crate::operations::{Operation, UnsignedOperation};
9use crate::validation::{
10 validate_also_known_as, validate_rotation_keys, validate_services,
11 validate_verification_methods,
12};
13use std::collections::HashMap;
14
15/// Builder for creating new did:plc identifiers
16///
17/// # Examples
18///
19/// ```
20/// use atproto_plc::{DidBuilder, SigningKey, ServiceEndpoint};
21///
22/// let rotation_key = SigningKey::generate_p256();
23/// let signing_key = SigningKey::generate_k256();
24///
25/// let (did, operation, keys) = DidBuilder::new()
26/// .add_rotation_key(rotation_key)
27/// .add_verification_method("atproto".into(), signing_key)
28/// .add_also_known_as("at://alice.example.com".into())
29/// .add_service(
30/// "atproto_pds".into(),
31/// ServiceEndpoint::new(
32/// "AtprotoPersonalDataServer".into(),
33/// "https://pds.example.com".into(),
34/// ),
35/// )
36/// .build()?;
37///
38/// println!("Created DID: {}", did);
39/// # Ok::<(), atproto_plc::PlcError>(())
40/// ```
41pub struct DidBuilder {
42 rotation_keys: Vec<SigningKey>,
43 verification_methods: HashMap<String, SigningKey>,
44 also_known_as: Vec<String>,
45 services: HashMap<String, ServiceEndpoint>,
46}
47
48impl DidBuilder {
49 /// Create a new DID builder
50 pub fn new() -> Self {
51 Self {
52 rotation_keys: Vec::new(),
53 verification_methods: HashMap::new(),
54 also_known_as: Vec::new(),
55 services: HashMap::new(),
56 }
57 }
58
59 /// Add a rotation key (1-5 required, no duplicates)
60 ///
61 /// Rotation keys are used to sign operations and can be used to recover
62 /// control of the DID within a 72-hour window.
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use atproto_plc::{DidBuilder, SigningKey};
68 ///
69 /// let key = SigningKey::generate_p256();
70 /// let builder = DidBuilder::new().add_rotation_key(key);
71 /// ```
72 pub fn add_rotation_key(mut self, key: SigningKey) -> Self {
73 self.rotation_keys.push(key);
74 self
75 }
76
77 /// Add a verification method (max 10)
78 ///
79 /// Verification methods are cryptographic keys used for authentication
80 /// and signing. In ATProto, these are typically used for signing posts
81 /// and other records.
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// use atproto_plc::{DidBuilder, SigningKey};
87 ///
88 /// let key = SigningKey::generate_k256();
89 /// let builder = DidBuilder::new()
90 /// .add_verification_method("atproto".into(), key);
91 /// ```
92 pub fn add_verification_method(mut self, name: String, key: SigningKey) -> Self {
93 self.verification_methods.insert(name, key);
94 self
95 }
96
97 /// Add an also-known-as URI
98 ///
99 /// Also-known-as URIs are alternate identifiers for the same entity.
100 /// In ATProto, this is typically the user's handle.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use atproto_plc::DidBuilder;
106 ///
107 /// let builder = DidBuilder::new()
108 /// .add_also_known_as("at://alice.bsky.social".into());
109 /// ```
110 pub fn add_also_known_as(mut self, uri: String) -> Self {
111 self.also_known_as.push(uri);
112 self
113 }
114
115 /// Add a service endpoint
116 ///
117 /// Services are endpoints that provide functionality for the DID.
118 /// In ATProto, this is typically the Personal Data Server (PDS).
119 ///
120 /// # Examples
121 ///
122 /// ```
123 /// use atproto_plc::{DidBuilder, ServiceEndpoint};
124 ///
125 /// let builder = DidBuilder::new()
126 /// .add_service(
127 /// "atproto_pds".into(),
128 /// ServiceEndpoint::new(
129 /// "AtprotoPersonalDataServer".into(),
130 /// "https://pds.example.com".into(),
131 /// ),
132 /// );
133 /// ```
134 pub fn add_service(mut self, name: String, endpoint: ServiceEndpoint) -> Self {
135 self.services.insert(name, endpoint);
136 self
137 }
138
139 /// Build and sign the genesis operation, returning the DID, operation, and keys
140 ///
141 /// This method:
142 /// 1. Validates all inputs
143 /// 2. Creates an unsigned genesis operation
144 /// 3. Signs it with the first rotation key
145 /// 4. Derives the DID from the signed operation's hash
146 /// 5. Returns the DID, signed operation, and all keys for safekeeping
147 ///
148 /// # Errors
149 ///
150 /// Returns errors if:
151 /// - No rotation keys provided
152 /// - Too many rotation keys (>5)
153 /// - Too many verification methods (>10)
154 /// - Invalid URIs or service endpoints
155 /// - Signing fails
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use atproto_plc::{DidBuilder, SigningKey};
161 ///
162 /// let rotation_key = SigningKey::generate_p256();
163 ///
164 /// let (did, operation, keys) = DidBuilder::new()
165 /// .add_rotation_key(rotation_key)
166 /// .build()?;
167 ///
168 /// assert!(did.as_str().starts_with("did:plc:"));
169 /// # Ok::<(), atproto_plc::PlcError>(())
170 /// ```
171 pub fn build(self) -> Result<(Did, Operation, BuilderKeys)> {
172 // Validate inputs
173 if self.rotation_keys.is_empty() {
174 return Err(PlcError::InvalidRotationKeys(
175 "At least one rotation key is required".to_string(),
176 ));
177 }
178
179 // Convert keys to did:key format for validation
180 let rotation_key_strings: Vec<String> =
181 self.rotation_keys.iter().map(|k| k.to_did_key()).collect();
182
183 let verification_method_strings: HashMap<String, String> = self
184 .verification_methods
185 .iter()
186 .map(|(name, key)| (name.clone(), key.to_did_key()))
187 .collect();
188
189 // Validate all fields
190 validate_rotation_keys(&rotation_key_strings)?;
191 validate_verification_methods(&verification_method_strings)?;
192 validate_also_known_as(&self.also_known_as)?;
193 validate_services(&self.services)?;
194
195 // Create unsigned genesis operation
196 let unsigned = UnsignedOperation::PlcOperation {
197 rotation_keys: rotation_key_strings,
198 verification_methods: verification_method_strings,
199 also_known_as: self.also_known_as,
200 services: self.services,
201 prev: None, // Genesis has no previous operation
202 };
203
204 // Sign with the first rotation key
205 let signed = unsigned.sign(&self.rotation_keys[0])?;
206
207 // Derive DID from the signed operation
208 let did = Self::derive_did(&signed)?;
209
210 // Collect all keys to return
211 let keys = BuilderKeys {
212 rotation_keys: self.rotation_keys,
213 verification_methods: self.verification_methods,
214 };
215
216 Ok((did, signed, keys))
217 }
218
219 /// Derive a DID from a signed genesis operation
220 ///
221 /// The DID is derived by:
222 /// 1. Computing the CID of the signed operation
223 /// 2. Taking the SHA-256 hash of the operation
224 /// 3. Base32-encoding the hash
225 /// 4. Taking the first 24 characters
226 fn derive_did(operation: &Operation) -> Result<Did> {
227 // Get the CID of the operation
228 let _cid = operation.cid()?;
229
230 // The DID is derived from the CID by taking the hash portion
231 // For simplicity, we'll hash the entire serialized operation
232 let serialized =
233 dag_cbor_encode(operation).map_err(|e| PlcError::DagCborError(e.to_string()))?;
234
235 let hash = sha256(&serialized);
236 let encoded = base32_encode(&hash);
237
238 // Take first 24 characters for the DID identifier
239 let identifier = &encoded[..24.min(encoded.len())];
240
241 Did::from_identifier(identifier)
242 }
243}
244
245impl Default for DidBuilder {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251/// Keys returned from the builder
252///
253/// These should be stored securely by the application.
254/// The rotation keys can be used to update or recover the DID.
255/// The verification methods are used for signing application data.
256pub struct BuilderKeys {
257 /// Rotation keys (private keys)
258 pub rotation_keys: Vec<SigningKey>,
259
260 /// Verification method keys (private keys)
261 pub verification_methods: HashMap<String, SigningKey>,
262}
263
264impl BuilderKeys {
265 /// Get a rotation key by index
266 pub fn rotation_key(&self, index: usize) -> Option<&SigningKey> {
267 self.rotation_keys.get(index)
268 }
269
270 /// Get a verification method key by name
271 pub fn verification_method(&self, name: &str) -> Option<&SigningKey> {
272 self.verification_methods.get(name)
273 }
274
275 /// Get the primary rotation key (first one)
276 pub fn primary_rotation_key(&self) -> Option<&SigningKey> {
277 self.rotation_key(0)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_builder_basic() {
287 let rotation_key = SigningKey::generate_p256();
288
289 let (did, operation, keys) = DidBuilder::new()
290 .add_rotation_key(rotation_key)
291 .build()
292 .unwrap();
293
294 assert!(did.as_str().starts_with("did:plc:"));
295 assert!(operation.is_genesis());
296 assert_eq!(keys.rotation_keys.len(), 1);
297 }
298
299 #[test]
300 fn test_builder_with_verification_methods() {
301 let rotation_key = SigningKey::generate_p256();
302 let signing_key = SigningKey::generate_k256();
303
304 let (did, _, keys) = DidBuilder::new()
305 .add_rotation_key(rotation_key)
306 .add_verification_method("atproto".into(), signing_key)
307 .build()
308 .unwrap();
309
310 assert!(did.as_str().starts_with("did:plc:"));
311 assert_eq!(keys.verification_methods.len(), 1);
312 assert!(keys.verification_method("atproto").is_some());
313 }
314
315 #[test]
316 fn test_builder_with_services() {
317 let rotation_key = SigningKey::generate_p256();
318
319 let (did, _, _) = DidBuilder::new()
320 .add_rotation_key(rotation_key)
321 .add_service(
322 "atproto_pds".into(),
323 ServiceEndpoint::new(
324 "AtprotoPersonalDataServer".into(),
325 "https://pds.example.com".into(),
326 ),
327 )
328 .build()
329 .unwrap();
330
331 assert!(did.as_str().starts_with("did:plc:"));
332 }
333
334 #[test]
335 fn test_builder_no_rotation_keys() {
336 let result = DidBuilder::new().build();
337 assert!(result.is_err());
338 }
339
340 #[test]
341 fn test_builder_too_many_rotation_keys() {
342 let mut builder = DidBuilder::new();
343
344 for _ in 0..6 {
345 builder = builder.add_rotation_key(SigningKey::generate_p256());
346 }
347
348 assert!(builder.build().is_err());
349 }
350
351 #[test]
352 fn test_builder_keys_access() {
353 let rotation_key = SigningKey::generate_p256();
354 let signing_key = SigningKey::generate_k256();
355
356 let (_, _, keys) = DidBuilder::new()
357 .add_rotation_key(rotation_key)
358 .add_verification_method("atproto".into(), signing_key)
359 .build()
360 .unwrap();
361
362 assert!(keys.primary_rotation_key().is_some());
363 assert!(keys.verification_method("atproto").is_some());
364 assert!(keys.verification_method("nonexistent").is_none());
365 }
366}