tap_agent/
did.rs

1//! DID resolution functionality for the TAP Agent.
2//!
3//! This module provides a multi-resolver for Decentralized Identifiers (DIDs)
4//! that integrates with the didcomm library's DID resolution system. The multi-resolver
5//! currently supports the did:key method, with the architecture allowing for additional
6//! methods to be added in the future.
7
8use async_trait::async_trait;
9use curve25519_dalek::edwards::CompressedEdwardsY;
10use didcomm::did::{
11    DIDDoc, DIDResolver, VerificationMaterial, VerificationMethod, VerificationMethodType,
12};
13use didcomm::error::{
14    Error as DidcommError, ErrorKind as DidcommErrorKind, Result as DidcommResult,
15};
16use multibase::{decode, encode, Base};
17
18use std::collections::HashMap;
19use std::fmt::Debug;
20use std::sync::{Arc, RwLock};
21
22use crate::error::{Error, Result};
23
24/// A trait for resolving DIDs to DID documents that is Send+Sync.
25///
26/// This is a wrapper around didcomm's DIDResolver that adds the
27/// Send+Sync bounds required for the TAP Agent.
28#[async_trait]
29pub trait SyncDIDResolver: Send + Sync + Debug {
30    /// Resolve a DID to a DID document.
31    ///
32    /// # Parameters
33    /// * `did` - The DID to resolve
34    ///
35    /// # Returns
36    /// The DID document as an Option
37    async fn resolve(&self, did: &str) -> Result<Option<DIDDoc>>;
38}
39
40/// A resolver for a specific DID method.
41#[async_trait]
42pub trait DIDMethodResolver: Send + Sync + Debug {
43    /// Returns the method name this resolver handles (e.g., "key", "web", "pkh").
44    fn method(&self) -> &str;
45
46    /// Resolve a DID to a DID document.
47    ///
48    /// # Parameters
49    /// * `did` - The DID to resolve
50    ///
51    /// # Returns
52    /// The DID document as an Option
53    async fn resolve_method(&self, did: &str) -> Result<Option<DIDDoc>>;
54}
55
56/// A resolver for the did:key method.
57#[derive(Debug, Default)]
58pub struct KeyResolver;
59
60impl KeyResolver {
61    /// Create a new KeyResolver
62    pub fn new() -> Self {
63        Self
64    }
65
66    /// Convert an Ed25519 public key to an X25519 public key
67    ///
68    /// This follows the conversion process described in RFC 7748
69    /// https://datatracker.ietf.org/doc/html/rfc7748#section-5
70    fn ed25519_to_x25519(ed25519_pubkey: &[u8]) -> Option<[u8; 32]> {
71        // The Ed25519 public key should be 32 bytes
72        if ed25519_pubkey.len() != 32 {
73            return None;
74        }
75
76        // Add debugging
77        println!("Ed25519 pubkey: {:?}", ed25519_pubkey);
78
79        // Try to create a CompressedEdwardsY from the bytes
80        let edwards_y = match CompressedEdwardsY::try_from(ed25519_pubkey) {
81            Ok(point) => point,
82            Err(e) => {
83                println!("Error converting to CompressedEdwardsY: {:?}", e);
84                return None;
85            }
86        };
87
88        // Try to decompress to get the Edwards point
89        let edwards_point = match edwards_y.decompress() {
90            Some(point) => point,
91            None => {
92                println!("Failed to decompress Edwards point");
93                return None;
94            }
95        };
96
97        // Convert to Montgomery form
98        let montgomery_point = edwards_point.to_montgomery();
99
100        // Get the raw bytes representation of the X25519 key
101        Some(montgomery_point.to_bytes())
102    }
103}
104
105#[async_trait]
106impl DIDMethodResolver for KeyResolver {
107    fn method(&self) -> &str {
108        "key"
109    }
110
111    async fn resolve_method(&self, did_key: &str) -> Result<Option<DIDDoc>> {
112        // Validate that this is a did:key
113        if !did_key.starts_with("did:key:") {
114            return Ok(None);
115        }
116
117        // Parse the multibase-encoded public key
118        let key_id = &did_key[8..]; // Skip the "did:key:" prefix
119        let (_, key_bytes) = match decode(key_id) {
120            Ok(result) => result,
121            Err(_) => return Ok(None),
122        };
123
124        // Check the key prefix - for did:key only Ed25519 is supported
125        if key_bytes.len() < 2 {
126            return Ok(None);
127        }
128
129        // Verify the key type - 0xED01 for Ed25519
130        if key_bytes[0] != 0xED || key_bytes[1] != 0x01 {
131            return Ok(None);
132        }
133
134        // Create the DID Document with the Ed25519 public key
135        let ed25519_public_key = &key_bytes[2..];
136
137        let ed_vm_id = format!("{}#{}", did_key, key_id);
138
139        // Create the Ed25519 verification method
140        let ed_verification_method = VerificationMethod {
141            id: ed_vm_id.clone(),
142            type_: VerificationMethodType::Ed25519VerificationKey2018,
143            controller: did_key.to_string(),
144            verification_material: VerificationMaterial::Multibase {
145                public_key_multibase: key_id.to_string(),
146            },
147        };
148
149        // Convert the Ed25519 public key to X25519 for key agreement
150        let mut verification_methods = vec![ed_verification_method.clone()];
151        let mut key_agreement = Vec::new();
152
153        if let Some(x25519_key) = Self::ed25519_to_x25519(ed25519_public_key) {
154            println!("Successfully converted Ed25519 to X25519!");
155            // Encode the X25519 public key in multibase format
156            let mut x25519_bytes = vec![0xEC, 0x01]; // Prefix for X25519
157            x25519_bytes.extend_from_slice(&x25519_key);
158            let x25519_multibase = encode(Base::Base58Btc, x25519_bytes);
159
160            // Create the X25519 verification method ID
161            let x25519_vm_id = format!("{}#{}", did_key, x25519_multibase);
162
163            // Create the X25519 verification method
164            let x25519_verification_method = VerificationMethod {
165                id: x25519_vm_id.clone(),
166                type_: VerificationMethodType::X25519KeyAgreementKey2019,
167                controller: did_key.to_string(),
168                verification_material: VerificationMaterial::Multibase {
169                    public_key_multibase: x25519_multibase,
170                },
171            };
172
173            // Add the X25519 key agreement method
174            verification_methods.push(x25519_verification_method);
175            key_agreement.push(x25519_vm_id);
176        } else {
177            println!("Failed to convert Ed25519 to X25519!");
178        }
179
180        // Create the DID document
181        let did_doc = DIDDoc {
182            id: did_key.to_string(),
183            verification_method: verification_methods,
184            authentication: vec![ed_vm_id],
185            key_agreement,
186            service: Vec::new(),
187        };
188
189        Ok(Some(did_doc))
190    }
191}
192
193/// A multi-resolver for DID methods. This resolver manages multiple
194/// method-specific resolver. New resolvers can be added at runtime.
195#[derive(Debug)]
196pub struct MultiResolver {
197    resolvers: RwLock<HashMap<String, Arc<dyn DIDMethodResolver>>>,
198}
199
200unsafe impl Send for MultiResolver {}
201unsafe impl Sync for MultiResolver {}
202
203impl MultiResolver {
204    /// Create a new empty MultiResolver
205    pub fn new() -> Self {
206        Self {
207            resolvers: RwLock::new(HashMap::new()),
208        }
209    }
210
211    /// Create a new MultiResolver with a list of resolvers
212    pub fn new_with_resolvers(resolvers: Vec<Arc<dyn DIDMethodResolver>>) -> Self {
213        let resolver = Self::new();
214
215        // Add each resolver to the map if we can acquire the write lock
216        if let Ok(mut resolver_map) = resolver.resolvers.write() {
217            for r in resolvers {
218                let method = r.method().to_string();
219                resolver_map.insert(method, r);
220            }
221        }
222
223        resolver
224    }
225
226    /// Register a new resolver for a specific DID method
227    pub fn register_method<R>(&mut self, method: &str, resolver: R) -> &mut Self
228    where
229        R: DIDMethodResolver + Send + Sync + 'static,
230    {
231        if let Ok(mut resolvers) = self.resolvers.write() {
232            resolvers.insert(method.to_string(), Arc::new(resolver));
233        }
234        self
235    }
236}
237
238impl Default for MultiResolver {
239    fn default() -> Self {
240        let mut resolver = Self::new();
241        resolver.register_method("key", KeyResolver::new());
242        resolver
243    }
244}
245
246#[async_trait]
247impl SyncDIDResolver for MultiResolver {
248    async fn resolve(&self, did: &str) -> Result<Option<DIDDoc>> {
249        // Extract the DID method
250        let parts: Vec<&str> = did.split(':').collect();
251        if parts.len() < 3 {
252            return Err(Error::InvalidDID);
253        }
254
255        let method = parts[1];
256
257        // Get the resolver from the map first
258        let resolver = {
259            let resolver_guard = self
260                .resolvers
261                .read()
262                .map_err(|_| Error::FailedToAcquireResolverReadLock)?;
263            if let Some(resolver) = resolver_guard.get(method) {
264                resolver.clone()
265            } else {
266                return Err(Error::UnsupportedDIDMethod(method.to_string()));
267            }
268            // Lock is dropped here when resolver_guard goes out of scope
269        };
270
271        // Now use the resolver without holding the lock
272        resolver.resolve_method(did).await
273    }
274}
275
276#[async_trait(?Send)]
277impl DIDResolver for MultiResolver {
278    async fn resolve(&self, did: &str) -> DidcommResult<Option<DIDDoc>> {
279        match SyncDIDResolver::resolve(self, did).await {
280            Ok(did_doc) => Ok(did_doc),
281            Err(e) => Err(DidcommError::new(DidcommErrorKind::InvalidState, e)),
282        }
283    }
284}
285
286#[cfg(target_arch = "wasm32")]
287use wasm_bindgen::prelude::*;
288
289#[cfg(target_arch = "wasm32")]
290use js_sys::{Function, Promise};
291
292#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
293use wasm_bindgen_futures::JsFuture;
294
295#[cfg(target_arch = "wasm32")]
296#[wasm_bindgen]
297extern "C" {
298    #[wasm_bindgen(typescript_type = "Promise<string>")]
299    pub type JsPromiseString;
300
301    #[wasm_bindgen(typescript_type = "Promise<string | null>")]
302    pub type JsPromiseStringOrNull;
303}
304
305#[cfg(target_arch = "wasm32")]
306#[wasm_bindgen]
307pub struct JsDIDResolver {
308    method: String,
309    resolve_fn: Function,
310}
311
312#[cfg(target_arch = "wasm32")]
313#[wasm_bindgen]
314impl JsDIDResolver {
315    #[wasm_bindgen(constructor)]
316    pub fn new(resolve_fn: Function) -> Self {
317        Self {
318            method: "".to_string(),
319            resolve_fn,
320        }
321    }
322
323    #[wasm_bindgen]
324    pub fn method(&self) -> String {
325        // The JS resolver should return its method
326        let this = JsValue::null();
327        let method = self
328            .resolve_fn
329            .call1(&this, &JsValue::from_str("method"))
330            .unwrap_or_else(|_| JsValue::from_str("unknown"));
331
332        method.as_string().unwrap_or_else(|| "unknown".to_string())
333    }
334}
335
336/// A resolver for a specific DID method without Send+Sync requirements (for WASM usage)
337#[cfg(target_arch = "wasm32")]
338#[async_trait(?Send)]
339pub trait WasmDIDMethodResolver: Debug {
340    /// Returns the method name this resolver handles (e.g., "key", "web", "pkh").
341    fn method(&self) -> &str;
342
343    /// Resolve a DID to a DID document.
344    ///
345    /// # Parameters
346    /// * `did` - The DID to resolve
347    ///
348    /// # Returns
349    /// The DID document as an Option
350    async fn resolve_method(&self, did: &str) -> Result<Option<DIDDoc>>;
351}
352
353/// A wrapper for JavaScript DID resolvers.
354#[cfg(target_arch = "wasm32")]
355#[derive(Debug)]
356pub struct JsDIDMethodResolver {
357    method: String,
358    resolve_fn: Function,
359}
360
361#[cfg(target_arch = "wasm32")]
362impl JsDIDMethodResolver {
363    /// Create a new JavaScript DID method resolver from a function in the global context
364    pub fn new(method: &str, resolve_fn: Function) -> Self {
365        Self {
366            method: method.to_string(),
367            resolve_fn,
368        }
369    }
370}
371
372#[cfg(target_arch = "wasm32")]
373#[async_trait(?Send)]
374impl WasmDIDMethodResolver for JsDIDMethodResolver {
375    fn method(&self) -> &str {
376        &self.method
377    }
378
379    #[cfg(feature = "wasm")]
380    async fn resolve_method(&self, did: &str) -> Result<Option<DIDDoc>> {
381        // Ensure the DID is for the method that this resolver is for
382        let parts: Vec<&str> = did.split(':').collect();
383        if parts.len() < 3 || parts[1] != self.method {
384            return Err(Error::InvalidDID);
385        }
386
387        let this = JsValue::null();
388        let js_did = JsValue::from_str(did);
389
390        let promise = self
391            .resolve_fn
392            .call1(&this, &js_did)
393            .map_err(|e| Error::JsResolverError(format!("Error calling JS resolver: {:?}", e)))?;
394
395        let promise = Promise::from(promise);
396        let doc_json = JsFuture::from(promise)
397            .await
398            .map_err(|e| Error::JsResolverError(format!("Error from JS promise: {:?}", e)))?;
399
400        if doc_json.is_null() || doc_json.is_undefined() {
401            return Ok(None);
402        }
403
404        let doc_str = doc_json.as_string().ok_or_else(|| {
405            Error::JsResolverError("JS resolver did not return a string".to_string())
406        })?;
407
408        // Parse the JSON string into a DIDDoc
409        serde_json::from_str(&doc_str)
410            .map(Some)
411            .map_err(|e| Error::SerdeError(e))
412    }
413
414    #[cfg(not(feature = "wasm"))]
415    async fn resolve_method(&self, _did: &str) -> Result<Option<DIDDoc>> {
416        Err(Error::NotImplemented(
417            "JavaScript DID Method resolver is only available with the 'wasm' feature".to_string(),
418        ))
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[cfg(feature = "native")]
427    #[tokio::test]
428    async fn test_key_resolver() {
429        let resolver = KeyResolver::new();
430
431        // Test a valid did:key for Ed25519
432        let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
433        let result = resolver.resolve_method(did).await.unwrap();
434
435        assert!(result.is_some());
436        let doc = result.unwrap();
437
438        assert_eq!(doc.id, did);
439        assert_eq!(doc.verification_method.len(), 2); // Should have both Ed25519 and X25519 methods
440
441        // Verify Ed25519 verification method is present
442        let ed25519_method = doc
443            .verification_method
444            .iter()
445            .find(|vm| matches!(vm.type_, VerificationMethodType::Ed25519VerificationKey2018))
446            .expect("Should have an Ed25519 verification method");
447
448        // Verify X25519 verification method
449        let x25519_method = doc
450            .verification_method
451            .iter()
452            .find(|vm| matches!(vm.type_, VerificationMethodType::X25519KeyAgreementKey2019))
453            .expect("Should have an X25519 key agreement method");
454
455        // Check that authentication uses the Ed25519 key
456        assert!(doc.authentication.contains(&ed25519_method.id));
457
458        // Check that key agreement uses the X25519 key
459        assert!(doc.key_agreement.contains(&x25519_method.id));
460    }
461
462    #[cfg(feature = "native")]
463    #[tokio::test]
464    async fn test_multi_resolver() {
465        let resolver = MultiResolver::default();
466
467        // Test resolving a valid did:key
468        let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
469        let result = <MultiResolver as SyncDIDResolver>::resolve(&resolver, did).await;
470
471        assert!(result.is_ok());
472        let doc_option = result.unwrap();
473        assert!(doc_option.is_some());
474
475        let doc = doc_option.unwrap();
476        assert_eq!(doc.id, did);
477        assert_eq!(doc.verification_method.len(), 2); // Should have both Ed25519 and X25519 methods
478
479        // Test resolving an unsupported DID method
480        let did = "did:unsupported:123";
481        let result = <MultiResolver as SyncDIDResolver>::resolve(&resolver, did).await;
482
483        // This should return an error since it's an unsupported method
484        assert!(result.is_err());
485        let err = result.unwrap_err();
486        assert!(err.to_string().contains("Unsupported DID method"));
487    }
488}