did_method_plc/
lib.rs

1extern crate thiserror;
2
3use async_trait::async_trait;
4use didkit::{
5    DIDMethod, DIDResolver, Document, DocumentMetadata, ResolutionInputMetadata,
6    ResolutionMetadata,
7};
8use operation::{PLCOperation, Service, SignedOperation, SignedPLCOperation, UnsignedPLCOperation};
9use util::op_from_json;
10
11mod audit;
12mod error;
13mod keypair;
14mod multicodec;
15mod op_builder;
16pub mod operation;
17mod util;
18
19pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
20pub const DEFAULT_HOST: &str = "https://plc.directory";
21
22pub use audit::{AuditLog, DIDAuditLogs};
23pub use error::PLCError;
24pub use keypair::{BlessedAlgorithm, Keypair};
25pub use op_builder::OperationBuilder;
26
27pub struct PLCOperationResult {
28    pub did: String,
29    pub status: u16,
30    pub body: String,
31}
32
33/// did:plc Method
34///
35/// [Specification](https://web.plc.directory/spec/v0.1/did-plc)
36pub struct DIDPLC {
37    host: String,
38    client: reqwest::Client,
39}
40
41impl DIDPLC {
42    pub fn new(host: &str) -> Self {
43        let client = reqwest::Client::builder()
44            .user_agent(USER_AGENT)
45            .build()
46            .unwrap();
47
48        Self {
49            host: host.to_string(),
50            client,
51        }
52    }
53
54    pub async fn execute_op(&self, did: &str, op: &SignedPLCOperation) -> Result<PLCOperationResult, PLCError> {
55        let res = self
56            .client
57            .post(format!("{}/{}", self.host, did))
58            .header(reqwest::header::CONTENT_TYPE, "application/json")
59            .body(op.to_json())
60            .send()
61            .await?;
62
63        let status = res.status().as_u16();
64        let body: String = res.text().await?;
65        Ok(PLCOperationResult {
66            did: did.to_string(),
67            status: status,
68            body,
69        })
70    }
71
72    pub async fn get_log(&self, did: &str) -> Result<Vec<PLCOperation>, PLCError> {
73        let res = self
74            .client
75            .get(format!("{}/{}/log", self.host, did))
76            .send()
77            .await?;
78
79        let body: String = res.text().await?;
80        let mut operations: Vec<PLCOperation> = vec![];
81        let json: Vec<serde_json::Value> =
82            serde_json::from_str(&body).map_err(|e| PLCError::Other(e.into()))?;
83
84        for op in json {
85            operations.push(
86                op_from_json(
87                    serde_json::to_string(&op)
88                        .map_err(|e| PLCError::Other(e.into()))?
89                        .as_str(),
90                )
91                .map_err(|e| PLCError::Other(e.into()))?,
92            );
93        }
94
95        Ok(operations)
96    }
97
98    pub async fn get_audit_log(&self, did: &str) -> Result<DIDAuditLogs, PLCError> {
99        let res = self
100            .client
101            .get(format!("{}/{}/log/audit", self.host, did))
102            .send()
103            .await?;
104
105        if !res.status().is_success() {
106            return Err(PLCError::Http(
107                res.status().as_u16(),
108                res.text().await.unwrap_or_default(),
109            ));
110        }
111
112        let body: String = res.text().await?;
113
114        Ok(DIDAuditLogs::from_json(&body).map_err(|e| PLCError::Other(e.into()))?)
115    }
116
117    pub async fn get_last_log(&self, did: &str) -> Result<PLCOperation, PLCError> {
118        let res = self
119            .client
120            .get(format!("{}/{}/log/last", self.host, did))
121            .send()
122            .await?;
123
124        let body: String = res.text().await?;
125        let op: serde_json::Value =
126            serde_json::from_str(&body).map_err(|e| PLCError::Other(e.into()))?;
127
128        Ok(op_from_json(
129            serde_json::to_string(&op)
130                .map_err(|e| PLCError::Other(e.into()))?
131                .as_str(),
132        )?)
133    }
134
135    pub async fn get_current_state(&self, did: &str) -> Result<PLCOperation, PLCError> {
136        let res = self
137            .client
138            .get(format!("{}/{}/data", self.host, did))
139            .send()
140            .await?;
141
142        let body: String = res.text().await?;
143
144        Ok(PLCOperation::UnsignedPLC(serde_json::from_str::<UnsignedPLCOperation>(&body)
145            .map_err(|e| PLCError::Other(e.into()))?
146        ))
147    }
148}
149
150impl Default for DIDPLC {
151    fn default() -> Self {
152        Self::new(DEFAULT_HOST)
153    }
154}
155
156#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
157#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
158impl DIDMethod for DIDPLC {
159    fn name(&self) -> &'static str {
160        "did:plc"
161    }
162
163    fn to_resolver(&self) -> &dyn DIDResolver {
164        self
165    }
166}
167
168#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
169#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
170impl DIDResolver for DIDPLC {
171    async fn resolve(
172        &self,
173        did: &str,
174        _input_metadata: &ResolutionInputMetadata,
175    ) -> (
176        ResolutionMetadata,
177        Option<Document>,
178        Option<DocumentMetadata>,
179    ) {
180        let res = match self
181            .client
182            .get(format!("{}/{}", self.host, did))
183            .send()
184            .await
185        {
186            Ok(res) => res,
187            Err(err) => {
188                return (
189                    ResolutionMetadata::from_error(&format!("Failed to get URL: {:?}", err)),
190                    None,
191                    None,
192                )
193            }
194        };
195
196        match res.status().as_u16() {
197            200 => {
198                let text = match res.text().await {
199                    Ok(json) => json,
200                    Err(err) => {
201                        return (
202                            ResolutionMetadata::from_error(&format!(
203                                "Failed to parse JSON response: {:?}",
204                                err
205                            )),
206                            None,
207                            None,
208                        )
209                    }
210                };
211
212                match Document::from_json(text.as_str()) {
213                    Ok(document) => (ResolutionMetadata::default(), Some(document), None),
214                    Err(err) => (
215                        ResolutionMetadata::from_error(&format!(
216                            "Unable to parse DID document: {:?}",
217                            err
218                        )),
219                        None,
220                        None,
221                    ),
222                }
223            }
224            404 => (
225                ResolutionMetadata::from_error(&format!("DID not found: {}", did)),
226                None,
227                None,
228            ),
229            _ => (
230                ResolutionMetadata::from_error(&format!("Failed to resolve DID: {}", res.status())),
231                None,
232                None,
233            ),
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use operation::PLCOperationType;
241
242    use super::*;
243
244    const PLC_HOST: &str = "https://plc.directory"; // "http://localhost:2894";
245
246    #[actix_rt::test]
247    async fn test_didplc_resolve() {
248        let didplc = DIDPLC::default();
249        let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
250        let (res_metadata, document, _) = didplc
251            .resolve(did, &ResolutionInputMetadata::default())
252            .await;
253
254        assert!(res_metadata.error.is_none());
255        assert!(document.is_some());
256    }
257
258    #[actix_rt::test]
259    async fn test_didplc_get_log() {
260        let didplc = DIDPLC::default();
261        let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
262        let log = didplc.get_log(did).await;
263
264        assert!(log.is_ok());
265        assert!(log.unwrap().len() > 0);
266    }
267
268    #[actix_rt::test]
269    async fn test_didplc_get_audit_log() {
270        let didplc = DIDPLC::default();
271        let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
272        let log = didplc.get_audit_log(did).await;
273
274        assert!(log.is_ok());
275        assert!(log.unwrap().len() > 0);
276    }
277
278    #[actix_rt::test]
279    async fn test_didplc_get_last_log() {
280        let didplc = DIDPLC::default();
281        let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
282        let log = didplc.get_last_log(did).await;
283
284        assert!(log.is_ok());
285    }
286
287    #[actix_rt::test]
288    async fn test_didplc_get_current_state() {
289        let didplc = DIDPLC::default();
290        let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
291        let log = didplc.get_current_state(did).await;
292
293        assert!(log.is_ok());
294    }
295
296    #[actix_rt::test]
297    async fn test_didplc_operations() {
298        let didplc = DIDPLC::new(PLC_HOST);
299        let recovery_key = Keypair::generate(BlessedAlgorithm::P256);
300        let signing_key = Keypair::generate(BlessedAlgorithm::P256);
301        let verification_key = Keypair::generate(BlessedAlgorithm::P256);
302
303        let create_op = OperationBuilder::new(&didplc)
304            .with_key(&signing_key)
305            .with_validation_key(&verification_key)
306            .add_rotation_key(&recovery_key)
307            .add_rotation_key(&signing_key)
308            .with_handle("example.test".to_owned())
309            .with_pds("example.test".to_owned())
310            .build(PLCOperationType::Operation)
311            .await;
312
313        assert!(create_op.is_ok(), "Failed to build create op: {:?}", create_op.err());
314        let create_op = create_op.unwrap();
315        let did = &create_op.to_did().expect("Failed to turn op to DID");
316
317        let create_res = didplc.execute_op(did, &create_op).await;
318
319        assert!(create_res.is_ok(), "Failed to execute create op: {:?}", create_res.err());
320        let create_res = create_res.unwrap();
321
322        assert!(create_res.status == 200, "Failed to execute create op: status = {}, body = {:?}", create_res.status, create_res.body);
323        assert!(&create_res.did == did, "Failed to execute create op: did = {}, expected = {}", create_res.did, did);
324
325        let update_op = OperationBuilder::for_did(&didplc, did.clone())
326            .with_key(&signing_key)
327            .with_validation_key(&verification_key)
328            .add_rotation_key(&recovery_key)
329            .add_rotation_key(&signing_key)
330            .with_handle("touma.example.test".to_owned())
331            .with_pds("example.test".to_owned())
332            .build(PLCOperationType::Operation)
333            .await;
334
335        assert!(update_op.is_ok(), "Failed to build update op: {:?}", update_op.err());
336        let update_op = update_op.unwrap();
337        let update_res = didplc.execute_op(did, &update_op).await;
338        assert!(update_res.is_ok(), "Failed to execute update op: {:?}", update_res.err());
339
340        let update_res = update_res.unwrap();
341        assert!(update_res.status == 200, "Failed to execute update op: status = {}, body = {:?}, json = {}", update_res.status, update_res.body, update_op.to_json());
342        assert!(&update_res.did == did, "Failed to execute update op: did = {}, expected = {}", update_res.did, did);
343
344        let deactivate_op = OperationBuilder::for_did(&didplc, did.clone())
345            .with_key(&signing_key)
346            .with_validation_key(&verification_key)
347            .add_rotation_key(&recovery_key)
348            .add_rotation_key(&signing_key)
349            .with_handle("touma.example.test".to_owned())
350            .with_pds("example.test".to_owned())
351            .build(PLCOperationType::Tombstone)
352            .await;
353        assert!(deactivate_op.is_ok(), "Failed to build deactivate op: {:?}", deactivate_op.err());
354        let deactivate_op = deactivate_op.unwrap();
355        let deactivate_res = didplc.execute_op(did, &deactivate_op).await;
356        assert!(deactivate_res.is_ok(), "Failed to execute deactivate op: {:?}, json = {}", deactivate_res.err(), deactivate_op.to_json());
357
358        let deactivate_res = deactivate_res.unwrap();
359        assert!(deactivate_res.status == 200, "Failed to execute deactivate op: status = {}, body = {:?}", deactivate_res.status, deactivate_res.body);
360        assert!(&deactivate_res.did == did, "Failed to execute deactivate op: did = {}, expected = {}", deactivate_res.did, did);
361
362        let recover_op = OperationBuilder::for_did(&didplc, did.clone())
363            .with_key(&recovery_key)
364            .with_validation_key(&verification_key)
365            .add_rotation_key(&recovery_key)
366            .add_rotation_key(&signing_key)
367            .with_handle("touma.example.test".to_owned())
368            .with_pds("example.test".to_owned())
369            .build(PLCOperationType::Operation)
370            .await;
371        assert!(recover_op.is_ok(), "Failed to build recover op: {:?}", recover_op.err());
372        let recover_op = recover_op.unwrap();
373        let recover_res = didplc.execute_op(did, &recover_op).await;
374        assert!(recover_res.is_ok(), "Failed to execute recover op: {:?}, json = {}", recover_res.err(), recover_op.to_json());
375
376        let recover_res = recover_res.unwrap();
377        assert!(recover_res.status == 200, "Failed to execute recover op: status = {}, body = {:?}", recover_res.status, recover_res.body);
378        assert!(&recover_res.did == did, "Failed to execute recover op: did = {}, expected = {}", recover_res.did, did);
379    }
380}