1use crate::util::normalize_op;
2use crate::util::op_from_json;
3use crate::Keypair;
4use crate::PLCError;
5use base32::Alphabet;
6use base64::Engine;
7use cid::Cid;
8use multihash_codetable::{Code, MultihashDigest};
9use serde::{Deserialize, Serialize, Serializer};
10use sha2::{Digest, Sha256};
11use std::collections::HashMap;
12
13#[derive(Clone, Debug)]
14pub enum PLCOperationType {
15 Operation,
16 Tombstone,
17}
18
19impl PLCOperationType {
20 pub fn to_string(&self) -> &str {
21 match self {
22 Self::Operation => "plc_operation",
23 Self::Tombstone => "plc_tombstone",
24 }
25 }
26
27 pub fn from_string(s: &str) -> Option<Self> {
28 match s {
29 "plc_operation" => Some(Self::Operation),
30 "plc_tombstone" => Some(Self::Tombstone),
31 "create" => Some(Self::Operation),
32 _ => None,
33 }
34 }
35}
36
37impl Default for PLCOperationType {
38 fn default() -> Self {
39 Self::Operation
40 }
41}
42
43impl PartialEq for PLCOperationType {
44 fn eq(&self, other: &Self) -> bool {
45 self.to_string() == other.to_string()
46 }
47}
48
49impl Serialize for PLCOperationType {
50 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51 where
52 S: serde::Serializer,
53 {
54 serializer.serialize_str(self.to_string())
55 }
56}
57
58impl<'de> Deserialize<'de> for PLCOperationType {
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: serde::Deserializer<'de>,
62 {
63 let s = String::deserialize(deserializer)?;
64 Self::from_string(&s).ok_or(serde::de::Error::custom("Invalid PLCOperationType"))
65 }
66}
67
68pub trait UnsignedOperation {
69 fn to_json(&self) -> String;
70 fn to_signed(&self, key: &str) -> Result<impl SignedOperation, PLCError>;
71}
72
73pub trait SignedOperation {
74 fn to_json(&self) -> String;
75 fn to_cid(&self) -> Result<String, PLCError>;
76 fn to_did(&self) -> Result<String, PLCError>;
77 fn verify_sig(
78 &self,
79 rotation_keys: Option<Vec<String>>,
80 ) -> Result<(bool, Option<String>), PLCError>;
81}
82
83#[derive(Clone)]
84pub enum PLCOperation {
85 UnsignedGenesis(UnsignedGenesisOperation),
86 SignedGenesis(SignedGenesisOperation),
87 UnsignedPLC(UnsignedPLCOperation),
88 SignedPLC(SignedPLCOperation),
89}
90
91impl Serialize for PLCOperation {
92 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
93 where
94 S: Serializer,
95 {
96 match self {
97 Self::UnsignedGenesis(op) => op.serialize(serializer),
98 Self::SignedGenesis(op) => op.serialize(serializer),
99 Self::UnsignedPLC(op) => op.serialize(serializer),
100 Self::SignedPLC(op) => op.serialize(serializer),
101 }
102 }
103}
104
105impl<'de> Deserialize<'de> for PLCOperation {
106 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
107 where
108 D: serde::Deserializer<'de>,
109 {
110 let value = serde_json::Value::deserialize(deserializer)?;
111 let json = match serde_json::to_string(&value) {
112 Ok(json) => json,
113 Err(e) => return Err(serde::de::Error::custom(e)),
114 };
115 let op = match op_from_json(json.as_str()) {
116 Ok(op) => op,
117 Err(e) => return Err(serde::de::Error::custom(e)),
118 };
119 Ok(op)
120 }
121}
122
123impl Into<UnsignedGenesisOperation> for PLCOperation {
124 fn into(self) -> UnsignedGenesisOperation {
125 match self {
126 Self::UnsignedGenesis(op) => op,
127 _ => panic!("Not a UnsignedGenesisOperation"),
128 }
129 }
130}
131
132impl Into<SignedGenesisOperation> for PLCOperation {
133 fn into(self) -> SignedGenesisOperation {
134 match self {
135 Self::SignedGenesis(op) => op,
136 _ => panic!("Not a SignedGenesisOperation"),
137 }
138 }
139}
140
141impl Into<UnsignedPLCOperation> for PLCOperation {
142 fn into(self) -> UnsignedPLCOperation {
143 match self {
144 Self::UnsignedPLC(op) => op,
145 _ => panic!("Not a UnsignedPLCOperation"),
146 }
147 }
148}
149
150impl Into<SignedPLCOperation> for PLCOperation {
151 fn into(self) -> SignedPLCOperation {
152 match self {
153 Self::SignedPLC(op) => op,
154 _ => panic!("Not a SignedPLCOperation"),
155 }
156 }
157}
158
159#[derive(Serialize, Deserialize, Clone)]
160#[serde(rename_all = "camelCase")]
161pub struct Service {
162 #[serde(rename = "type")]
163 pub type_: String,
164 pub endpoint: String,
165}
166
167#[derive(Serialize, Deserialize, Clone)]
168struct TombstoneOperation {
169 #[serde(rename = "type")]
170 pub type_: String,
171 pub prev: String,
172}
173
174#[derive(Serialize, Deserialize, Clone)]
175#[serde(rename_all = "camelCase")]
176pub struct UnsignedPLCOperation {
177 #[serde(rename = "type")]
178 #[serde(default)]
179 pub type_: PLCOperationType,
180 #[serde(default)]
181 pub rotation_keys: Vec<String>,
182 #[serde(default)]
183 pub verification_methods: HashMap<String, String>,
184 #[serde(default)]
185 pub also_known_as: Vec<String>,
186 #[serde(default)]
187 pub services: HashMap<String, Service>,
188 pub prev: Option<String>,
189}
190
191#[derive(Serialize, Deserialize, Clone)]
192#[serde(rename_all = "camelCase")]
193pub struct SignedPLCOperation {
194 #[serde(flatten)]
195 pub unsigned: UnsignedPLCOperation,
196 pub sig: String,
197}
198
199#[derive(Serialize, Deserialize, Clone)]
200#[serde(rename_all = "camelCase")]
201pub struct UnsignedGenesisOperation {
202 #[serde(rename = "type")]
203 pub type_: String,
204 pub signing_key: String,
205 pub recovery_key: String,
206 pub handle: String,
207 pub service: String,
208 pub prev: Option<String>,
209}
210
211#[derive(Serialize, Deserialize, Clone)]
212#[serde(rename_all = "camelCase")]
213pub struct SignedGenesisOperation {
214 #[serde(flatten)]
215 pub unsigned: UnsignedGenesisOperation,
216 pub sig: String,
217}
218
219impl Default for UnsignedPLCOperation {
220 fn default() -> Self {
221 Self {
222 type_: PLCOperationType::Operation,
223 rotation_keys: Vec::new(),
224 verification_methods: HashMap::new(),
225 also_known_as: Vec::new(),
226 services: HashMap::new(),
227 prev: None,
228 }
229 }
230}
231
232impl UnsignedOperation for UnsignedPLCOperation {
233 fn to_json(&self) -> String {
234 let value = serde_json::to_value(self).unwrap();
235 match self.type_ {
236 PLCOperationType::Operation => serde_json::to_string(&value).unwrap(),
237 PLCOperationType::Tombstone => {
238 let mut map = serde_json::Map::new();
239 map.insert(
240 "type".to_string(),
241 serde_json::Value::String("plc_tombstone".to_string()),
242 );
243 map.insert(
244 "prev".to_string(),
245 serde_json::Value::String(self.prev.clone().unwrap()),
246 );
247 serde_json::to_string(&serde_json::Value::Object(map)).unwrap()
248 }
249 }
250 }
251
252 #[allow(refining_impl_trait)]
253 fn to_signed(&self, key: &str) -> Result<SignedPLCOperation, PLCError> {
254 let keypair = Keypair::from_private_key(key)?;
255 let dag = match self.type_ {
256 PLCOperationType::Operation => serde_ipld_dagcbor::to_vec(&self).unwrap(),
257 PLCOperationType::Tombstone => {
258 let tombstone = TombstoneOperation {
259 type_: "plc_tombstone".to_string(),
260 prev: self.prev.clone().unwrap(),
261 };
262 serde_ipld_dagcbor::to_vec(&tombstone).unwrap()
263 }
264 };
265
266 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
267 let sig = engine.encode(keypair.sign(&dag.as_slice())?);
268
269 Ok(SignedPLCOperation {
270 unsigned: self.clone(),
271 sig,
272 })
273 }
274}
275
276impl UnsignedGenesisOperation {
277 pub fn normalize(&self) -> Result<PLCOperation, PLCError> {
278 let op = serde_json::to_value(self).map_err(|e| PLCError::Other(e.into()))?;
279 let normalized = normalize_op(op);
280 Ok(PLCOperation::UnsignedPLC(
281 serde_json::from_value::<UnsignedPLCOperation>(normalized)
282 .map_err(|e| PLCError::Other(e.into()))?,
283 ))
284 }
285}
286
287impl UnsignedOperation for UnsignedGenesisOperation {
288 fn to_json(&self) -> String {
289 serde_json::to_string(&self).unwrap()
290 }
291
292 #[allow(refining_impl_trait)]
293 fn to_signed(&self, key: &str) -> Result<SignedGenesisOperation, PLCError> {
294 let keypair = Keypair::from_private_key(key)?;
295 let dag = serde_ipld_dagcbor::to_vec(&self).unwrap();
296
297 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
298 let sig = engine.encode(keypair.sign(&dag.as_slice())?);
299
300 Ok(SignedGenesisOperation {
301 unsigned: self.clone(),
302 sig,
303 })
304 }
305}
306
307impl SignedPLCOperation {
308 pub fn from_json(json: &str) -> Result<Self, PLCError> {
309 let raw: serde_json::Value = serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
310 let mut raw = raw.as_object().unwrap().to_owned();
311 let sig = match raw.get("sig") {
312 Some(serde_json::Value::String(s)) => s.clone(),
313 _ => return Err(PLCError::InvalidSignature),
314 };
315 raw.remove("sig");
316 let raw = normalize_op(serde_json::to_value(raw.clone()).map_err(|e| PLCError::Other(e.into()))?);
317
318 let unsigned: UnsignedPLCOperation = serde_json::from_value(raw.clone()).map_err(|e| PLCError::Other(e.into()))?;
319 Ok(Self { unsigned, sig })
320 }
321}
322
323impl SignedOperation for SignedPLCOperation {
324 fn to_json(&self) -> String {
325 match self.unsigned.type_ {
326 PLCOperationType::Operation => serde_json::to_string(&self).unwrap(),
327 PLCOperationType::Tombstone => {
328 let mut map = serde_json::Map::new();
329 map.insert(
330 "type".to_string(),
331 serde_json::Value::String("plc_tombstone".to_string()),
332 );
333 map.insert(
334 "prev".to_string(),
335 serde_json::Value::String(self.unsigned.prev.clone().unwrap()),
336 );
337 map.insert(
338 "sig".to_string(),
339 serde_json::Value::String(self.sig.clone())
340 );
341 serde_json::to_string(&serde_json::Value::Object(map)).unwrap()
342 }
343 }
344 }
345
346 fn to_cid(&self) -> Result<String, PLCError> {
347 let dag = serde_ipld_dagcbor::to_vec(&self).map_err(|e| PLCError::Other(e.into()))?;
348 let result = Code::Sha2_256.digest(&dag.as_slice());
349 let cid = Cid::new_v1(0x71, result);
350 Ok(cid.to_string())
351 }
352
353 fn to_did(&self) -> Result<String, PLCError> {
354 let dag = serde_ipld_dagcbor::to_vec(&self).map_err(|e| PLCError::Other(e.into()))?;
355 let hashed = Sha256::digest(dag.as_slice());
356 let b32 = base32::encode(Alphabet::Rfc4648Lower { padding: false }, hashed.as_slice());
357 Ok(format!("did:plc:{}", b32[0..24].to_string()))
358 }
359
360 fn verify_sig(
361 &self,
362 rotation_keys: Option<Vec<String>>,
363 ) -> Result<(bool, Option<String>), PLCError> {
364 let dag =
365 serde_ipld_dagcbor::to_vec(&self.unsigned).map_err(|e| PLCError::Other(e.into()))?;
366 let dag = dag.as_slice();
367
368 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
369 let decoded_sig = engine
370 .decode(self.sig.as_bytes())
371 .map_err(|_| PLCError::InvalidSignature)?;
372
373 let rotation_keys = match rotation_keys {
374 Some(keys) => keys.clone(),
375 None => self.unsigned.rotation_keys.clone(),
376 };
377
378 for key in rotation_keys {
379 let keypair =
380 Keypair::from_did_key(&key).map_err(|_| PLCError::InvalidOperation)?;
381
382 if keypair
383 .verify(dag, &decoded_sig)
384 .map_err(|e| PLCError::Other(e.into()))?
385 {
386 return Ok((true, Some(key.to_string())));
387 }
388 }
389 Ok((false, None))
390 }
391}
392
393impl SignedGenesisOperation {
394 pub fn from_json(json: &str) -> Result<Self, PLCError> {
395 let raw: serde_json::Value =
396 serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
397 let mut raw = raw.as_object().unwrap().to_owned();
398 let sig = match raw.get("sig") {
399 Some(serde_json::Value::String(s)) => s.clone(),
400 _ => return Err(PLCError::InvalidSignature),
401 };
402 raw.remove("sig");
403
404 let unsigned: UnsignedGenesisOperation = serde_json::from_value(
405 serde_json::to_value(raw.clone()).map_err(|e| PLCError::Other(e.into()))?,
406 )
407 .map_err(|e| PLCError::Other(e.into()))?;
408 Ok(Self { unsigned, sig })
409 }
410
411 pub fn normalize(&self) -> Result<PLCOperation, PLCError> {
412 let op = serde_json::to_value(self).map_err(|e| PLCError::Other(e.into()))?;
413 let normalized = normalize_op(op);
414 Ok(PLCOperation::SignedPLC(
415 serde_json::from_value::<SignedPLCOperation>(normalized)
416 .map_err(|e| PLCError::Other(e.into()))?,
417 ))
418 }
419}
420
421impl SignedOperation for SignedGenesisOperation {
422 fn to_json(&self) -> String {
423 serde_json::to_string(&self).unwrap()
424 }
425
426 fn to_cid(&self) -> Result<String, PLCError> {
427 let dag = serde_ipld_dagcbor::to_vec(&self).map_err(|e| PLCError::Other(e.into()))?;
428 let result = Code::Sha2_256.digest(&dag.as_slice());
429 let cid = Cid::new_v1(0x71, result);
430 Ok(cid.to_string())
431 }
432
433 fn to_did(&self) -> Result<String, PLCError> {
434 let dag = serde_ipld_dagcbor::to_vec(&self).map_err(|e| PLCError::Other(e.into()))?;
435 let hashed = Sha256::digest(dag.as_slice());
436 let b32 = base32::encode(Alphabet::Rfc4648Lower { padding: false }, hashed.as_slice());
437 Ok(format!("did:plc:{}", b32[0..24].to_string()))
438 }
439
440 fn verify_sig(
441 &self,
442 rotation_keys: Option<Vec<String>>,
443 ) -> Result<(bool, Option<String>), PLCError> {
444 let dag =
445 serde_ipld_dagcbor::to_vec(&self.unsigned).map_err(|e| PLCError::Other(e.into()))?;
446 let dag = dag.as_slice();
447
448 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
449 let decoded_sig = engine
450 .decode(self.sig.as_bytes())
451 .map_err(|_| PLCError::InvalidSignature)?;
452
453 let rotation_keys = match rotation_keys {
454 Some(keys) => keys.clone(),
455 None => [
456 self.unsigned.recovery_key.clone(),
457 self.unsigned.signing_key.clone(),
458 ]
459 .to_vec(),
460 };
461 for key in rotation_keys {
462 let keypair =
463 Keypair::from_did_key(&key).map_err(|_| PLCError::InvalidOperation)?;
464
465 if keypair
466 .verify(dag, &decoded_sig)
467 .map_err(|e| PLCError::Other(e.into()))?
468 {
469 return Ok((true, Some(key.to_string())));
470 }
471 }
472 Ok((false, None))
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use crate::BlessedAlgorithm;
479
480 use super::*;
481
482 const TEST_OP_JSON: &str = "{\"sig\":\"8Wj9Cf74dZFNKx7oucZSHbBDFOMJ3xx9lkvj5rT9xMErssWYl1D9n4PeGC0mNml7xDG7uoQqZ1JWoApGADUgXg\",\"prev\":\"bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://puffball.us-east.host.bsky.network\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shQo6TF2moaqMTrUZEM1jeuYRQXeHEx4evX9751y2qPqRA\"}}";
483 const TEST_GENESIS_OP_JSON: &str = "{\"sig\":\"9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA\",\"prev\":null,\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bluesky-team.bsky.social\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}}";
484 const TEST_PREV_OP_JSON: &str = "{\"sig\":\"OoDJihYhLUEWp2MGiAoCN1sRj9cgUEqNjZe6FIOePB8Ugp-IWAZplFRm-pU-fbYSpYV1_tQ9Gx8d_PR9f3NBAg\",\"prev\":\"bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}}";
485 const TEST_DID: &str = "did:plc:z72i7hdynmk6r22z27h6tvur";
486
487 #[actix_rt::test]
488 async fn test_signed_to_json() {
489 let op: SignedPLCOperation = SignedPLCOperation::from_json(TEST_OP_JSON).unwrap();
490 let json = op.to_json();
491
492 let object = serde_json::from_str::<serde_json::Value>(&json).unwrap();
493 let object = object.as_object().unwrap();
494
495 assert!(object.contains_key("sig"));
496 assert!(object.contains_key("prev"));
497 assert!(object.contains_key("type"));
498 assert!(object.contains_key("services"));
499 assert!(object.contains_key("alsoKnownAs"));
500 assert!(object.contains_key("rotationKeys"));
501 assert!(object.contains_key("verificationMethods"));
502 assert!(object.get("type").unwrap() == "plc_operation");
503
504 let rotation_keys = object.get("rotationKeys").unwrap().as_array().unwrap();
506 assert!(rotation_keys.len() == 2);
507 for key in rotation_keys {
508 assert!(key.is_string());
509 match Keypair::from_did_key(key.as_str().unwrap()) {
510 Ok(_) => {}
511 Err(e) => panic!("{}", e),
512 }
513 }
514
515 let verification_methods = object
517 .get("verificationMethods")
518 .unwrap()
519 .as_object()
520 .unwrap();
521 assert!(verification_methods.len() == 1);
522 assert!(verification_methods.contains_key("atproto"));
523 for (key, value) in verification_methods {
524 assert!(value.is_string());
525 if key == "atproto" {
526 match Keypair::from_did_key(value.as_str().unwrap()) {
527 Ok(_) => {}
528 Err(e) => panic!("{}", e),
529 }
530 }
531 }
532
533 let also_known_as = object.get("alsoKnownAs").unwrap().as_array().unwrap();
535 assert!(also_known_as.len() == 1);
536 assert!(also_known_as[0].is_string());
537 assert!(also_known_as[0].as_str().unwrap().starts_with("at://"));
538 assert!(also_known_as[0].as_str().unwrap() == "at://bsky.app");
539
540 let services = object.get("services").unwrap().as_object().unwrap();
542 assert!(services.len() == 1);
543 assert!(services.contains_key("atproto_pds"));
544 let service = services.get("atproto_pds").unwrap().as_object().unwrap();
545 assert!(service.len() == 2);
546 assert!(service.contains_key("type"));
547 assert!(service.contains_key("endpoint"));
548 assert!(service.get("type").unwrap() == "AtprotoPersonalDataServer");
549 assert!(service
550 .get("endpoint")
551 .unwrap()
552 .as_str()
553 .unwrap()
554 .starts_with("https://"));
555 }
556
557 #[actix_rt::test]
558 async fn test_to_cid() {
559 let op: SignedPLCOperation = SignedPLCOperation::from_json(TEST_PREV_OP_JSON).unwrap();
560 let cid = op.to_cid().unwrap();
561
562 assert!(cid == "bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a".to_string());
563 }
564
565 #[actix_rt::test]
566 async fn test_to_did() {
567 let op: SignedPLCOperation = SignedPLCOperation::from_json(TEST_GENESIS_OP_JSON).unwrap();
568 let did = op.to_did().unwrap();
569
570 assert!(did == TEST_DID.to_string());
571 }
572
573 #[actix_rt::test]
574 async fn test_verify_sig() {
575 let op: SignedPLCOperation = SignedPLCOperation::from_json(TEST_OP_JSON).unwrap();
576 let (valid, key) = op.verify_sig(None).unwrap();
577
578 assert!(valid);
579 assert!(
580 key.unwrap() == "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK".to_string()
581 );
582 }
583
584 #[actix_rt::test]
585 async fn test_to_signed() {
586 let signing_key = Keypair::generate(BlessedAlgorithm::P256);
587 let recovery_key = Keypair::generate(BlessedAlgorithm::P256);
588 let validation_key = Keypair::generate(BlessedAlgorithm::P256);
589
590 let op = UnsignedPLCOperation {
591 prev: None,
592 type_: PLCOperationType::Operation,
593 services: HashMap::from([(
594 "atproto_pds".to_string(),
595 Service {
596 type_: "AtprotoPersonalDataServer".to_string(),
597 endpoint: "https://example.test".to_string(),
598 },
599 )]),
600 also_known_as: vec!["at://example.test".to_string()],
601 rotation_keys: vec![
602 recovery_key.to_did_key().unwrap(),
603 signing_key.to_did_key().unwrap(),
604 ],
605 verification_methods: HashMap::from([(
606 "atproto".to_string(),
607 validation_key.to_did_key().unwrap(),
608 )]),
609 };
610 let signed = op
611 .to_signed(signing_key.to_private_key().unwrap().as_str())
612 .unwrap();
613 let (valid, key) = signed.verify_sig(None).unwrap();
614 assert!(valid);
615 assert!(key.unwrap() == signing_key.to_did_key().unwrap());
616 }
617}