From 0e647725e1d9006e618b5faeb1d80fffca80741b Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Thu, 9 May 2024 00:11:50 -0300 Subject: [PATCH 1/5] 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/web5/Cargo.toml | 3 +- crates/web5/src/dids/identifier.rs | 2 +- .../src/dids/methods/{web.rs => web/mod.rs} | 28 +++--- crates/web5/src/dids/methods/web/resolver.rs | 99 +++++++++++++++++++ 4 files changed, 117 insertions(+), 15 deletions(-) rename crates/web5/src/dids/methods/{web.rs => web/mod.rs} (60%) create mode 100644 crates/web5/src/dids/methods/web/resolver.rs diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 37076bca..0705d6a0 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -20,6 +20,7 @@ jsonschema = { version = "0.18.0", default-features = false } k256 = { version = "0.13.3", features = ["ecdsa", "jwk"] } rand = { workspace = true } regex = "1.10.4" +reqwest = { version = "0.12.4", features = ["json"] } serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } @@ -35,4 +36,4 @@ uuid = { version = "1.8.0", features = ["v4"] } [dev-dependencies] chrono = { workspace = true } serde_canonical_json = "1.0.0" -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/web5/src/dids/identifier.rs b/crates/web5/src/dids/identifier.rs index c5c3e5f8..ee4be5dc 100644 --- a/crates/web5/src/dids/identifier.rs +++ b/crates/web5/src/dids/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/web5/src/dids/methods/web.rs b/crates/web5/src/dids/methods/web/mod.rs similarity index 60% rename from crates/web5/src/dids/methods/web.rs rename to crates/web5/src/dids/methods/web/mod.rs index ca9d6fa6..1dc1d2eb 100644 --- a/crates/web5/src/dids/methods/web.rs +++ b/crates/web5/src/dids/methods/web/mod.rs @@ -1,6 +1,11 @@ -use crate::dids::methods::{Method, ResolutionResult, Resolve}; -use did_web::DIDWeb as SpruceDidWebMethod; -use ssi_dids::did_resolve::{DIDResolver, ResolutionInputMetadata}; +mod resolver; + +use crate::dids::{ + 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/web5/src/dids/methods/web/resolver.rs b/crates/web5/src/dids/methods/web/resolver.rs new file mode 100644 index 00000000..2684cad2 --- /dev/null +++ b/crates/web5/src/dids/methods/web/resolver.rs @@ -0,0 +1,99 @@ +use std::{ + future::{Future, IntoFuture}, + pin::Pin, +}; + +use reqwest::header::{HeaderMap, HeaderValue}; + +use crate::dids::{ + 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"); + } +} From 817711086a629e51a332d44bb2c5fc18242c3eb2 Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Wed, 15 May 2024 23:09:24 -0300 Subject: [PATCH 2/5] test: test for did-web parsing for id with encoded port --- crates/web5/src/dids/methods/web/resolver.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/web5/src/dids/methods/web/resolver.rs b/crates/web5/src/dids/methods/web/resolver.rs index 2684cad2..070c0f83 100644 --- a/crates/web5/src/dids/methods/web/resolver.rs +++ b/crates/web5/src/dids/methods/web/resolver.rs @@ -95,5 +95,19 @@ mod tests { 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"); + + let did_uri = "did:web:tbd.website%3A8080"; + let result = Resolver::new(Identifier::parse(did_uri).unwrap()); + assert_eq!( + result.did_url, + "https://tbd.website:8080/.well-known/did.json" + ); + + let did_uri = "did:web:tbd.website%3A8080:with:path"; + let result = Resolver::new(Identifier::parse(did_uri).unwrap()); + assert_eq!( + result.did_url, + "https://tbd.website:8080/with/path/did.json" + ); } } From 777bab237097258fd76388e309589f610aac8226 Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Tue, 11 Jun 2024 14:01:35 -0300 Subject: [PATCH 3/5] chore: move logic for resolution into static Resolver fn --- crates/web5/src/dids/methods/web/resolver.rs | 65 ++++++++++---------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/crates/web5/src/dids/methods/web/resolver.rs b/crates/web5/src/dids/methods/web/resolver.rs index 070c0f83..305e6b5e 100644 --- a/crates/web5/src/dids/methods/web/resolver.rs +++ b/crates/web5/src/dids/methods/web/resolver.rs @@ -39,46 +39,49 @@ impl Resolver { 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 { + async fn resolve(url: String) -> Result { 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 client = reqwest::Client::builder() + .default_headers(headers) + .build() + .map_err(|_| ResolutionError::InternalError)?; - let response = client - .get(&self.did_url) - .send() + let response = client + .get(&url) + .send() + .await + .map_err(|_| ResolutionError::InternalError)?; + + if response.status().is_success() { + let did_document = response + .json::() .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) - } - }) + .map_err(|_| ResolutionError::RepresentationNotSupported)?; + + Ok(ResolutionResult { + did_document: Some(did_document), + ..Default::default() + }) + } else { + Err(ResolutionError::NotFound) + } + } +} + +// 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>>; + + fn into_future(self) -> Self::IntoFuture { + let did_url = self.did_url; + Box::pin(async move { Self::resolve(did_url).await }) } } From 633df6b544b0d43d4b4216c6f61bc950d353ee65 Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Wed, 12 Jun 2024 23:07:45 -0300 Subject: [PATCH 4/5] fix: default to empty vec for missing VerificationMethods --- crates/web5/src/dids/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/web5/src/dids/document.rs b/crates/web5/src/dids/document.rs index 7988cef6..a8862a7b 100644 --- a/crates/web5/src/dids/document.rs +++ b/crates/web5/src/dids/document.rs @@ -10,7 +10,7 @@ pub struct Document { pub controller: Option>, #[serde(rename = "alsoKnownAs", skip_serializing_if = "Option::is_none")] pub also_known_as: Option>, - #[serde(rename = "verificationMethod")] + #[serde(rename = "verificationMethod", default = "Vec::new")] pub verification_method: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub authentication: Option>, From 69ccca3a200c3c858b6d5b6c24efea030ab0de69 Mon Sep 17 00:00:00 2001 From: Dan Enman Date: Thu, 13 Jun 2024 13:13:07 -0300 Subject: [PATCH 5/5] fix: type in crates/web5/src/dids/methods/web/resolver.rs Co-authored-by: Kendall Weihe --- crates/web5/src/dids/methods/web/resolver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/web5/src/dids/methods/web/resolver.rs b/crates/web5/src/dids/methods/web/resolver.rs index 305e6b5e..4489ab13 100644 --- a/crates/web5/src/dids/methods/web/resolver.rs +++ b/crates/web5/src/dids/methods/web/resolver.rs @@ -16,7 +16,7 @@ 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 +/// Resolver is the implementation of the did:web method for resolving 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,