did_method_plc/
operation.rs

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        // Validate structure of rotationKeys
505        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        // Validate structure of verificationMethods
516        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        // Validate structure of alsoKnownAs
534        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        // Validate structure of services
541        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}