credibil_did/
resolution.rs

1//! # DID Resolver
2//!
3//! This crate provides a DID Resolver trait and a set of default
4//! implementations for resolving DIDs.
5//!
6//! See [DID resolution](https://www.w3.org/TR/did-core/#did-resolution) fpr more.
7
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::document::{Document, DocumentMetadata, Service, VerificationMethod};
14use crate::error::Error;
15use crate::{jwk, key, web, DidResolver};
16
17/// Resolve a DID to a DID document.
18///
19/// The [DID resolution](https://www.w3.org/TR/did-core/#did-resolution) functions
20/// resolve a DID into a DID document by using the "Read" operation of the
21/// applicable DID method.
22///
23/// Caveats:
24/// - No JSON-LD Processing, however, valid JSON-LD is returned.
25/// - Ignores accept header.
26/// - Only returns application/did+ld+json.
27/// - did:key support for ed25519
28/// - did:web support for .well-known and path based DIDs.
29///
30/// # Errors
31///
32/// Returns a [DID resolution](https://www.w3.org/TR/did-core/#did-resolution-metadata)
33/// error as specified.
34pub async fn resolve(
35    did: &str, opts: Option<Options>, resolver: impl DidResolver,
36) -> crate::Result<Resolved> {
37    // use DID-specific resolver
38    let method = did.split(':').nth(1).unwrap_or_default();
39
40    let result = match method {
41        "key" => key::DidKey::resolve(did),
42        "jwk" => jwk::DidJwk::resolve(did, opts, resolver),
43        "web" => web::DidWeb::resolve(did, opts, resolver).await,
44        _ => Err(Error::MethodNotSupported(format!("{method} is not supported"))),
45    };
46
47    if let Err(e) = result {
48        return Ok(Resolved {
49            metadata: Metadata {
50                error: Some(e.to_string()),
51                error_message: Some(e.message()),
52                content_type: ContentType::DidLdJson,
53                ..Metadata::default()
54            },
55            ..Resolved::default()
56        });
57    }
58
59    result
60}
61
62/// Dereference a DID URL into a resource.
63///
64/// # Errors
65pub async fn dereference(
66    did_url: &str, opts: Option<Options>, resolver: impl DidResolver,
67) -> crate::Result<Dereferenced> {
68    // extract DID from DID URL
69    let url = url::Url::parse(did_url)
70        .map_err(|e| Error::InvalidDidUrl(format!("issue parsing URL: {e}")))?;
71    let did = format!("did:{}", url.path());
72
73    // resolve DID document
74    let method = did_url.split(':').nth(1).unwrap_or_default();
75    let resolution = match method {
76        "key" => key::DidKey::resolve(&did)?,
77        "web" => web::DidWeb::resolve(&did, opts, resolver).await?,
78        _ => return Err(Error::MethodNotSupported(format!("{method} is not supported"))),
79    };
80
81    let Some(document) = resolution.document else {
82        return Err(Error::InvalidDid("Unable to resolve DID document".into()));
83    };
84
85    // process document to dereference DID URL for requested resource
86    let Some(verifcation_methods) = document.verification_method else {
87        return Err(Error::NotFound("verification method missing".into()));
88    };
89
90    // for now we assume the DID URL is the ID of the verification method
91    // e.g. did:web:demo.credibil.io#key-0
92    let Some(vm) = verifcation_methods.iter().find(|vm| vm.id == did_url) else {
93        return Err(Error::NotFound("verification method not found".into()));
94    };
95
96    Ok(Dereferenced {
97        metadata: Metadata {
98            content_type: ContentType::DidLdJson,
99            ..Metadata::default()
100        },
101        content_stream: Some(Resource::VerificationMethod(vm.clone())),
102        content_metadata: Some(ContentMetadata {
103            document_metadata: resolution.document_metadata,
104        }),
105    })
106}
107
108/// Used to pass addtional values to a `resolve` and `dereference` methods. Any
109/// properties used should be registered in the DID Specification Registries.
110///
111/// The `accept` property is common to all resolver implementations. It is used
112/// by users to specify the Media Type when calling the `resolve_representation`
113/// method. For example:
114///
115/// ```json
116/// {
117///    "accept": "application/did+ld+json"
118/// }
119/// ```
120#[derive(Clone, Debug, Default, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct Options {
123    /// [`accept`](https://www.w3.org/TR/did-spec-registries/#accept) resolution option.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub accept: Option<ContentType>,
126
127    // pub public_key_format: Option<String>,
128    /// Additional options.
129    #[serde(flatten)]
130    pub additional: Option<HashMap<String, Metadata>>,
131}
132
133/// The DID URL syntax supports parameters in the URL query component. Adding a
134/// DID parameter to a DID URL means the parameter becomes part of the
135/// identifier for a resource.
136#[derive(Clone, Debug, Default, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct Parameters {
139    /// Identifies a service from the DID document by service's ID.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub service: Option<String>,
142
143    /// A relative URI reference that identifies a resource at a service
144    /// endpoint, which is selected from a DID document by using the service
145    /// parameter. MUST use URL encoding if set.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    #[serde(alias = "relative-ref")]
148    pub relative_ref: Option<String>,
149
150    /// Identifies a specific version of a DID document to be resolved (the
151    /// version ID could be sequential, or a UUID, or method-specific).
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub version_id: Option<String>,
154
155    /// Identifies a version timestamp of a DID document to be resolved. That
156    /// is, the DID document that was valid for a DID at a certain time.
157    /// An XML datetime value [XMLSCHEMA11-2] normalized to UTC 00:00:00 without
158    /// sub-second decimal precision. For example: 2020-12-20T19:17:47Z.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub version_time: Option<String>,
161
162    /// A resource hash of the DID document to add integrity protection, as
163    /// specified in [HASHLINK]. This parameter is non-normative.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    #[serde(rename = "hl")]
166    pub hashlink: Option<String>,
167
168    /// Additional parameters.
169    #[serde(flatten)]
170    pub additional: Option<HashMap<String, Value>>,
171}
172
173/// Returned by `resolve` DID methods.
174#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
175#[serde(rename_all = "camelCase")]
176pub struct Resolved {
177    /// The DID resolution context.
178    #[serde(rename = "@context")]
179    pub context: String,
180
181    /// Resolution metadata.
182    pub metadata: Metadata,
183
184    /// The DID document.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub document: Option<Document>,
187
188    /// DID document metadata.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub document_metadata: Option<DocumentMetadata>,
191}
192
193/// `Dereferenced` contains the result of dereferencing a DID URL.
194#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
195#[serde(rename_all = "camelCase")]
196pub struct Dereferenced {
197    /// A metadata structure consisting of values relating to the results of the
198    /// DID URL dereferencing process. MUST NOT be empty in the case of an
199    /// error.
200    pub metadata: Metadata,
201
202    /// The dereferenced resource corresponding to the DID URL. MUST be empty if
203    /// dereferencing was unsuccessful. MUST be empty if dereferencing is
204    /// unsuccessful.
205    pub content_stream: Option<Resource>,
206
207    /// Metadata about the `content_stream`. If `content_stream` is a DID
208    /// document, this MUST be `DidDocumentMetadata`. If dereferencing is
209    /// unsuccessful, MUST be empty.
210    pub content_metadata: Option<ContentMetadata>,
211}
212
213/// Resource represents the DID document resource returned as a result of DID
214/// dereferencing. The resource is a DID document or a subset of a DID document.
215#[allow(clippy::large_enum_variant)]
216#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
217pub enum Resource {
218    ///  DID `Document` resource.
219    Document(Document),
220
221    /// `VerificationMethod` resource.
222    VerificationMethod(VerificationMethod),
223
224    /// `Service` resource.
225    Service(Service),
226}
227
228impl Default for Resource {
229    fn default() -> Self {
230        Self::VerificationMethod(VerificationMethod::default())
231    }
232}
233
234/// DID document metadata.
235#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
236#[serde(rename_all = "camelCase")]
237pub struct Metadata {
238    /// The Media Type of the returned resource.
239    pub content_type: ContentType,
240
241    /// The error code from the dereferencing process, if applicable.
242    /// Values of this field SHOULD be registered in the DID Specification
243    /// Registries. Common values are `invalid_did_url` and `not_found`.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub error: Option<String>,
246
247    /// A human-readable explanation of the error.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub error_message: Option<String>,
250
251    /// Additional information about the resolution or dereferencing process.
252    #[serde(flatten)]
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub additional: Option<Value>,
255}
256
257/// The Media Type of the returned resource.
258#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
259pub enum ContentType {
260    /// JSON-LD representation of a DID document.
261    #[default]
262    #[serde(rename = "application/did+ld+json")]
263    DidLdJson,
264    //
265    // /// The JSON-LD Media Type.
266    // #[serde(rename = "application/ld+json")]
267    // LdJson,
268}
269
270/// Metadata about the `content_stream`. If `content_stream` is a DID document,
271/// this MUST be `DidDocumentMetadata`. If dereferencing is unsuccessful, MUST
272/// be empty.
273#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
274#[serde(rename_all = "camelCase")]
275pub struct ContentMetadata {
276    /// The DID document metadata.
277    #[serde(flatten)]
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub document_metadata: Option<DocumentMetadata>,
280}
281
282#[cfg(test)]
283mod test {
284    use anyhow::anyhow;
285    use insta::assert_json_snapshot as assert_snapshot;
286
287    use super::*;
288
289    #[derive(Clone)]
290    struct MockResolver;
291    impl DidResolver for MockResolver {
292        async fn resolve(&self, _url: &str) -> anyhow::Result<Document> {
293            serde_json::from_slice(include_bytes!("web/did-ecdsa.json"))
294                .map_err(|e| anyhow!("issue deserializing document: {e}"))
295        }
296    }
297
298    #[test]
299    fn error_code() {
300        let err = Error::MethodNotSupported("Method not supported".into());
301        assert_eq!(err.message(), "Method not supported");
302    }
303
304    #[tokio::test]
305    async fn deref_web() {
306        const DID_URL: &str = "did:web:demo.credibil.io#key-0";
307
308        let dereferenced =
309            dereference(DID_URL, None, MockResolver).await.expect("should dereference");
310        assert_snapshot!("deref_web", dereferenced);
311    }
312
313    #[tokio::test]
314    async fn deref_key() {
315        const DID_URL: &str = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
316
317        let dereferenced =
318            dereference(DID_URL, None, MockResolver).await.expect("should dereference");
319        assert_snapshot!("deref_key", dereferenced);
320    }
321}