forked from
smokesignal.events/atproto-plc
Rust and WASM did-method-plc tools and structures
1//! Operation types for did:plc (genesis, update, tombstone)
2
3use crate::crypto::{SigningKey, VerifyingKey};
4use crate::document::ServiceEndpoint;
5use crate::encoding::{base64url_decode, compute_cid, dag_cbor_encode};
6use crate::error::{PlcError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Represents a PLC operation (genesis, update, or tombstone)
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(tag = "type")]
13pub enum Operation {
14 /// Standard PLC operation (genesis or update)
15 #[serde(rename = "plc_operation")]
16 PlcOperation {
17 /// Rotation keys (1-5 did:key strings)
18 #[serde(rename = "rotationKeys")]
19 rotation_keys: Vec<String>,
20
21 /// Verification methods (max 10 entries)
22 #[serde(rename = "verificationMethods")]
23 verification_methods: HashMap<String, String>,
24
25 /// Also-known-as URIs
26 #[serde(rename = "alsoKnownAs")]
27 also_known_as: Vec<String>,
28
29 /// Service endpoints
30 services: HashMap<String, ServiceEndpoint>,
31
32 /// Previous operation CID (null for genesis)
33 prev: Option<String>,
34
35 /// Base64url-encoded signature
36 sig: String,
37 },
38
39 /// Tombstone operation (marks DID as deleted)
40 #[serde(rename = "plc_tombstone")]
41 PlcTombstone {
42 /// Previous operation CID (never null for tombstone)
43 prev: String,
44
45 /// Base64url-encoded signature
46 sig: String,
47 },
48
49 /// Legacy create operation (for backwards compatibility)
50 #[serde(rename = "create")]
51 LegacyCreate {
52 /// Signing key (did:key format)
53 #[serde(rename = "signingKey")]
54 signing_key: String,
55
56 /// Recovery key (did:key format)
57 #[serde(rename = "recoveryKey")]
58 recovery_key: String,
59
60 /// Handle (e.g., "alice.bsky.social")
61 handle: String,
62
63 /// Service endpoint URL
64 service: String,
65
66 /// Previous operation CID
67 prev: Option<String>,
68
69 /// Base64url-encoded signature
70 sig: String,
71 },
72}
73
74impl Operation {
75 /// Create a new unsigned genesis operation
76 pub fn new_genesis(
77 rotation_keys: Vec<String>,
78 verification_methods: HashMap<String, String>,
79 also_known_as: Vec<String>,
80 services: HashMap<String, ServiceEndpoint>,
81 ) -> UnsignedOperation {
82 UnsignedOperation::PlcOperation {
83 rotation_keys,
84 verification_methods,
85 also_known_as,
86 services,
87 prev: None,
88 }
89 }
90
91 /// Create a new unsigned update operation
92 pub fn new_update(
93 rotation_keys: Vec<String>,
94 verification_methods: HashMap<String, String>,
95 also_known_as: Vec<String>,
96 services: HashMap<String, ServiceEndpoint>,
97 prev: String,
98 ) -> UnsignedOperation {
99 UnsignedOperation::PlcOperation {
100 rotation_keys,
101 verification_methods,
102 also_known_as,
103 services,
104 prev: Some(prev),
105 }
106 }
107
108 /// Create a new unsigned tombstone operation
109 pub fn new_tombstone(prev: String) -> UnsignedOperation {
110 UnsignedOperation::PlcTombstone { prev }
111 }
112
113 /// Get the previous operation CID, if any
114 pub fn prev(&self) -> Option<&str> {
115 match self {
116 Operation::PlcOperation { prev, .. } => prev.as_deref(),
117 Operation::PlcTombstone { prev, .. } => Some(prev),
118 Operation::LegacyCreate { prev, .. } => prev.as_deref(),
119 }
120 }
121
122 /// Get the signature as a base64url string
123 pub fn signature(&self) -> &str {
124 match self {
125 Operation::PlcOperation { sig, .. } => sig,
126 Operation::PlcTombstone { sig, .. } => sig,
127 Operation::LegacyCreate { sig, .. } => sig,
128 }
129 }
130
131 /// Check if this is a genesis operation (prev is None)
132 pub fn is_genesis(&self) -> bool {
133 self.prev().is_none()
134 }
135
136 /// Compute the CID of this operation
137 ///
138 /// # Errors
139 ///
140 /// Returns an error if DAG-CBOR encoding fails
141 pub fn cid(&self) -> Result<String> {
142 let encoded = dag_cbor_encode(self)?;
143 compute_cid(&encoded)
144 }
145
146 /// Verify the signature on this operation using the provided rotation keys
147 ///
148 /// # Errors
149 ///
150 /// Returns `PlcError::SignatureVerificationFailed` if verification fails
151 pub fn verify(&self, rotation_keys: &[VerifyingKey]) -> Result<()> {
152 if rotation_keys.is_empty() {
153 return Err(PlcError::InvalidRotationKeys(
154 "At least one rotation key is required for verification".to_string(),
155 ));
156 }
157
158 // Get the unsigned operation data
159 let unsigned_data = self.unsigned_data()?;
160
161 // Decode signature
162 let signature = base64url_decode(self.signature())?;
163
164 // Try to verify with each rotation key
165 let mut last_error = None;
166 for key in rotation_keys {
167 match key.verify(&unsigned_data, &signature) {
168 Ok(_) => return Ok(()), // Success!
169 Err(e) => last_error = Some(e),
170 }
171 }
172
173 // If we get here, none of the keys verified the signature
174 Err(last_error.unwrap_or(PlcError::SignatureVerificationFailed))
175 }
176
177 /// Get the unsigned data that was signed
178 fn unsigned_data(&self) -> Result<Vec<u8>> {
179 let unsigned = match self {
180 Operation::PlcOperation {
181 rotation_keys,
182 verification_methods,
183 also_known_as,
184 services,
185 prev,
186 ..
187 } => UnsignedOperation::PlcOperation {
188 rotation_keys: rotation_keys.clone(),
189 verification_methods: verification_methods.clone(),
190 also_known_as: also_known_as.clone(),
191 services: services.clone(),
192 prev: prev.clone(),
193 },
194 Operation::PlcTombstone { prev, .. } => UnsignedOperation::PlcTombstone {
195 prev: prev.clone(),
196 },
197 Operation::LegacyCreate {
198 signing_key,
199 recovery_key,
200 handle,
201 service,
202 prev,
203 ..
204 } => UnsignedOperation::LegacyCreate {
205 signing_key: signing_key.clone(),
206 recovery_key: recovery_key.clone(),
207 handle: handle.clone(),
208 service: service.clone(),
209 prev: prev.clone(),
210 },
211 };
212
213 dag_cbor_encode(&unsigned)
214 }
215
216 /// Get the rotation keys from this operation, if any
217 pub fn rotation_keys(&self) -> Option<&[String]> {
218 match self {
219 Operation::PlcOperation { rotation_keys, .. } => Some(rotation_keys),
220 _ => None,
221 }
222 }
223}
224
225/// An unsigned operation that needs to be signed
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(tag = "type")]
228pub enum UnsignedOperation {
229 /// Standard PLC operation (genesis or update)
230 #[serde(rename = "plc_operation")]
231 PlcOperation {
232 /// Rotation keys for signing future operations
233 #[serde(rename = "rotationKeys")]
234 rotation_keys: Vec<String>,
235
236 /// Verification methods for authentication
237 #[serde(rename = "verificationMethods")]
238 verification_methods: HashMap<String, String>,
239
240 /// Also-known-as URIs (aliases)
241 #[serde(rename = "alsoKnownAs")]
242 also_known_as: Vec<String>,
243
244 /// Service endpoints
245 services: HashMap<String, ServiceEndpoint>,
246
247 /// CID of previous operation (None for genesis)
248 prev: Option<String>,
249 },
250
251 /// Tombstone operation
252 #[serde(rename = "plc_tombstone")]
253 PlcTombstone {
254 /// CID of previous operation
255 prev: String,
256 },
257
258 /// Legacy create operation
259 #[serde(rename = "create")]
260 LegacyCreate {
261 /// Signing key for the DID
262 #[serde(rename = "signingKey")]
263 signing_key: String,
264
265 /// Recovery key for the DID
266 #[serde(rename = "recoveryKey")]
267 recovery_key: String,
268
269 /// Handle for the DID
270 handle: String,
271
272 /// Service endpoint
273 service: String,
274
275 /// CID of previous operation (None for genesis)
276 prev: Option<String>,
277 },
278}
279
280impl UnsignedOperation {
281 /// Sign this operation with the provided signing key
282 ///
283 /// # Errors
284 ///
285 /// Returns an error if signing or encoding fails
286 pub fn sign(self, key: &SigningKey) -> Result<Operation> {
287 // Serialize to DAG-CBOR
288 let data = dag_cbor_encode(&self)?;
289
290 // Sign the data
291 let signature = key.sign_base64url(&data)?;
292
293 // Create the signed operation
294 let operation = match self {
295 UnsignedOperation::PlcOperation {
296 rotation_keys,
297 verification_methods,
298 also_known_as,
299 services,
300 prev,
301 } => Operation::PlcOperation {
302 rotation_keys,
303 verification_methods,
304 also_known_as,
305 services,
306 prev,
307 sig: signature,
308 },
309 UnsignedOperation::PlcTombstone { prev } => Operation::PlcTombstone {
310 prev,
311 sig: signature,
312 },
313 UnsignedOperation::LegacyCreate {
314 signing_key,
315 recovery_key,
316 handle,
317 service,
318 prev,
319 } => Operation::LegacyCreate {
320 signing_key,
321 recovery_key,
322 handle,
323 service,
324 prev,
325 sig: signature,
326 },
327 };
328
329 Ok(operation)
330 }
331
332 /// Compute the CID of this unsigned operation
333 ///
334 /// This is used to derive the DID from the genesis operation
335 pub fn cid(&self) -> Result<String> {
336 let encoded = dag_cbor_encode(self)?;
337 compute_cid(&encoded)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::crypto::SigningKey;
345
346 #[test]
347 fn test_genesis_operation() {
348 let key = SigningKey::generate_p256();
349 let did_key = key.to_did_key();
350
351 let unsigned = Operation::new_genesis(
352 vec![did_key.clone()],
353 HashMap::new(),
354 vec![],
355 HashMap::new(),
356 );
357
358 let signed = unsigned.sign(&key).unwrap();
359 assert!(signed.is_genesis());
360 assert_eq!(signed.prev(), None);
361 }
362
363 #[test]
364 fn test_update_operation() {
365 let key = SigningKey::generate_p256();
366 let did_key = key.to_did_key();
367
368 let unsigned = Operation::new_update(
369 vec![did_key],
370 HashMap::new(),
371 vec![],
372 HashMap::new(),
373 "bafyreib2rxk3rybk3aobmv5msrxগত7h4b4kfzxx4wxltyqu7e7vgq".to_string(),
374 );
375
376 let signed = unsigned.sign(&key).unwrap();
377 assert!(!signed.is_genesis());
378 assert!(signed.prev().is_some());
379 }
380
381 #[test]
382 fn test_tombstone_operation() {
383 let key = SigningKey::generate_p256();
384
385 let unsigned = Operation::new_tombstone(
386 "bafyreib2rxk3rybk3aobmv5msrxhgt7h4b4kfzxx4wxltyqu7e7vgq".to_string(),
387 );
388
389 let signed = unsigned.sign(&key).unwrap();
390 assert!(!signed.is_genesis());
391 assert!(signed.prev().is_some());
392 }
393
394 #[test]
395 fn test_sign_and_verify() {
396 let key = SigningKey::generate_p256();
397 let did_key = key.to_did_key();
398
399 let unsigned = Operation::new_genesis(
400 vec![did_key.clone()],
401 HashMap::new(),
402 vec![],
403 HashMap::new(),
404 );
405
406 let signed = unsigned.sign(&key).unwrap();
407
408 // Parse the rotation key to get verifying key
409 let verifying_key = VerifyingKey::from_did_key(&did_key).unwrap();
410
411 // Verify should succeed
412 assert!(signed.verify(&[verifying_key]).is_ok());
413
414 // Verify with wrong key should fail
415 let wrong_key = SigningKey::generate_p256();
416 let wrong_verifying_key = wrong_key.verifying_key();
417 assert!(signed.verify(&[wrong_verifying_key]).is_err());
418 }
419
420 #[test]
421 fn test_operation_cid() {
422 let key = SigningKey::generate_p256();
423 let did_key = key.to_did_key();
424
425 let unsigned = Operation::new_genesis(
426 vec![did_key],
427 HashMap::new(),
428 vec![],
429 HashMap::new(),
430 );
431
432 let signed = unsigned.sign(&key).unwrap();
433 let cid = signed.cid().unwrap();
434
435 // CID should start with 'b' (CIDv1 in base32)
436 assert!(cid.starts_with('b'));
437 }
438}