Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add internal did:web resolution #190

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/web5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"] }
tokio = { version = "1.34.0", features = ["macros", "test-util"] }
2 changes: 1 addition & 1 deletion crates/web5/src/dids/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct Document {
pub controller: Option<Vec<String>>,
#[serde(rename = "alsoKnownAs", skip_serializing_if = "Option::is_none")]
pub also_known_as: Option<Vec<String>>,
#[serde(rename = "verificationMethod")]
#[serde(rename = "verificationMethod", default = "Vec::new")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever. This may have implications but I'm fine with moving forward with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it because the example did:web (https://tbd.website/.well-known/did.json) was failing to Deserialize, complaining about verification_methods being missing -- though maybe there's another way (aside from #233)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should update that DID because it's not really adding any value. I'm not sure how it can be used in its current state.

pub verification_method: Vec<VerificationMethod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<Vec<String>>,
Expand Down
2 changes: 1 addition & 1 deletion crates/web5/src/dids/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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),
}
}
Expand Down
116 changes: 116 additions & 0 deletions crates/web5/src/dids/methods/web/resolver.rs
Original file line number Diff line number Diff line change
@@ -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"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, @enmand can you help me understand your reasoning for including setting the User-Agent header? I can come up with a few reasons for and against, but I'm slightly skeptical and default to not implementing it unless absolutely necessary.

Also I wonder, do you know how this would impact browser environments given we were to run this in a browser via a WASM binding? It's not a huge deal in this moment because we're deprioritizing WASM in this moment, so no need to spend a bunch of time investigating, but again my skepticism would lead me to conclude we're better off leaving this out until we have clarity on the broad array of cross-platform implications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because it's a way for the did:web server to understand what versions/libraries it's dealing with, though given it's not a service-to-service environment and there are many callers maybe it's not as important. I'm not strong on keeping it or removing it -- I only added it because it was a natural thing for me to include, but also happy to not have it included.

Re. WASM environments -- that's a good questions. I'll implement it in a browser/WASM environment soon for the DWN project I'm working on, and if this is included I can report back (otherwise, I'll probably have other places where I make reqwest requests in the DWN so I can try it there). One challenge with WASM in general is networking, but WASIX and other modern preview specs include some networking (including HTTP stubbed by fetch, iirc)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wrangling around code into the APID in this PR. I'm going to go ahead and merge this code and copy everything over into the temporary apid module, but I'm going to remove this feature. It's probably something we want, but I want to approach these matters conservatively because of downstream impacts to cross-language binding. I appreciate the thought! And I suspect we'll add it back at some point once we fully comprehend the implications.


/// 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::<Vec<&str>>().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<ResolutionResult, ResolutionError> {
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::<Document>()
.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<ResolutionResult, ResolutionError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;

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"
);
}
}
Loading