ssi_dids_core/resolution/
dereference.rs

1use iref::{Iri, IriBuf};
2use ssi_core::one_or_many::OneOrMany;
3
4use crate::{
5    document::{self, representation, service, DIDVerificationMethod, Represented, Resource},
6    DIDURLBuf, Fragment, PrimaryDIDURL, DIDURL,
7};
8
9use super::{DIDResolver, Error, Metadata, Output, Parameters, MEDIA_TYPE_URL};
10
11#[derive(Debug, thiserror::Error)]
12pub enum DerefError {
13    #[error("DID resolution failed: {0}")]
14    Resolution(#[from] Error),
15
16    #[error("missing service endpoint `{0}`")]
17    MissingServiceEndpoint(String),
18
19    #[error("unsupported service endpoint map")]
20    UnsupportedServiceEndpointMap,
21
22    #[error("unsupported multiple service endpoints")]
23    UnsupportedMultipleServiceEndpoints,
24
25    #[error("service endpoint construction failed: {0}")]
26    ServiceEndpointConstructionFailed(#[from] ServiceEndpointConstructionConflict),
27
28    #[error("both the DID URL and input service endpoint URL have a fragment component")]
29    FragmentConflict,
30
31    #[error("tried to dereference null primary content")]
32    NullDereference,
33
34    #[error("DID document not found")]
35    NotFound,
36
37    #[error("could not find resource `{0}` in DID document")]
38    ResourceNotFound(DIDURLBuf),
39}
40
41pub struct DerefOutput<T = Content> {
42    pub content: T,
43    pub content_metadata: document::Metadata,
44    pub metadata: Metadata,
45}
46
47impl<T> DerefOutput<T> {
48    pub fn new(content: T, content_metadata: document::Metadata, metadata: Metadata) -> Self {
49        Self {
50            content,
51            content_metadata,
52            metadata,
53        }
54    }
55
56    pub fn url(url: IriBuf) -> Self
57    where
58        T: From<IriBuf>,
59    {
60        Self::new(
61            url.into(),
62            document::Metadata::default(),
63            Metadata::from_content_type(Some(MEDIA_TYPE_URL.to_string())),
64        )
65    }
66
67    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> DerefOutput<U> {
68        DerefOutput {
69            content: f(self.content),
70            content_metadata: self.content_metadata,
71            metadata: self.metadata,
72        }
73    }
74
75    pub fn cast<U: From<T>>(self) -> DerefOutput<U> {
76        self.map(T::into)
77    }
78
79    pub fn into_content(self) -> T {
80        self.content
81    }
82}
83
84impl DerefOutput<PrimaryContent> {
85    pub fn null() -> Self {
86        Self::new(
87            PrimaryContent::Null,
88            document::Metadata::default(),
89            Metadata::default(),
90        )
91    }
92}
93
94pub enum PrimaryContent {
95    Null,
96    Url(IriBuf), // TODO must be an URL
97    Document(document::Represented),
98}
99
100impl From<IriBuf> for PrimaryContent {
101    fn from(value: IriBuf) -> Self {
102        Self::Url(value)
103    }
104}
105
106#[derive(Debug)]
107pub enum Content {
108    Null,
109    Url(IriBuf), // TODO must be an URL
110    Resource(Resource),
111}
112
113impl Content {
114    pub fn as_verification_method(&self) -> Option<&DIDVerificationMethod> {
115        match self {
116            Self::Resource(r) => r.as_verification_method(),
117            _ => None,
118        }
119    }
120
121    pub fn into_verification_method(self) -> Result<DIDVerificationMethod, Self> {
122        match self {
123            Self::Resource(r) => r.into_verification_method().map_err(Self::Resource),
124            other => Err(other),
125        }
126    }
127}
128
129impl From<PrimaryContent> for Content {
130    fn from(value: PrimaryContent) -> Self {
131        match value {
132            PrimaryContent::Null => Self::Null,
133            PrimaryContent::Url(url) => Self::Url(url),
134            PrimaryContent::Document(doc) => {
135                Self::Resource(Resource::Document(doc.into_document()))
136            }
137        }
138    }
139}
140
141/// [Dereferencing the Primary Resource](https://w3c-ccg.github.io/did-resolution/#dereferencing-algorithm-primary) - a subalgorithm of [DID URL dereferencing](https://w3c-ccg.github.io/did-resolution/#dereferencing-algorithm)
142pub(crate) async fn dereference_primary_resource<'a, R: ?Sized + DIDResolver>(
143    resolver: &'a R,
144    primary_did_url: &'a PrimaryDIDURL,
145    parameters: Parameters,
146    resolution_output: Output,
147) -> Result<DerefOutput<PrimaryContent>, DerefError> {
148    // 1
149    match &parameters.service {
150        Some(id) => {
151            // 1.1
152            match resolution_output.document.service(id) {
153                Some(service) => {
154                    // 1.2, 1.2.1
155                    // TODO: support these other cases?
156                    let input_service_endpoint_url = match &service.service_endpoint {
157                        None => return Err(DerefError::MissingServiceEndpoint(id.clone())),
158                        Some(OneOrMany::One(service::Endpoint::Uri(uri))) => uri.as_iri(),
159                        Some(OneOrMany::One(service::Endpoint::Map(_))) => {
160                            return Err(DerefError::UnsupportedServiceEndpointMap)
161                        }
162                        Some(OneOrMany::Many(_)) => {
163                            return Err(DerefError::UnsupportedMultipleServiceEndpoints)
164                        }
165                    };
166
167                    // 1.2.2, 1.2.3
168                    let r = construct_service_endpoint(
169                        primary_did_url,
170                        &parameters,
171                        input_service_endpoint_url,
172                    );
173
174                    match r {
175                        Ok(output_service_endpoint_url) => {
176                            // 1.3
177                            Ok(DerefOutput::url(output_service_endpoint_url))
178                        }
179                        Err(e) => Err(e.into()),
180                    }
181                }
182                None => Err(DerefError::MissingServiceEndpoint(id.clone())),
183            }
184        }
185        None => {
186            // 2
187            if primary_did_url.path().is_empty() && primary_did_url.query().is_none() {
188                // 2.1
189                return Ok(DerefOutput::new(
190                    PrimaryContent::Document(resolution_output.document),
191                    document::Metadata::default(),
192                    resolution_output.metadata,
193                ));
194            }
195
196            // 3
197            if !primary_did_url.path().is_empty() || primary_did_url.query().is_some() {
198                return resolver
199                    .dereference_primary_with_path_or_query(primary_did_url)
200                    .await;
201            }
202
203            // 4
204            Err(DerefError::NotFound)
205        }
206    }
207}
208
209#[derive(Debug, thiserror::Error)]
210pub enum ServiceEndpointConstructionConflict {
211    #[error("both the DID URL and `relativeRef` parameter have a path component")]
212    Path,
213
214    #[error("both the DID URL and input service endpoint URL have a query component")]
215    Query,
216
217    #[error("both the DID URL and input service endpoint URL have a fragment component")]
218    Fragment,
219}
220
221/// <https://w3c-ccg.github.io/did-resolution/#service-endpoint-construction>
222fn construct_service_endpoint(
223    did_url: &DIDURL,
224    did_parameters: &Parameters,
225    service_endpoint_url: &Iri,
226) -> Result<IriBuf, ServiceEndpointConstructionConflict> {
227    // https://w3c-ccg.github.io/did-resolution/#algorithm
228    let mut output = IriBuf::from_scheme(service_endpoint_url.scheme().to_owned());
229    output.set_authority(service_endpoint_url.authority());
230    output.set_path(service_endpoint_url.path());
231
232    let relative_ref_path = did_parameters
233        .relative_ref
234        .as_ref()
235        .map(|r| r.path())
236        .unwrap_or("".try_into().unwrap());
237    match (did_url.path().is_empty(), relative_ref_path.is_empty()) {
238        (false, true) => output.set_path(did_url.path().as_str().try_into().unwrap()),
239        (true, false) => output.set_path(relative_ref_path),
240        (false, false) => return Err(ServiceEndpointConstructionConflict::Path),
241        (true, true) => (),
242    }
243
244    match (did_url.query(), service_endpoint_url.query()) {
245        (Some(query), None) => output.set_query(Some(query.as_str().try_into().unwrap())),
246        (None, Some(query)) => output.set_query(Some(query)),
247        (Some(_), Some(_)) => return Err(ServiceEndpointConstructionConflict::Query),
248        (None, None) => (),
249    }
250
251    match (did_url.fragment(), service_endpoint_url.fragment()) {
252        (Some(fragment), None) => output.set_fragment(Some(fragment.as_str().try_into().unwrap())),
253        (None, Some(fragment)) => output.set_fragment(Some(fragment)),
254        (Some(_), Some(_)) => return Err(ServiceEndpointConstructionConflict::Fragment),
255        (None, None) => (),
256    }
257
258    Ok(output)
259}
260
261impl Represented {
262    pub fn dereference_secondary_resource(
263        self,
264        primary_did_url: &PrimaryDIDURL,
265        fragment: &Fragment,
266        content_metadata: document::Metadata,
267        metadata: Metadata,
268    ) -> Result<DerefOutput, DerefError> {
269        match self {
270            Self::Json(d) => d.dereference_secondary_resource(
271                primary_did_url,
272                fragment,
273                content_metadata,
274                metadata,
275            ),
276            Self::JsonLd(d) => d.dereference_secondary_resource(
277                primary_did_url,
278                fragment,
279                content_metadata,
280                metadata,
281            ),
282        }
283    }
284}
285
286impl representation::Json {
287    pub fn dereference_secondary_resource(
288        self,
289        primary_did_url: &PrimaryDIDURL,
290        fragment: &Fragment,
291        content_metadata: document::Metadata,
292        metadata: Metadata,
293    ) -> Result<DerefOutput, DerefError> {
294        let id = primary_did_url.to_owned().with_fragment(fragment);
295        match self.into_document().into_resource(&id) {
296            Some(resource) => Ok(DerefOutput::new(
297                Content::Resource(resource),
298                content_metadata,
299                metadata,
300            )),
301            None => Err(DerefError::ResourceNotFound(id)),
302        }
303    }
304}
305
306impl representation::JsonLd {
307    pub fn dereference_secondary_resource(
308        self,
309        primary_did_url: &PrimaryDIDURL,
310        fragment: &Fragment,
311        content_metadata: document::Metadata,
312        metadata: Metadata,
313    ) -> Result<DerefOutput, DerefError> {
314        // TODO: use actual JSON-LD fragment dereferencing
315        // https://www.w3.org/TR/did-core/#application-did-ld-json
316        //   Fragment identifiers used with application/did+ld+json are treated according to the
317        //   rules associated with the JSON-LD 1.1: application/ld+json media type [JSON-LD11].
318        let id = primary_did_url.to_owned().with_fragment(fragment);
319        match self.into_document().into_resource(&id) {
320            Some(resource) => Ok(DerefOutput::new(
321                Content::Resource(resource),
322                content_metadata,
323                metadata,
324            )),
325            None => Err(DerefError::ResourceNotFound(id)),
326        }
327    }
328}
329
330/// [Dereferencing the Secondary Resource](https://w3c-ccg.github.io/did-resolution/#dereferencing-algorithm-secondary) - a subalgorithm of [DID URL dereferencing](https://w3c-ccg.github.io/did-resolution/#dereferencing-algorithm)
331pub(crate) fn dereference_secondary_resource(
332    primary_did_url: &PrimaryDIDURL,
333    fragment: &Fragment,
334    primary_deref_output: DerefOutput<PrimaryContent>,
335) -> Result<DerefOutput, DerefError> {
336    // 1
337    match primary_deref_output.content {
338        PrimaryContent::Document(doc) => doc.dereference_secondary_resource(
339            primary_did_url,
340            fragment,
341            primary_deref_output.content_metadata,
342            primary_deref_output.metadata,
343        ),
344        PrimaryContent::Url(mut url) => {
345            // 2
346            // 2.1
347            if url.fragment().is_some() {
348                Err(DerefError::FragmentConflict)
349            } else {
350                url.set_fragment(Some(fragment.as_str().try_into().unwrap()));
351                Ok(DerefOutput::new(
352                    Content::Url(url),
353                    primary_deref_output.content_metadata,
354                    primary_deref_output.metadata,
355                ))
356            }
357        }
358        PrimaryContent::Null => Err(DerefError::NullDereference),
359    }
360}