Skip to main content

identity_kit/resolver/
dereference.rs

1//! DID URL dereferencing (DID Resolution "DID URL Dereferencing Algorithm").
2
3use serde_json::Value;
4
5use super::Resolver;
6use crate::did::{
7    Did, DidDocument, DidUrl, DocumentError, MethodType, ResolutionError, ResolutionOptions,
8    Service, VerificationMaterial, VerificationMethod, VerificationRelationship,
9};
10use crate::jose::{Jwk, JwkError};
11
12/// The resource a DID URL names, as [`Resolver::dereference`] returns it.
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Dereferenced {
16    /// The DID document: the URL had no fragment.
17    Document(Box<DidDocument>),
18    /// The verification method with the id of the URL, listed under `verificationMethod` or
19    /// embedded in a relationship.
20    VerificationMethod(Box<VerificationMethod>),
21    /// The service with the id of the URL.
22    Service(Box<Service>),
23}
24
25impl Dereferenced {
26    /// The document, when that is what was dereferenced.
27    pub fn as_document(&self) -> Option<&DidDocument> {
28        match self {
29            Self::Document(document) => Some(document),
30            _ => None,
31        }
32    }
33
34    /// The verification method, when that is what was dereferenced.
35    pub fn as_verification_method(&self) -> Option<&VerificationMethod> {
36        match self {
37            Self::VerificationMethod(method) => Some(method),
38            _ => None,
39        }
40    }
41
42    /// The service, when that is what was dereferenced.
43    pub fn as_service(&self) -> Option<&Service> {
44        match self {
45            Self::Service(service) => Some(service),
46            _ => None,
47        }
48    }
49}
50
51/// Why a DID URL could not be dereferenced.
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
53#[non_exhaustive]
54pub enum DereferenceError {
55    /// Resolving the DID of the URL failed.
56    #[error("resolving `{did}`: {error}")]
57    Resolution {
58        /// The DID that was resolved.
59        did: Did,
60        /// What went wrong.
61        #[source]
62        error: ResolutionError,
63    },
64    /// The URL is syntactically valid but not usable: a DID parameter is given twice.
65    #[error("`{url}`: {reason}")]
66    InvalidDidUrl {
67        /// The URL.
68        url: DidUrl,
69        /// What is wrong with it.
70        reason: String,
71    },
72    /// The URL has a path or query parameter whose meaning only its DID method defines; this
73    /// crate dereferences none of them (the `service` and `relativeRef` parameters included).
74    #[error("`{0}` has a path or query that only its DID method can dereference")]
75    MethodSpecific(DidUrl),
76    /// The DID has been deactivated, so none of its resources may be used.
77    #[error("`{0}` is deactivated")]
78    Deactivated(Did),
79    /// No verification method or service of the document has the id of the URL.
80    #[error("`{0}` names no verification method or service of the DID document")]
81    NotFound(DidUrl),
82    /// The URL names the document or a service, not a verification method.
83    #[error("`{0}` is not a verification method")]
84    NotVerificationMethod(DidUrl),
85    /// The verification method exists but is not usable for the required relationship.
86    #[error("verification method `{id}` is not listed under `{relationship}`")]
87    NotInRelationship {
88        /// The id of the verification method.
89        id: DidUrl,
90        /// The relationship that was required.
91        relationship: VerificationRelationship,
92    },
93    /// The verification method has no `publicKeyJwk` and its `publicKeyMultibase` is not a
94    /// Multikey this crate can decode (see [`Jwk::from_multikey`]).
95    #[error("verification method `{id}` of type `{method_type}` has no usable JWK: {source}")]
96    NoJwk {
97        /// The id of the verification method.
98        id: DidUrl,
99        /// Its `type`.
100        method_type: MethodType,
101        /// Why the multibase material did not decode.
102        #[source]
103        source: Box<JwkError>,
104    },
105}
106
107impl Resolver {
108    /// Dereferences `url`: the DID document when the URL is a bare DID, else the verification
109    /// method or service whose id is the URL. The fragment is looked up against the id of the
110    /// resolved document, which is the canonical DID when `url` names an equivalent one.
111    ///
112    /// The `versionId` and `versionTime` DID parameters select the version of the document and
113    /// are passed to the method's resolver as resolution options. A path, any other query
114    /// parameter and the `service`/`relativeRef` parameters are method-specific and not
115    /// dereferenced here.
116    ///
117    /// # Errors
118    ///
119    /// - [`DereferenceError::Resolution`] when resolving the DID fails.
120    /// - [`DereferenceError::InvalidDidUrl`] when a DID parameter is given twice.
121    /// - [`DereferenceError::MethodSpecific`] for a path or a query this crate does not handle.
122    /// - [`DereferenceError::Deactivated`] when the DID has been deactivated.
123    /// - [`DereferenceError::NotFound`] when nothing in the document has the id of the URL.
124    pub async fn dereference(&self, url: &DidUrl) -> Result<Dereferenced, DereferenceError> {
125        let document = self.resolve_for(url).await?;
126        let Some(fragment) = url.fragment() else {
127            return Ok(Dereferenced::Document(Box::new(document)));
128        };
129        let id = document.id.to_url().with_fragment(fragment);
130        if let Some(method) = document.verification_method(&id) {
131            return Ok(Dereferenced::VerificationMethod(Box::new(method.clone())));
132        }
133        if let Some(service) = document.service(&id) {
134            return Ok(Dereferenced::Service(Box::new(service.clone())));
135        }
136        Err(DereferenceError::NotFound(id))
137    }
138
139    /// The verification method `url` names, which must be usable for `relationship` when one
140    /// is given (the `verificationRelationship` dereferencing option of DID Resolution).
141    ///
142    /// # Errors
143    ///
144    /// As [`dereference`](Self::dereference), plus [`DereferenceError::NotVerificationMethod`]
145    /// when the URL has no fragment or names a service, and
146    /// [`DereferenceError::NotInRelationship`] when the method is not referenced or embedded
147    /// under `relationship`.
148    pub async fn resolve_verification_method(
149        &self,
150        url: &DidUrl,
151        relationship: Option<VerificationRelationship>,
152    ) -> Result<VerificationMethod, DereferenceError> {
153        let document = self.resolve_for(url).await?;
154        let Some(fragment) = url.fragment() else {
155            return Err(DereferenceError::NotVerificationMethod(url.clone()));
156        };
157        let id = document.id.to_url().with_fragment(fragment);
158        document
159            .resolve_verification_method(&id, relationship)
160            .cloned()
161            .map_err(|error| match error {
162                DocumentError::NotInRelationship { id, relationship } => {
163                    DereferenceError::NotInRelationship { id, relationship }
164                }
165                _ if document.service(&id).is_some() => DereferenceError::NotVerificationMethod(id),
166                _ => DereferenceError::NotFound(id),
167            })
168    }
169
170    /// The public key of the verification method `url` names, as a JWK: its `publicKeyJwk`,
171    /// or its `publicKeyMultibase` decoded as a Multikey. The method must be usable for
172    /// `relationship` when one is given: `assertionMethod` for the key that signed a
173    /// credential, `authentication` for the key that signed a presentation.
174    ///
175    /// # Errors
176    ///
177    /// As [`resolve_verification_method`](Self::resolve_verification_method), plus
178    /// [`DereferenceError::NoJwk`] when the method carries `publicKeyMultibase` material that
179    /// is not a Multikey this crate can decode.
180    pub async fn resolve_public_key(
181        &self,
182        url: &DidUrl,
183        relationship: Option<VerificationRelationship>,
184    ) -> Result<Jwk, DereferenceError> {
185        let method = self.resolve_verification_method(url, relationship).await?;
186        match &method.material {
187            VerificationMaterial::PublicKeyJwk(jwk) => Ok(jwk.public()),
188            VerificationMaterial::PublicKeyMultibase(multikey) => Jwk::from_multikey(multikey)
189                .map_err(|source| DereferenceError::NoJwk {
190                    id: method.id.clone(),
191                    method_type: method.method_type.clone(),
192                    source: Box::new(source),
193                }),
194        }
195    }
196
197    /// Resolves the DID of `url` with the resolution options its DID parameters carry, and
198    /// refuses a path, an unknown parameter and a deactivated DID.
199    async fn resolve_for(&self, url: &DidUrl) -> Result<DidDocument, DereferenceError> {
200        let options = resolution_options(url)?;
201        let did = url.did();
202        let result = self.resolve_with(did, &options).await.map_err(|error| {
203            DereferenceError::Resolution {
204                did: did.clone(),
205                error,
206            }
207        })?;
208        if result.document_metadata.is_deactivated() {
209            return Err(DereferenceError::Deactivated(did.clone()));
210        }
211        // `resolve_with` returns a document whenever the DID is not deactivated.
212        result
213            .into_document()
214            .map_err(|error| DereferenceError::Resolution {
215                did: did.clone(),
216                error,
217            })
218    }
219}
220
221/// The DID parameters this crate understands, which select a version of the document.
222const VERSION_PARAMETERS: [&str; 2] = ["versionId", "versionTime"];
223
224/// The resolution options for the DID parameters of `url`.
225fn resolution_options(url: &DidUrl) -> Result<ResolutionOptions, DereferenceError> {
226    if url.path().is_some() {
227        return Err(DereferenceError::MethodSpecific(url.clone()));
228    }
229    let mut options = ResolutionOptions::default();
230    let Some(query) = url.query() else {
231        return Ok(options);
232    };
233    for pair in query.split('&').filter(|pair| !pair.is_empty()) {
234        let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
235        let name = percent_decode(name);
236        if !VERSION_PARAMETERS.contains(&name.as_str()) {
237            return Err(DereferenceError::MethodSpecific(url.clone()));
238        }
239        let value = Value::String(percent_decode(value));
240        if options.properties.insert(name.clone(), value).is_some() {
241            return Err(DereferenceError::InvalidDidUrl {
242                url: url.clone(),
243                reason: format!("DID parameter `{name}` is given twice"),
244            });
245        }
246    }
247    Ok(options)
248}
249
250/// Decodes the percent-encoded octets of a query component, which `DidUrl` has already checked
251/// to be complete; `+` is not a space in a DID URL.
252fn percent_decode(s: &str) -> String {
253    let bytes = s.as_bytes();
254    let mut out = Vec::with_capacity(bytes.len());
255    let mut i = 0;
256    while i < bytes.len() {
257        let decoded = (bytes[i] == b'%')
258            .then(|| bytes.get(i + 1..i + 3))
259            .flatten()
260            .and_then(|hex| core::str::from_utf8(hex).ok())
261            .and_then(|hex| u8::from_str_radix(hex, 16).ok());
262        if let Some(byte) = decoded {
263            out.push(byte);
264            i += 3;
265        } else {
266            out.push(bytes[i]);
267            i += 1;
268        }
269    }
270    String::from_utf8_lossy(&out).into_owned()
271}