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/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>, 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..4489ab13 --- /dev/null +++ b/crates/web5/src/dids/methods/web/resolver.rs @@ -0,0 +1,116 @@ +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 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, +} + +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), + } + } + + async fn resolve(url: String) -> Result { + let mut headers = HeaderMap::new(); + headers.append( + reqwest::header::USER_AGENT, + HeaderValue::from_static(USER_AGENT), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .map_err(|_| ResolutionError::InternalError)?; + + 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::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 }) + } +} + +#[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"); + + 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" + ); + } +}