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

Implementing did:web resolution #52

Merged
merged 13 commits into from
Oct 5, 2023
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ Cargo.lock
.rust-analyzer/

# setting json
*/.vscode/settings.json
.vscode/settings.json

# Mac OS X Finder metadata
.DS_Store
7 changes: 7 additions & 0 deletions did-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ json-patch = "1.0.0"
serde = "1.0"
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
hyper = { version = "0.14.26", features = ["client", "http2"] }
hyper-tls = "0.5.0"
tokio = { version = "1.20.4" }

# cross-platform random number generator from os
getrandom = { version = "0.2", features = ["js"] }
Expand All @@ -28,4 +31,8 @@ num-bigint = "0.4.4"
hex = "0.4.3"

[dev-dependencies]
hyper = { version = "0.14.26", features = ["server"] }
async-std = { version = "1.12.0", features = ["attributes"] }
hex = "0.4.3"
tokio = { version = "1.27.0", default-features = false, features = ["macros", "rt"] }
tokio-test = "0.4.2"
1 change: 1 addition & 0 deletions did-utils/src/methods/did_web/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod resolver;
151 changes: 151 additions & 0 deletions did-utils/src/methods/did_web/resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use async_trait::async_trait;
use hyper::{
client::{ connect::Connect, HttpConnector },
http::uri::{ self, Scheme },
Body,
Client,
Uri,
};
use hyper_tls::HttpsConnector;

use crate::methods::{
errors::DidWebError,
traits::{
DIDResolutionMetadata,
DIDResolutionOptions,
DIDResolver,
MediaType,
ResolutionOutput,
},
};

use crate::ldmodel::Context;

use crate::didcore::Document as DIDDocument;

pub struct DidWebResolver<C> where C: Connect + Send + Sync + Clone + 'static {
client: Client<C>,
scheme: Scheme,
}

impl DidWebResolver<HttpConnector> {
pub fn http() -> DidWebResolver<HttpConnector> {
DidWebResolver {
client: Client::builder().build::<_, Body>(HttpConnector::new()),
scheme: Scheme::HTTP,
}
}
}

impl DidWebResolver<HttpsConnector<HttpConnector>> {
pub fn https() -> DidWebResolver<HttpsConnector<HttpConnector>> {
DidWebResolver {
client: Client::builder().build::<_, Body>(HttpsConnector::new()),
scheme: Scheme::HTTPS,
}
}
}

impl<C> DidWebResolver<C> where C: Connect + Send + Sync + Clone + 'static {
async fn fetch_did_document(&self, url: Uri) -> Result<String, DidWebError> {
let res = self.client.get(url).await?;

if !res.status().is_success() {
return Err(DidWebError::NonSuccessResponse(res.status()));
}

let body = hyper::body::to_bytes(res.into_body()).await?;

String::from_utf8(body.to_vec()).map_err(|err| err.into())
}
}

impl<C> DidWebResolver<C> where C: Connect + Send + Sync + Clone + 'static {
async fn resolver_fetcher(&self, did: &str) -> Result<DIDDocument, DidWebError> {
let (path, domain_name) = match parse_did_web_url(did) {
Ok((path, domain_name)) => (path, domain_name),
Err(err) => {
return Err(DidWebError::RepresentationNotSupported(err.to_string()));
}
};

let url: Uri = match
uri::Builder
::new()
.scheme(self.scheme.clone())
.authority(domain_name)
.path_and_query(path)
.build()
{
Ok(url) => url,
Err(err) => {
return Err(DidWebError::RepresentationNotSupported(err.to_string()));
}
};

let json_string = match self.fetch_did_document(url).await {
Ok(json) => json,
Err(err) => {
return Err(err);
}
};

let did_document: DIDDocument = match serde_json::from_str(&json_string) {
Ok(document) => document,
Err(err) => {
return Err(DidWebError::RepresentationNotSupported(err.to_string()));
}
};

Ok(did_document)
}
}

pub fn parse_did_web_url(did: &str) -> Result<(String, String), DidWebError> {
let mut parts = did.split(':').peekable();
let domain_name = match (parts.next(), parts.next(), parts.next()) {
(Some("did"), Some("web"), Some(domain_name)) => domain_name.replacen("%3A", ":", 1),
_ => {
return Err(DidWebError::InvalidDid("Invalid DID".to_string()));
}
};

let mut path = match parts.peek() {
Some(_) => parts.collect::<Vec<&str>>().join("/"),
None => ".well-known".to_string(),
};

path = format!("/{}/did.json", path);

Ok((path, domain_name))
}

#[async_trait]
impl<C> DIDResolver for DidWebResolver<C> where C: Connect + Send + Sync + Clone + 'static {
async fn resolve(&self, did: &str, _options: &DIDResolutionOptions) -> ResolutionOutput {
let context = Context::SingleString(String::from("https://www.w3.org/ns/did/v1"));

match self.resolver_fetcher(did).await {
Ok(diddoc) =>
ResolutionOutput {
context,
did_document: Some(diddoc),
did_resolution_metadata: Some(DIDResolutionMetadata {
error: None,
content_type: Some(MediaType::DidLdJson.to_string()),
additional_properties: None,
}),
did_document_metadata: None,
additional_properties: None,
},
Err(_err) =>
ResolutionOutput {
context,
did_document: None,
did_resolution_metadata: None,
did_document_metadata: None,
additional_properties: None,
},
}
}
}
42 changes: 42 additions & 0 deletions did-utils/src/methods/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use thiserror::Error;

Expand Down Expand Up @@ -48,4 +49,45 @@ pub enum DIDResolutionError {
NotAllowedLocalDerivedKey,
#[error("notAllowedGlobalDuplicateKey")]
NotAllowedGlobalDuplicateKey,
#[error("Non-success server response")]
NonSuccessResponse,
}

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum DidWebError {
#[error("DID method not supported: {0}")]
MethodNotSupported(String),
#[error("Representation not supported: {0}")]
RepresentationNotSupported(String),
#[error("Invalid DID: {0}")]
InvalidDid(String),
#[error("Parsing error: {0}")]
ParsingError(#[from] ParsingErrorSource),
#[error("URL parsing error: {0}")]
HttpError(#[from] hyper::Error),
#[error("Non-success server response: {0}")]
NonSuccessResponse(StatusCode),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}

#[derive(Error, Debug)]
pub enum ParsingErrorSource {
#[error("JSON parsing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Invalid encoding: {0}")]
Utf8Error(#[from] std::string::FromUtf8Error),
}

impl From<serde_json::Error> for DidWebError {
fn from(error: serde_json::Error) -> Self {
DidWebError::ParsingError(ParsingErrorSource::JsonError(error))
}
}

impl From<std::string::FromUtf8Error> for DidWebError {
fn from(error: std::string::FromUtf8Error) -> Self {
DidWebError::ParsingError(ParsingErrorSource::Utf8Error(error))
}
}
1 change: 1 addition & 0 deletions did-utils/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod errors;
pub mod traits;

pub mod did_key;
pub mod did_web;

mod utils;
130 changes: 130 additions & 0 deletions did-utils/tests/methods/did_web/resolution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#![allow(unused_imports)]
use did_utils::methods::{
did_web::resolver::DidWebResolver,
traits::{ DIDResolutionOptions, DIDResolver, ResolutionOutput },
};

use hyper::{ service::{ make_service_fn, service_fn }, Body, Request, Response, Server };

use serde_json::Value;
use std::convert::Infallible;
use std::net::SocketAddr;

#[allow(dead_code)]
async fn mock_server_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
const DID_JSON: &str =
r#"
{"@context": "https://www.w3.org/ns/did/v1",
"id": "did:web:localhost",
"verificationMethod": [{
"id": "did:web:localhost#key1",
"type": "Ed25519VerificationKey2018",
"controller": "did:web:localhost",
"publicKeyJwk": {
"key_id": "ed25519-2020-10-18",
"kty": "OKP",
"crv": "Ed25519",
"x": "G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww"
}
}],
"assertionMethod": ["did:web:localhost#key1"]
}"#;

let response = match req.uri().path() {
"/.well-known/did.json" | "/user/alice/did.json" => Response::new(Body::from(DID_JSON)),
_ => Response::builder().status(404).body(Body::from("Not Found")).unwrap(),
};

Ok(response)
}

#[allow(dead_code)]
async fn create_mock_server(port: u16) -> String {
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(mock_server_handler))
});

let addr = SocketAddr::from(([127, 0, 0, 1], port));
let server = Server::bind(&addr).serve(make_svc);

tokio::spawn(async move {
server.await.unwrap();
});

"localhost".to_string()
}

#[tokio::test]
async fn resolves_document() {
let port = 3000;
let host = create_mock_server(port).await;

let formatted_string = format!("did:web:{}%3A{}", host, port);

let did: &str = &formatted_string;

let did_web_resolver = DidWebResolver::http();
let output: ResolutionOutput = did_web_resolver.resolve(
did,
&DIDResolutionOptions::default()
).await;

let expected: Value = serde_json
::from_str(
r#"{
"@context": "https://www.w3.org/ns/did/v1",
"didDocument": {
"@context": "https://www.w3.org/ns/did/v1",
"assertionMethod": ["did:web:localhost#key1"],
"id": "did:web:localhost",
"verificationMethod": [
{
"controller": "did:web:localhost",
"id": "did:web:localhost#key1",
"publicKeyJwk": {
"crv": "Ed25519",
"kty": "OKP",
"x": "G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww"
},
"type": "Ed25519VerificationKey2018"
}
]
},
"didDocumentMetadata": null,
"didResolutionMetadata": {
"contentType": "application/did+ld+json"
}
}
"#
)
.unwrap();

assert_eq!(json_canon::to_string(&output).unwrap(), json_canon::to_string(&expected).unwrap());
}

use did_utils::methods::did_web::resolver;
use did_utils::methods::errors::DidWebError;

#[test]
fn test_parse_did_web_url() {
let input_1 = "did:web:w3c-ccg.github.io";
let result_1 = resolver::parse_did_web_url(input_1);
assert!(result_1.is_ok(), "Expected Ok, got {:?}", result_1);
let (path_1, domain_name_1) = result_1.unwrap();
assert_eq!(domain_name_1, "w3c-ccg.github.io");
assert_eq!(path_1, "/.well-known/did.json");

let input_2 = "did:web:w3c-ccg.github.io:user:alice";
let result_2 = resolver::parse_did_web_url(input_2);
assert!(result_2.is_ok(), "Expected Ok, got {:?}", result_2);
let (path_2, domain_name_2) = result_2.unwrap();
assert_eq!(domain_name_2, "w3c-ccg.github.io");
assert_eq!(path_2, "/user/alice/did.json");

let input_3 = "did:web:example.com%3A3000:user:alice";
let result_3 = resolver::parse_did_web_url(input_3);
assert!(result_3.is_ok(), "Expected Ok, got {:?}", result_3);
let (path_3, domain_name_3) = result_3.unwrap();
assert_eq!(domain_name_3, "example.com:3000");
assert_eq!(path_3, "/user/alice/did.json");
}
4 changes: 4 additions & 0 deletions did-utils/tests/test_methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod methods {
#[path = "did_web/resolution.rs"]
mod resolution;
}
Loading