From 6a580a0a88d7056be3aed52edac78f0fa0db6d9e Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Thu, 9 May 2024 00:11:50 -0300 Subject: [PATCH] feat: add internal did:web resolution Remove dependency on the Spruce crates for did:web resolution Resolves #42 Adds a test related to #39, but needs a trusted host for a real-resolution test for a path-based DID. --- crates/dids/Cargo.toml | 3 +- crates/dids/src/identifier.rs | 2 +- .../dids/src/methods/{web.rs => web/mod.rs} | 28 +++--- crates/dids/src/methods/web/resolver.rs | 99 +++++++++++++++++++ 4 files changed, 117 insertions(+), 15 deletions(-) rename crates/dids/src/methods/{web.rs => web/mod.rs} (60%) create mode 100644 crates/dids/src/methods/web/resolver.rs diff --git a/crates/dids/Cargo.toml b/crates/dids/Cargo.toml index ac2e95e7..2709943e 100644 --- a/crates/dids/Cargo.toml +++ b/crates/dids/Cargo.toml @@ -14,6 +14,7 @@ did-web = "0.2.2" jwk = { path = "../jwk" } keys = { path = "../keys" } regex = "1.10.4" +reqwest = { version = "0.12.4", features = ["json"] } serde = { workspace = true } serde_json = { workspace = true } ssi-core = "0.1.0" @@ -23,4 +24,4 @@ thiserror = { workspace = true } [dev-dependencies] chrono = { workspace = true } -tokio = { version = "1.34.0", features = ["macros", "test-util"] } \ No newline at end of file +tokio = { version = "1.34.0", features = ["macros", "test-util"] } diff --git a/crates/dids/src/identifier.rs b/crates/dids/src/identifier.rs index c5c3e5f8..ee4be5dc 100644 --- a/crates/dids/src/identifier.rs +++ b/crates/dids/src/identifier.rs @@ -12,7 +12,7 @@ pub enum IdentifierError { ParseFailure(String), } -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Identifier { // URI represents the complete Decentralized Identifier (DID) URI. // Spec: https://www.w3.org/TR/did-core/#did-syntax diff --git a/crates/dids/src/methods/web.rs b/crates/dids/src/methods/web/mod.rs similarity index 60% rename from crates/dids/src/methods/web.rs rename to crates/dids/src/methods/web/mod.rs index 86336ccf..01598043 100644 --- a/crates/dids/src/methods/web.rs +++ b/crates/dids/src/methods/web/mod.rs @@ -1,6 +1,11 @@ -use crate::methods::{Method, ResolutionResult, Resolve}; -use did_web::DIDWeb as SpruceDidWebMethod; -use ssi_dids::did_resolve::{DIDResolver, ResolutionInputMetadata}; +mod resolver; + +use crate::{ + identifier::Identifier, + methods::{Method, ResolutionResult, Resolve}, + resolver::ResolutionError, +}; +use resolver::Resolver; /// Concrete implementation for a did:web DID pub struct DidWeb {} @@ -11,16 +16,13 @@ impl Method for DidWeb { impl Resolve for DidWeb { async fn resolve(did_uri: &str) -> ResolutionResult { - let input_metadata = ResolutionInputMetadata::default(); - let (spruce_resolution_metadata, spruce_document, spruce_document_metadata) = - SpruceDidWebMethod.resolve(did_uri, &input_metadata).await; - - match ResolutionResult::from_spruce( - spruce_resolution_metadata, - spruce_document, - spruce_document_metadata, - ) { - Ok(r) => r, + let identifier = match Identifier::parse(did_uri) { + Ok(identifier) => identifier, + Err(_) => return ResolutionResult::from_error(ResolutionError::InvalidDid), + }; + + match Resolver::new(identifier).await { + Ok(result) => result, Err(e) => ResolutionResult::from_error(e), } } diff --git a/crates/dids/src/methods/web/resolver.rs b/crates/dids/src/methods/web/resolver.rs new file mode 100644 index 00000000..7bbf18c6 --- /dev/null +++ b/crates/dids/src/methods/web/resolver.rs @@ -0,0 +1,99 @@ +use std::{ + future::{Future, IntoFuture}, + pin::Pin, +}; + +use reqwest::header::{HeaderMap, HeaderValue}; + +use crate::{ + document::Document, + identifier::Identifier, + resolver::{ResolutionError, ResolutionResult}, +}; + +// PORT_SEP is the : character that separates the domain from the port in a URI. +const PORT_SEP: &str = "%3A"; + +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +/// Resolver is the implementation of the did:web method for resolcing DID URIs. It is responsible +/// for fetching the DID Document from the web according for the did-web spec. +pub struct Resolver { + did_url: String, +} + +impl Resolver { + pub fn new(did_uri: Identifier) -> Self { + // note: delimited is : generally, but ; is allowed by the spec. The did-web spec (ยง3.2) says + // ; should be avoided because of it's potential use for matrix URIs. + let did_url = match did_uri.id.split_once(':') { + Some((domain, path)) => format!( + "{}/{}", + domain.replace(PORT_SEP, ":"), + path.split(':').collect::>().join("/"), + ), + None => format!("{}/{}", did_uri.id.replace(PORT_SEP, ":"), ".well-known",), + }; + + Self { + did_url: format!("https://{}/did.json", did_url), + } + } +} + +// This trait implements the actual logic for resolving a DID URI to a DID Document. +impl IntoFuture for Resolver { + type Output = Result; + type IntoFuture = Pin + Send + Sync>>; + + fn into_future(self) -> Self::IntoFuture { + let mut headers = HeaderMap::new(); + headers.append( + reqwest::header::USER_AGENT, + HeaderValue::from_static(USER_AGENT), + ); + + Box::pin(async move { + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .map_err(|_| ResolutionError::InternalError)?; + + let response = client + .get(&self.did_url) + .send() + .await + .map_err(|_| ResolutionError::InternalError)?; + + if response.status().is_success() { + let did_document = response + .json::() + .await + .map_err(|_| ResolutionError::RepresentationNotSupported)?; + + Ok(ResolutionResult { + did_document: Some(did_document), + ..Default::default() + }) + } else { + Err(ResolutionError::NotFound) + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn resolution_success() { + let did_uri = "did:web:tbd.website"; + let result = Resolver::new(Identifier::parse(did_uri).unwrap()); + assert_eq!(result.did_url, "https://tbd.website/.well-known/did.json"); + + let did_uri = "did:web:tbd.website:with:path"; + let result = Resolver::new(Identifier::parse(did_uri).unwrap()); + assert_eq!(result.did_url, "https://tbd.website/with/path/did.json"); + } +}