ndn_security/did/
resolver.rs

1//! [`DidResolver`] trait and built-in resolver implementations.
2//!
3//! Per W3C DID Core §7.1, `resolve(did, options)` returns a
4//! [`DidResolutionResult`] containing three components: the document, document
5//! metadata, and resolution metadata. Every resolver in this module returns the
6//! full result; [`UniversalResolver::resolve_document`] provides a convenience
7//! shortcut when only the document is needed.
8
9pub mod key;
10pub mod ndn;
11
12pub use key::KeyDidResolver;
13pub use ndn::NdnDidResolver;
14
15use std::{collections::HashMap, future::Future, pin::Pin};
16
17use crate::did::{
18    document::DidDocument,
19    metadata::{DidResolutionError, DidResolutionResult},
20};
21
22// ── Error type (legacy convenience) ──────────────────────────────────────────
23
24/// High-level DID error for use in application code.
25///
26/// Resolvers return [`DidResolutionResult`] per the W3C spec; `DidError` is
27/// produced by [`DidResolutionResult::into_document()`] for callers that only
28/// need the document and want a simple `Result<DidDocument, DidError>`.
29#[derive(Debug, thiserror::Error)]
30pub enum DidError {
31    #[error("invalid DID: {0}")]
32    InvalidDid(String),
33    #[error("unsupported DID method: {0}")]
34    UnsupportedMethod(String),
35    #[error("DID document not found: {0}")]
36    NotFound(String),
37    #[error("resolution failed: {0}")]
38    Resolution(String),
39    #[error("invalid DID document: {0}")]
40    InvalidDocument(String),
41}
42
43// ── DidResolver trait ─────────────────────────────────────────────────────────
44
45/// A resolver that can dereference a DID string to a [`DidResolutionResult`].
46///
47/// Per W3C DID Core §7.1, `resolve` must return the complete resolution result
48/// including document metadata and resolution metadata — even on failure (the
49/// error is encoded in `did_resolution_metadata.error`).
50pub trait DidResolver: Send + Sync {
51    /// The DID method this resolver handles (e.g., `"ndn"`, `"key"`).
52    fn method(&self) -> &str;
53
54    /// Resolve the DID, returning the full W3C resolution result.
55    fn resolve<'a>(
56        &'a self,
57        did: &'a str,
58    ) -> Pin<Box<dyn Future<Output = DidResolutionResult> + Send + 'a>>;
59}
60
61// ── UniversalResolver ─────────────────────────────────────────────────────────
62
63/// A resolver that dispatches to method-specific resolvers.
64///
65/// Ships with [`KeyDidResolver`] and [`NdnDidResolver`] pre-registered.
66/// Additional resolvers can be added with [`UniversalResolver::with`].
67pub struct UniversalResolver {
68    resolvers: HashMap<String, Box<dyn DidResolver>>,
69}
70
71impl UniversalResolver {
72    /// Create a resolver with [`KeyDidResolver`] and [`NdnDidResolver`] registered.
73    pub fn new() -> Self {
74        let mut r = Self {
75            resolvers: HashMap::new(),
76        };
77        r.register(KeyDidResolver);
78        r.register(NdnDidResolver::default());
79        r
80    }
81
82    /// Register an additional resolver. Replaces any existing resolver for the same method.
83    pub fn with(mut self, resolver: impl DidResolver + 'static) -> Self {
84        self.register(resolver);
85        self
86    }
87
88    fn register(&mut self, resolver: impl DidResolver + 'static) {
89        self.resolvers
90            .insert(resolver.method().to_string(), Box::new(resolver));
91    }
92
93    /// Resolve a DID, returning the full W3C [`DidResolutionResult`].
94    pub async fn resolve(&self, did: &str) -> DidResolutionResult {
95        let method = match parse_method(did) {
96            Some(m) => m,
97            None => {
98                return DidResolutionResult::err(
99                    DidResolutionError::InvalidDid,
100                    format!("cannot parse DID method from: {did}"),
101                );
102            }
103        };
104
105        match self.resolvers.get(method) {
106            Some(resolver) => resolver.resolve(did).await,
107            None => DidResolutionResult::err(
108                DidResolutionError::MethodNotSupported,
109                format!("no resolver registered for did:{method}"),
110            ),
111        }
112    }
113
114    /// Convenience: resolve and return just the [`DidDocument`].
115    ///
116    /// Maps W3C resolution errors to [`DidError`] for simpler call sites.
117    pub async fn resolve_document(&self, did: &str) -> Result<DidDocument, DidError> {
118        self.resolve(did).await.into_document()
119    }
120}
121
122impl Default for UniversalResolver {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128/// Extract the method name from a DID string (`did:<method>:...` → `<method>`).
129pub(crate) fn parse_method(did: &str) -> Option<&str> {
130    let rest = did.strip_prefix("did:")?;
131    let colon = rest.find(':')?;
132    Some(&rest[..colon])
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn parse_method_valid() {
141        assert_eq!(parse_method("did:ndn:com:acme:alice"), Some("ndn"));
142        assert_eq!(parse_method("did:key:z6Mk..."), Some("key"));
143        assert_eq!(parse_method("did:web:example.com"), Some("web"));
144    }
145
146    #[test]
147    fn parse_method_invalid() {
148        assert_eq!(parse_method("not-a-did"), None);
149        assert_eq!(parse_method("did:"), None);
150        assert_eq!(parse_method("did:no-colon"), None);
151    }
152
153    #[tokio::test]
154    async fn unsupported_method_returns_error_result() {
155        let resolver = UniversalResolver::new();
156        let result = resolver.resolve("did:web:example.com").await;
157        assert!(result.did_resolution_metadata.error.is_some());
158    }
159}