Skip to content

Commit

Permalink
Basic implementation of did:web resolver (#828)
Browse files Browse the repository at this point in the history
Signed-off-by: Miroslav Kovar <miroslav.kovar@absa.africa>
  • Loading branch information
mirgee authored May 10, 2023
1 parent a7cc540 commit 244b8b3
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ jobs:
uses: ./.github/actions/setup-testing-rust
- name: "Run resolver tests"
run: |
RUST_TEST_THREADS=1 cargo test -p did_doc_builder -p did_parser -p did_resolver -p did_resolver_registry -p did_resolver_sov --test "*"
RUST_TEST_THREADS=1 cargo test -p did_doc_builder -p did_parser -p did_resolver -p did_resolver_registry -p did_resolver_sov -p did_resolver_web --test "*"
test-node-wrapper:
needs: workflow-setup
Expand Down
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ members = [
"did_parser",
"did_resolver",
"did_resolver_registry",
"did_resolver_sov"
"did_resolver_sov",
"did_resolver_web"
]

[workspace.package]
Expand Down
19 changes: 19 additions & 0 deletions did_resolver_web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "did_resolver_web"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
did_resolver = { path = "../did_resolver" }
async-trait = "0.1.68"
serde_json = "1.0.96"
thiserror = "1.0.40"
hyper = { version = "0.14.26", features = ["client", "http2"] }
hyper-tls = "0.5.0"

[dev-dependencies]
hyper = { version = "0.14.26", features = ["server"] }
tokio = { version = "1.27.0", default-features = false, features = ["macros", "rt"] }
tokio-test = "0.4.2"
25 changes: 25 additions & 0 deletions did_resolver_web/src/error/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
pub mod parsing;

use hyper::StatusCode;
use thiserror::Error;

use self::parsing::ParsingErrorSource;

#[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>),
}
23 changes: 23 additions & 0 deletions did_resolver_web/src/error/parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use thiserror::Error;

use super::DidWebError;

#[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))
}
}
2 changes: 2 additions & 0 deletions did_resolver_web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod error;
pub mod resolution;
1 change: 1 addition & 0 deletions did_resolver_web/src/resolution/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod resolver;
109 changes: 109 additions & 0 deletions did_resolver_web/src/resolution/resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use async_trait::async_trait;
use did_resolver::{
did_parser::Did,
error::GenericError,
shared_types::{did_document_metadata::DidDocumentMetadata, media_type::MediaType},
traits::resolvable::{
resolution_metadata::DidResolutionMetadata, resolution_options::DidResolutionOptions,
resolution_output::DidResolutionOutput, DidResolvable,
},
};
use hyper::{
client::{connect::Connect, HttpConnector},
http::uri::{self, Scheme},
Body, Client, Uri,
};
use hyper_tls::HttpsConnector;

use crate::error::DidWebError;

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())
}
}

#[async_trait]
impl<C> DidResolvable for DidWebResolver<C>
where
C: Connect + Send + Sync + Clone + 'static,
{
async fn resolve(&self, did: &Did, options: &DidResolutionOptions) -> Result<DidResolutionOutput, GenericError> {
if did.method() != "web" {
return Err(Box::new(DidWebError::MethodNotSupported(did.method().to_string())));
}

if let Some(accept) = options.accept() {
if accept != &MediaType::DidJson {
return Err(Box::new(DidWebError::RepresentationNotSupported(accept.to_string())));
}
}

let did_parts: Vec<&str> = did.id().split(':').collect();

if did_parts.is_empty() {
return Err(Box::new(DidWebError::InvalidDid(did.id().to_string())));
}

let domain = did_parts[0].replace("%3A", ":");

let path_parts = &did_parts[1..];
let path_and_query = if path_parts.is_empty() {
"/.well-known/did.json".to_string()
} else {
let path = path_parts.join("/");
format!("/{}/did.json", path)
};
let url = uri::Builder::new()
.scheme(self.scheme.clone())
.authority(domain.as_str())
.path_and_query(path_and_query.as_str())
.build()?;

let did_document = serde_json::from_str(&self.fetch_did_document(url).await?)?;

let did_resolution_output = DidResolutionOutput::builder(did_document)
.did_resolution_metadata(DidResolutionMetadata::default())
.did_document_metadata(DidDocumentMetadata::default())
.build();

Ok(did_resolution_output)
}
}
120 changes: 120 additions & 0 deletions did_resolver_web/tests/resolution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use did_resolver::did_doc_builder::schema::did_doc::DidDocument;
use did_resolver::did_parser::Did;
use did_resolver::traits::resolvable::{resolution_options::DidResolutionOptions, DidResolvable};
use did_resolver_web::resolution::resolver::DidWebResolver;
use hyper::{
service::{make_service_fn, service_fn},
Body, Request, Response, Server,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use tokio_test::assert_ok;

const DID_DOCUMENT: &'static str = r##"
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:example.com",
"verificationMethod": [
{
"id": "did:web:example.com#key-0",
"type": "JsonWebKey2020",
"controller": "did:web:example.com",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "0-e2i2_Ua1S5HbTYnVB0lj2Z2ytXu2-tYmDFf8f5NjU"
}
},
{
"id": "did:web:example.com#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:example.com",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": "9GXjPGGvmRq9F6Ng5dQQ_s31mfhxrcNZxRGONrmH30k"
}
},
{
"id": "did:web:example.com#key-2",
"type": "JsonWebKey2020",
"controller": "did:web:example.com",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "38M1FDts7Oea7urmseiugGW7tWc3mLpJh6rKe7xINZ8",
"y": "nDQW6XZ7b_u2Sy9slofYLlG03sOEoug3I0aAPQ0exs4"
}
}
],
"authentication": [
"did:web:example.com#key-0",
"did:web:example.com#key-2"
],
"assertionMethod": [
"did:web:example.com#key-0",
"did:web:example.com#key-2"
],
"keyAgreement": [
"did:web:example.com#key-1",
"did:web:example.com#key-2"
]
}"##;

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

Ok(response)
}

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 test_did_web_resolver() {
fn verify_did_document(did_document: &DidDocument) {
assert_eq!(did_document.id().to_string(), "did:web:example.com".to_string());
assert_eq!(did_document.verification_method().len(), 3);
assert_eq!(did_document.authentication().len(), 2);
assert_eq!(did_document.assertion_method().len(), 2);
assert_eq!(did_document.key_agreement().len(), 2);
}

let port = 3000;
let host = create_mock_server(port).await;

let did_web_resolver = DidWebResolver::http();

let did_example_1 = Did::parse(format!("did:web:{}%3A{}", host, port)).unwrap();
let did_example_2 = Did::parse(format!("did:web:{}%3A{}:user:alice", host, port)).unwrap();

let result_1 = assert_ok!(
did_web_resolver
.resolve(&did_example_1, &DidResolutionOptions::default())
.await
);
verify_did_document(result_1.did_document());

let result_2 = assert_ok!(
did_web_resolver
.resolve(&did_example_2, &DidResolutionOptions::default())
.await
);
verify_did_document(result_2.did_document());
}

0 comments on commit 244b8b3

Please sign in to comment.