identity_kit/resolver/
dereference.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Dereferenced {
16 Document(Box<DidDocument>),
18 VerificationMethod(Box<VerificationMethod>),
21 Service(Box<Service>),
23}
24
25impl Dereferenced {
26 pub fn as_document(&self) -> Option<&DidDocument> {
28 match self {
29 Self::Document(document) => Some(document),
30 _ => None,
31 }
32 }
33
34 pub fn as_verification_method(&self) -> Option<&VerificationMethod> {
36 match self {
37 Self::VerificationMethod(method) => Some(method),
38 _ => None,
39 }
40 }
41
42 pub fn as_service(&self) -> Option<&Service> {
44 match self {
45 Self::Service(service) => Some(service),
46 _ => None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
53#[non_exhaustive]
54pub enum DereferenceError {
55 #[error("resolving `{did}`: {error}")]
57 Resolution {
58 did: Did,
60 #[source]
62 error: ResolutionError,
63 },
64 #[error("`{url}`: {reason}")]
66 InvalidDidUrl {
67 url: DidUrl,
69 reason: String,
71 },
72 #[error("`{0}` has a path or query that only its DID method can dereference")]
75 MethodSpecific(DidUrl),
76 #[error("`{0}` is deactivated")]
78 Deactivated(Did),
79 #[error("`{0}` names no verification method or service of the DID document")]
81 NotFound(DidUrl),
82 #[error("`{0}` is not a verification method")]
84 NotVerificationMethod(DidUrl),
85 #[error("verification method `{id}` is not listed under `{relationship}`")]
87 NotInRelationship {
88 id: DidUrl,
90 relationship: VerificationRelationship,
92 },
93 #[error("verification method `{id}` of type `{method_type}` has no usable JWK: {source}")]
96 NoJwk {
97 id: DidUrl,
99 method_type: MethodType,
101 #[source]
103 source: Box<JwkError>,
104 },
105}
106
107impl Resolver {
108 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 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 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 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 result
213 .into_document()
214 .map_err(|error| DereferenceError::Resolution {
215 did: did.clone(),
216 error,
217 })
218 }
219}
220
221const VERSION_PARAMETERS: [&str; 2] = ["versionId", "versionTime"];
223
224fn 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
250fn 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}