From 164a7b85063c4ec78a7c7ed97bb8730548888806 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 21 Sep 2020 13:44:24 -0400 Subject: [PATCH 1/4] Move OneOrMany into own file --- src/bin/ssi-vc-test/main.rs | 2 +- src/lib.rs | 1 + src/one_or_many.rs | 89 +++++++++++++++++++++++++++++++++++++ src/vc.rs | 79 +------------------------------- 4 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 src/one_or_many.rs diff --git a/src/bin/ssi-vc-test/main.rs b/src/bin/ssi-vc-test/main.rs index 58cfb50dd..63021db9d 100644 --- a/src/bin/ssi-vc-test/main.rs +++ b/src/bin/ssi-vc-test/main.rs @@ -1,7 +1,7 @@ use ssi::jwk::JWTKeys; +use ssi::one_or_many::OneOrMany; use ssi::vc::Context; use ssi::vc::Credential; -use ssi::vc::OneOrMany; use ssi::vc::Presentation; fn usage() { diff --git a/src/lib.rs b/src/lib.rs index 60d5354d8..7a37a6bdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod der; pub mod did; pub mod error; pub mod jwk; +pub mod one_or_many; pub mod rdf; pub mod vc; diff --git a/src/one_or_many.rs b/src/one_or_many.rs new file mode 100644 index 000000000..20e86f7cc --- /dev/null +++ b/src/one_or_many.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum OneOrMany { + One(T), + Many(Vec), +} + +impl OneOrMany { + pub fn any(&self, f: F) -> bool + where + F: Fn(&T) -> bool, + { + match self { + Self::One(value) => f(value), + Self::Many(values) => values.iter().any(f), + } + } + + pub fn len(&self) -> usize { + match self { + Self::One(_) => 1, + Self::Many(values) => values.len(), + } + } + + pub fn contains(&self, x: &T) -> bool + where + T: PartialEq, + { + match self { + Self::One(value) => x == value, + Self::Many(values) => values.contains(x), + } + } + + pub fn first(&self) -> Option<&T> { + match self { + Self::One(value) => Some(&value), + Self::Many(values) => { + if values.len() > 0 { + Some(&values[0]) + } else { + None + } + } + } + } + + pub fn to_single(&self) -> Option<&T> { + match self { + Self::One(value) => Some(&value), + Self::Many(values) => { + if values.len() == 1 { + Some(&values[0]) + } else { + None + } + } + } + } +} + +// consuming iterator +impl IntoIterator for OneOrMany { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + Self::One(value) => vec![value].into_iter(), + Self::Many(values) => values.into_iter(), + } + } +} + +// non-consuming iterator +impl<'a, T> IntoIterator for &'a OneOrMany { + type Item = &'a T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + OneOrMany::One(value) => vec![value].into_iter(), + OneOrMany::Many(values) => values.into_iter().collect::>().into_iter(), + } + } +} diff --git a/src/vc.rs b/src/vc.rs index 7cf7daa0b..8bf861711 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -4,6 +4,7 @@ use std::convert::TryInto; use crate::error::Error; use crate::jwk::{Header, JWTKeys, JWK}; +use crate::one_or_many::OneOrMany; use crate::rdf::{ BlankNodeLabel, DataSet, IRIRef, Literal, Object, Predicate, Statement, StringLiteral, Subject, }; @@ -69,13 +70,6 @@ pub struct Credential { pub refresh_service: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum OneOrMany { - One(T), - Many(Vec), -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] #[serde(try_from = "OneOrMany")] @@ -277,77 +271,6 @@ pub struct JWTClaims { pub verifiable_presentation: Option, } -impl OneOrMany { - pub fn len(&self) -> usize { - match self { - Self::One(_) => 1, - Self::Many(values) => values.len(), - } - } - - pub fn contains(&self, x: &T) -> bool - where - T: PartialEq, - { - match self { - Self::One(value) => x == value, - Self::Many(values) => values.contains(x), - } - } - - pub fn first(&self) -> Option<&T> { - match self { - Self::One(value) => Some(&value), - Self::Many(values) => { - if values.len() > 0 { - Some(&values[0]) - } else { - None - } - } - } - } - - pub fn to_single(&self) -> Option<&T> { - match self { - Self::One(value) => Some(&value), - Self::Many(values) => { - if values.len() == 1 { - Some(&values[0]) - } else { - None - } - } - } - } -} - -// consuming iterator -impl IntoIterator for OneOrMany { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - Self::One(value) => vec![value].into_iter(), - Self::Many(values) => values.into_iter(), - } - } -} - -// non-consuming iterator -impl<'a, T> IntoIterator for &'a OneOrMany { - type Item = &'a T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - OneOrMany::One(value) => vec![value].into_iter(), - OneOrMany::Many(values) => values.into_iter().collect::>().into_iter(), - } - } -} - impl TryFrom> for Contexts { type Error = Error; fn try_from(context: OneOrMany) -> Result { From 914bf3429fe43c7c326e517b0107515c400a6f99 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 21 Sep 2020 15:54:17 -0400 Subject: [PATCH 2/4] DID updates - Update default context - Allow context used by Universal Resolver - Make Document properties public - Allow multiple contexts - Validate DID Document context property - Use crate Error type for Document builder validation - Add helper method for deserializing Document from bytes --- src/did.rs | 117 +++++++++++++++++++++++++++++++++++++++------------ src/error.rs | 8 ++++ 2 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/did.rs b/src/did.rs index 29ae3ef07..261abd2a1 100644 --- a/src/did.rs +++ b/src/did.rs @@ -1,4 +1,8 @@ use std::collections::HashMap as Map; +use std::convert::TryFrom; + +use crate::error::Error; +use crate::one_or_many::OneOrMany; use chrono::prelude::*; use serde::{Deserialize, Serialize}; @@ -13,7 +17,10 @@ use serde_json::Value; // *********************************************** // @TODO `id` must be URI -pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/2019/did/v1"; +pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/did/v1"; + +// v0.11 context used by universal resolver +pub const V0_11_CONTEXT: &str = "https://w3id.org/did/v0.11"; // @TODO parsed data structs for DID and DIDURL type DID = String; @@ -28,22 +35,32 @@ type DIDURL = String; )] pub struct Document { #[serde(rename = "@context")] - context: String, - id: DID, + pub context: Contexts, + pub id: DID, #[serde(skip_serializing_if = "Option::is_none")] - created: Option>, + pub created: Option>, #[serde(skip_serializing_if = "Option::is_none")] - updated: Option>, + pub updated: Option>, #[serde(skip_serializing_if = "Option::is_none")] - authentication: Option>, + pub authentication: Option>, #[serde(skip_serializing_if = "Option::is_none")] - service: Option>, + pub service: Option>, #[serde(skip_serializing_if = "Option::is_none")] - public_key: Option, + pub public_key: Option, #[serde(skip_serializing_if = "Option::is_none")] - controller: Option, + pub controller: Option, #[serde(skip_serializing_if = "Option::is_none")] - proof: Option, + pub proof: Option, + #[serde(flatten)] + pub property_set: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +#[serde(try_from = "OneOrMany")] +pub enum Contexts { + One(String), + Many(Vec), } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -65,15 +82,15 @@ pub enum PublicKeyEntry { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PublicKeyObject { - id: String, + pub id: String, #[serde(rename = "type")] - type_: String, + pub type_: String, // Note: different than when the DID Document is the subject: // The value of the controller property, which identifies the // controller of the corresponding private key, MUST be a valid DID. - controller: DID, + pub controller: DID, #[serde(flatten)] - property_set: Option>, + pub property_set: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -95,20 +112,20 @@ pub enum Controller { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Service { - id: String, + pub id: String, #[serde(rename = "type")] - type_: String, - service_endpoint: String, + pub type_: String, + pub service_endpoint: String, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] - property_set: Option>, + pub property_set: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Proof { #[serde(rename = "type")] - type_: String, + pub type_: String, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] pub property_set: Option>, @@ -117,7 +134,7 @@ pub struct Proof { impl Default for Document { fn default() -> Self { Document { - context: DEFAULT_CONTEXT.to_string(), + context: Contexts::One(DEFAULT_CONTEXT.to_string()), id: "".to_string(), created: None, updated: None, @@ -126,19 +143,58 @@ impl Default for Document { public_key: None, controller: None, proof: None, + property_set: None, + } + } +} + +impl TryFrom> for Contexts { + type Error = Error; + fn try_from(context: OneOrMany) -> Result { + let first_uri = match context.first() { + None => return Err(Error::MissingContext), + Some(uri) => uri, + }; + if first_uri != DEFAULT_CONTEXT && first_uri != V0_11_CONTEXT { + return Err(Error::InvalidContext); + } + Ok(match context { + OneOrMany::One(context) => Contexts::One(context), + OneOrMany::Many(contexts) => Contexts::Many(contexts), + }) + } +} + +impl From for OneOrMany { + fn from(contexts: Contexts) -> OneOrMany { + match contexts { + Contexts::One(context) => OneOrMany::One(context), + Contexts::Many(contexts) => OneOrMany::Many(contexts), } } } impl DocumentBuilder { - fn validate(&self) -> Result<(), String> { + fn validate(&self) -> Result<(), Error> { // validate is called before defaults are assigned. // None means default will be used. if self.id == None || self.id == Some("".to_string()) { - return Err("Missing document id".to_string()); + return Err(Error::MissingDocumentId); } - if self.context != None && self.context != Some(DEFAULT_CONTEXT.to_string()) { - return Err("Invalid context".to_string()); + if let Some(first_context) = match &self.context { + None => None, + Some(Contexts::One(context)) => Some(context), + Some(Contexts::Many(contexts)) => { + if contexts.len() > 0 { + Some(&contexts[0]) + } else { + None + } + } + } { + if first_context != &DEFAULT_CONTEXT && first_context != &V0_11_CONTEXT { + return Err(Error::InvalidContext); + } } Ok(()) } @@ -147,7 +203,7 @@ impl DocumentBuilder { impl Document { pub fn new(id: &str) -> Document { Document { - context: DEFAULT_CONTEXT.to_string(), + context: Contexts::One(DEFAULT_CONTEXT.to_string()), id: String::from(id), created: None, updated: None, @@ -156,12 +212,17 @@ impl Document { public_key: None, controller: None, proof: None, + property_set: None, } } pub fn from_json(json: &str) -> Result { serde_json::from_str(json) } + + pub fn from_json_bytes(json: &[u8]) -> Result { + serde_json::from_slice(json) + } } #[cfg(test)] @@ -188,7 +249,7 @@ mod tests { } #[test] - #[should_panic(expected = "Missing document id")] + #[should_panic(expected = "Missing document ID")] fn build_document_no_id() { let doc = DocumentBuilder::default().build().unwrap(); println!("{}", serde_json::to_string_pretty(&doc).unwrap()); @@ -199,7 +260,7 @@ mod tests { fn build_document_invalid_context() { let id = "did:test:deadbeefcafe"; let doc = DocumentBuilder::default() - .context("example:bad") + .context(Contexts::One("example:bad".to_string())) .id(id) .build() .unwrap(); @@ -209,7 +270,7 @@ mod tests { #[test] fn document_from_json() { let doc_str = "{\ - \"@context\": \"https://www.w3.org/2019/did/v1\",\ + \"@context\": \"https://www.w3.org/ns/did/v1\",\ \"id\": \"did:test:deadbeefcafe\"\ }"; let id = "did:test:deadbeefcafe"; diff --git a/src/error.rs b/src/error.rs index 5a55325b3..88ca11be6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,7 @@ pub enum Error { URI, InvalidContext, MissingContext, + MissingDocumentId, MissingProofSignature, ExpiredProof, FutureProof, @@ -76,6 +77,7 @@ impl fmt::Display for Error { Error::TimeError => write!(f, "Unable to convert date/time"), Error::InvalidContext => write!(f, "Invalid context"), Error::MissingContext => write!(f, "Missing context"), + Error::MissingDocumentId => write!(f, "Missing document ID"), Error::MissingProofSignature => write!(f, "Missing JWS in proof"), Error::ExpiredProof => write!(f, "Expired proof"), Error::FutureProof => write!(f, "Proof creation time is in the future"), @@ -114,3 +116,9 @@ impl From for Error { Error::JSON(err) } } + +impl From for String { + fn from(err: Error) -> String { + format!("{}", err) + } +} From 7005daf097701ef0eb5c02f7bf26680cedc627ab Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 21 Sep 2020 16:21:13 -0400 Subject: [PATCH 3/4] Add DID Resolver crate --- .github/workflows/build.yml | 14 +- did-resolve/.gitignore | 2 + did-resolve/Cargo.toml | 22 + did-resolve/src/lib.rs | 582 ++++++++++++++++++ .../tests/did-key-uniresolver-resp.json | 20 + 5 files changed, 638 insertions(+), 2 deletions(-) create mode 100644 did-resolve/.gitignore create mode 100644 did-resolve/Cargo.toml create mode 100644 did-resolve/src/lib.rs create mode 100644 did-resolve/tests/did-key-uniresolver-resp.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aafe666a6..88d7e1368 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Build + - name: Build ssi run: cargo build --verbose - - name: Test + - name: Test ssi run: cargo test --verbose - name: Test vc-test-suite @@ -31,3 +31,13 @@ jobs: npm i cp ../ssi/src/bin/ssi-vc-test/config.json . npm test + + - name: Build did-resolve + run: | + cd did-resolve + cargo build --verbose + + - name: Test did-resolve + run: | + cd did-resolve + cargo test --verbose diff --git a/did-resolve/.gitignore b/did-resolve/.gitignore new file mode 100644 index 000000000..96ef6c0b9 --- /dev/null +++ b/did-resolve/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/did-resolve/Cargo.toml b/did-resolve/Cargo.toml new file mode 100644 index 000000000..6a924e041 --- /dev/null +++ b/did-resolve/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ssi-did-resolve" +version = "0.1.0" +authors = ["Charles E. Lehner "] +edition = "2018" + +[dependencies] +ssi = "0.1" +chrono = { version = "0.4", features = ["serde"] } +tokio = { version = "0.2", features = ["macros", "stream"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +bytes = "0.5" +hyper = "0.13" +hyper-tls = "0.4" +http = "0.2" +async-trait = "0.1" +percent-encoding = "2.1" +serde_urlencoded = "0.7" + +[patch.crates-io] +ssi = { path = "../" } diff --git a/did-resolve/src/lib.rs b/did-resolve/src/lib.rs new file mode 100644 index 000000000..ec009129e --- /dev/null +++ b/did-resolve/src/lib.rs @@ -0,0 +1,582 @@ +use async_trait::async_trait; +use bytes::Bytes; +use chrono::prelude::{DateTime, Utc}; +use hyper::{header, Client, Request, StatusCode, Uri}; +use hyper_tls::HttpsConnector; +use serde::{Deserialize, Serialize}; +use serde_json; +use serde_urlencoded; +use std::collections::HashMap; +use tokio::stream::StreamExt; +use tokio::stream::{self, Stream}; + +// https://w3c-ccg.github.io/did-resolution/ + +use ssi::did::Document; + +pub const TYPE_DID_LD_JSON: &str = "application/did+ld+json"; +pub const ERROR_INVALID_DID: &str = "invalid-did"; +pub const ERROR_UNAUTHORIZED: &str = "unauthorized"; +pub const ERROR_NOT_FOUND: &str = "not-found"; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum Metadata { + String(String), + Map(HashMap), + List(Vec), + Boolean(bool), + Null, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ResolutionInputMetadata { + accept: Option, + #[serde(flatten)] + property_set: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ResolutionMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "content-type")] + content_type: Option, + #[serde(flatten)] + property_set: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DocumentMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + created: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + updated: Option>, + #[serde(flatten)] + property_set: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ResolutionResult { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "didDocument")] + document: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "didResolutionMetadata")] + resolution_metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "didDocumentMetadata")] + document_metadata: Option, + #[serde(flatten)] + property_set: Option>, +} + +#[async_trait] +pub trait DIDResolver { + async fn resolve( + &self, + did: &str, + input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ); + async fn resolve_stream( + &self, + did: &str, + input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Box> + Unpin + Send>, + Option, + ); +} + +pub struct HTTPDIDResolver { + pub endpoint: String, +} + +#[async_trait] +impl DIDResolver for HTTPDIDResolver { + // https://w3c-ccg.github.io/did-resolution/#bindings-https + async fn resolve( + &self, + did: &str, + input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + let querystring = match serde_urlencoded::to_string(input_metadata) { + Ok(qs) => qs, + Err(err) => { + return ( + ResolutionMetadata { + error: Some( + "Unable to serialize input metadata into query string: ".to_string() + + &err.to_string(), + ), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; + let did_urlencoded = + percent_encoding::utf8_percent_encode(&did, percent_encoding::CONTROLS).to_string(); + let url = self.endpoint.clone() + &did_urlencoded + "?" + &querystring; + let uri: Uri = match url.parse() { + Ok(uri) => uri, + Err(_) => { + return ( + ResolutionMetadata { + error: Some(ERROR_INVALID_DID.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; + let https = HttpsConnector::new(); + let client = Client::builder().build::<_, hyper::Body>(https); + let request = match Request::get(uri) + .header("Accept", "application/json") + .body(hyper::Body::default()) + { + Ok(req) => req, + Err(err) => { + return ( + ResolutionMetadata { + error: Some("Error building HTTP request: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; + let mut resp = match client.request(request).await { + Ok(resp) => resp, + Err(err) => { + return ( + ResolutionMetadata { + error: Some("HTTP Error: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; + let bytes = match resp + .body_mut() + .collect::>() + .await + { + Ok(bytes) => bytes, + Err(err) => { + return ( + ResolutionMetadata { + error: Some("Error reading HTTP response: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; + let result: ResolutionResult = match serde_json::from_slice(&bytes) { + Ok(result) => result, + Err(err) => ResolutionResult { + document: None, + resolution_metadata: Some(ResolutionMetadata { + error: Some("JSON Error: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }), + document_metadata: None, + property_set: None, + }, + }; + let mut res_meta = result.resolution_metadata.unwrap_or(ResolutionMetadata { + error: None, + content_type: None, + property_set: None, + }); + if resp.status() == StatusCode::NOT_FOUND { + res_meta.error = Some(ERROR_NOT_FOUND.to_string()); + } + if let Some(content_type) = resp.headers().get(header::CONTENT_TYPE) { + res_meta.content_type = Some(String::from(match content_type.to_str() { + Ok(content_type) => content_type, + Err(err) => { + return ( + ResolutionMetadata { + error: Some( + "Error reading HTTP header: ".to_string() + &err.to_string(), + ), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + })); + }; + (res_meta, result.document, result.document_metadata) + } + + async fn resolve_stream( + &self, + did: &str, + input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Box> + Unpin + Send>, + Option, + ) { + // Implement resolveStream in terms of resolve, + // until resolveStream has its own HTTP(S) binding: + // https://github.com/w3c-ccg/did-resolution/issues/57 + let (mut res_meta, doc, doc_meta) = self.resolve(did, input_metadata).await; + let stream: Box> + Unpin + Send> = match doc { + None => Box::new(stream::empty()), + Some(doc) => match serde_json::to_vec_pretty(&doc) { + Ok(bytes) => Box::new(stream::iter(vec![Ok(Bytes::from(bytes))])), + Err(err) => { + res_meta.error = + Some("Error serializing JSON: ".to_string() + &err.to_string()); + Box::new(stream::empty()) + } + }, + }; + (res_meta, stream, doc_meta) + } +} + +#[cfg(test)] +mod tests { + use hyper::{Body, Response, Server}; + // use std::future::Future; + use serde_json::Value; + use tokio::stream; + + use super::*; + + struct ExampleResolver {} + + const EXAMPLE_123_ID: &str = "did:example:123"; + const EXAMPLE_123_JSON: &str = r#"{ + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:example:123", + "authentication": [ + { + "id": "did:example:123#z6MkecaLyHuYWkayBDLw5ihndj3T1m6zKTGqau3A51G7RBf3", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123", + "publicKeyBase58": "AKJP3f7BD6W4iWEQ9jwndVTCBq8ua2Utt8EEjJ6Vxsf" + } + ], + "capabilityInvocation": [ + { + "id": "did:example:123#z6MkhdmzFu659ZJ4XKj31vtEDmjvsi5yDZG5L7Caz63oP39k", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123", + "publicKeyBase58": "4BWwfeqdp1obQptLLMvPNgBw48p7og1ie6Hf9p5nTpNN" + } + ], + "capabilityDelegation": [ + { + "id": "did:example:123#z6Mkw94ByR26zMSkNdCUi6FNRsWnc2DFEeDXyBGJ5KTzSWyi", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123", + "publicKeyBase58": "Hgo9PAmfeoxHG8Mn2XHXamxnnSwPpkyBHAMNF3VyXJCL" + } + ], + "assertionMethod": [ + { + "id": "did:example:123#z6MkiukuAuQAE8ozxvmahnQGzApvtW7KT5XXKfojjwbdEomY", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123", + "publicKeyBase58": "5TVraf9itbKXrRvt2DSS95Gw4vqU3CHAdetoufdcKazA" + } + ] + }"#; + const DID_KEY_ID: &'static str = "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6"; + const DID_KEY_TYPE: &'static str = + "application/ld+json;profile=\"https://w3id.org/did-resolution\";charset=utf-8"; + const DID_KEY_JSON: &'static str = include_str!("../tests/did-key-uniresolver-resp.json"); + + #[async_trait] + impl DIDResolver for ExampleResolver { + async fn resolve( + &self, + did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + if did == EXAMPLE_123_ID { + let doc = match Document::from_json(EXAMPLE_123_JSON) { + Ok(doc) => doc, + Err(err) => { + return ( + ResolutionMetadata { + // https://github.com/w3c/did-core/issues/402 + error: Some("JSON Error: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ); + } + }; + ( + ResolutionMetadata { + error: None, + content_type: None, + property_set: None, + }, + Some(doc), + Some(DocumentMetadata { + created: None, + updated: None, + property_set: None, + }), + ) + } else { + ( + ResolutionMetadata { + error: Some(ERROR_NOT_FOUND.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + } + + async fn resolve_stream( + &self, + did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Box> + Unpin + Send>, + Option, + ) { + if did == EXAMPLE_123_ID { + let bytes = Bytes::from_static(EXAMPLE_123_JSON.as_bytes()); + ( + ResolutionMetadata { + error: None, + content_type: Some(TYPE_DID_LD_JSON.to_string()), + property_set: None, + }, + Box::new(stream::iter(vec![Ok(bytes)])), + Some(DocumentMetadata { + created: None, + updated: None, + property_set: None, + }), + ) + } else { + ( + ResolutionMetadata { + error: Some(ERROR_NOT_FOUND.to_string()), + content_type: None, + property_set: None, + }, + Box::new(stream::empty()), + None, + ) + } + } + } + + #[tokio::test] + async fn resolve() { + let resolver = ExampleResolver {}; + let (res_meta, doc, doc_meta) = resolver + .resolve( + EXAMPLE_123_ID, + &ResolutionInputMetadata { + accept: None, + property_set: None, + }, + ) + .await; + assert_eq!(res_meta.error, None); + assert!(doc_meta.is_some()); + let doc = doc.unwrap(); + assert_eq!(doc.id, EXAMPLE_123_ID); + } + + #[tokio::test] + async fn resolve_stream() { + let resolver = ExampleResolver {}; + let (res_meta, stream, doc_meta) = resolver + .resolve_stream( + EXAMPLE_123_ID, + &ResolutionInputMetadata { + accept: None, + property_set: None, + }, + ) + .await; + assert_eq!(res_meta.error, None); + assert!(doc_meta.is_some()); + let bytes = stream + .collect::>() + .await + .unwrap(); + assert_eq!(bytes, EXAMPLE_123_JSON); + } + + fn did_resolver_server() -> Result<(String, impl FnOnce() -> Result<(), ()>), hyper::Error> { + // @TODO: + // - handle errors instead of using unwrap + // - handle `accept` input metadata property + use hyper::service::{make_service_fn, service_fn}; + let addr = ([127, 0, 0, 1], 0).into(); + let make_svc = make_service_fn(|_| async { + Ok::<_, hyper::Error>(service_fn(|req| async move { + let uri = req.uri(); + // skip root "/" to get DID + let id: String = uri.path().chars().skip(1).collect(); + let res_input_meta: ResolutionInputMetadata = + serde_urlencoded::from_str(uri.query().unwrap_or("")).unwrap(); + + // fixture response from universal-resolver + if id == DID_KEY_ID { + let body = Body::from(DID_KEY_JSON); + let mut response = Response::new(body); + response + .headers_mut() + .insert(header::CONTENT_TYPE, DID_KEY_TYPE.parse().unwrap()); + return Ok::<_, hyper::Error>(response); + } + + // wrap ExampleResolver in a local HTTP server + let resolver = ExampleResolver {}; + let (res_meta, doc_opt, doc_meta_opt) = + resolver.resolve(&id, &res_input_meta).await; + let (mut parts, _) = Response::::default().into_parts(); + if res_meta.error == Some(ERROR_NOT_FOUND.to_string()) { + parts.status = StatusCode::NOT_FOUND; + } + if let Some(ref content_type) = res_meta.content_type { + parts + .headers + .insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + } + let result = ResolutionResult { + document: doc_opt, + resolution_metadata: Some(res_meta), + document_metadata: doc_meta_opt, + property_set: None, + }; + let body = Body::from(serde_json::to_vec_pretty(&result).unwrap()); + Ok::<_, hyper::Error>(Response::from_parts(parts, body)) + })) + }); + let server = Server::try_bind(&addr)?.serve(make_svc); + let url = "http://".to_string() + &server.local_addr().to_string() + "/"; + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + let graceful = server.with_graceful_shutdown(async { + shutdown_rx.await.ok(); + }); + tokio::task::spawn(async move { + graceful.await.ok(); + }); + let shutdown = || shutdown_tx.send(()); + Ok((url, shutdown)) + } + + #[tokio::test] + async fn http_resolve_stream() { + let (endpoint, shutdown) = did_resolver_server().unwrap(); + let resolver = HTTPDIDResolver { endpoint }; + let (res_meta, stream, doc_meta) = resolver + .resolve_stream( + EXAMPLE_123_ID, + &ResolutionInputMetadata { + accept: None, + property_set: None, + }, + ) + .await; + assert_eq!(res_meta.error, None); + assert!(doc_meta.is_some()); + let bytes = stream + .collect::>() + .await + .unwrap(); + let doc: Value = serde_json::from_slice(&bytes).unwrap(); + let doc_expected: Value = serde_json::from_str(&EXAMPLE_123_JSON).unwrap(); + assert_eq!(doc, doc_expected); + shutdown().ok(); + } + + #[tokio::test] + async fn http_resolve() { + let (endpoint, shutdown) = did_resolver_server().unwrap(); + let resolver = HTTPDIDResolver { endpoint }; + let (res_meta, doc, doc_meta) = resolver + .resolve( + EXAMPLE_123_ID, + &ResolutionInputMetadata { + accept: None, + property_set: None, + }, + ) + .await; + assert_eq!(res_meta.error, None); + assert!(doc_meta.is_some()); + let doc = doc.unwrap(); + assert_eq!(doc.id, EXAMPLE_123_ID); + shutdown().ok(); + } + + #[tokio::test] + async fn resolve_uniresolver_fixture() { + let id = DID_KEY_ID; + let (endpoint, shutdown) = did_resolver_server().unwrap(); + let resolver = HTTPDIDResolver { endpoint }; + let (res_meta, doc, doc_meta) = resolver + .resolve( + &id, + &ResolutionInputMetadata { + accept: None, + property_set: None, + }, + ) + .await; + eprintln!("res_meta = {:?}", &res_meta); + eprintln!("doc_meta = {:?}", &doc_meta); + eprintln!("doc = {:?}", &doc); + assert_eq!(res_meta.error, None); + let doc = doc.unwrap(); + assert_eq!(doc.id, id); + shutdown().ok(); + } +} diff --git a/did-resolve/tests/did-key-uniresolver-resp.json b/did-resolve/tests/did-key-uniresolver-resp.json new file mode 100644 index 000000000..db08009c3 --- /dev/null +++ b/did-resolve/tests/did-key-uniresolver-resp.json @@ -0,0 +1,20 @@ +{"didDocument":{ + "@context" : [ "https://w3id.org/did/v0.11" ], + "id" : "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", + "assertionMethod" : [ "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" ], + "authentication" : [ "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" ], + "capabilityDelegation" : [ "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" ], + "capabilityInvocation" : [ "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6" ], + "keyAgreement" : [ { + "id" : "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6LSbgq3GejX88eiAYWmZ9EiddS3GaXodvm8MJJyEH7bqXgz", + "type" : "X25519KeyAgreementKey2019", + "controller" : "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", + "publicKeyBase58" : "1eskLvf2fvy5A912VimK3DZRRzgwKayUKbHjpU589vE" + } ], + "publicKey" : [ { + "id" : "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6#z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", + "type" : "Ed25519VerificationKey2018", + "controller" : "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", + "publicKeyBase58" : "2QTnR7atrFu3Y7S6Xmmr4hTsMaL1KDh6Mpe9MgnJugbi" + } ] +},"content":null,"contentType":null,"resolverMetadata":{"duration":11,"identifier":"did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6","driverId":"driver-universalresolver/driver-did-key","didUrl":{"didUrlString":"did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6","did":{"didString":"did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6","method":"key","methodSpecificId":"z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6","parseTree":null,"parseRuleCount":null},"parameters":null,"parametersMap":{},"path":"","query":null,"fragment":null,"parseTree":null,"parseRuleCount":null}},"methodMetadata":{}} From 4a15f07665f5bdfc4bf978b0863a7df61b525461 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Thu, 24 Sep 2020 10:36:54 -0400 Subject: [PATCH 4/4] Use Workspace --- .github/workflows/build.yml | 18 ++++-------------- Cargo.toml | 5 +++++ did-resolve/.gitignore | 2 -- did-resolve/Cargo.toml | 5 +---- 4 files changed, 10 insertions(+), 20 deletions(-) delete mode 100644 did-resolve/.gitignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88d7e1368..86e8cdb24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,11 +17,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Build ssi - run: cargo build --verbose + - name: Build + run: cargo build --verbose --workspace - - name: Test ssi - run: cargo test --verbose + - name: Test + run: cargo test --verbose --workspace - name: Test vc-test-suite run: | @@ -31,13 +31,3 @@ jobs: npm i cp ../ssi/src/bin/ssi-vc-test/config.json . npm test - - - name: Build did-resolve - run: | - cd did-resolve - cargo build --verbose - - - name: Test did-resolve - run: | - cd did-resolve - cargo test --verbose diff --git a/Cargo.toml b/Cargo.toml index 34a20aa06..2af9c5aa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,8 @@ derive_builder = "0.9" base64 = "0.12" jsonwebtoken = "7" ring = "0.16" + +[workspace] +members = [ + "did-resolve", +] diff --git a/did-resolve/.gitignore b/did-resolve/.gitignore deleted file mode 100644 index 96ef6c0b9..000000000 --- a/did-resolve/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/did-resolve/Cargo.toml b/did-resolve/Cargo.toml index 6a924e041..20f2d0ca1 100644 --- a/did-resolve/Cargo.toml +++ b/did-resolve/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Charles E. Lehner "] edition = "2018" [dependencies] -ssi = "0.1" +ssi = { path = "../" } chrono = { version = "0.4", features = ["serde"] } tokio = { version = "0.2", features = ["macros", "stream"] } serde = { version = "1.0", features = ["derive"] } @@ -17,6 +17,3 @@ http = "0.2" async-trait = "0.1" percent-encoding = "2.1" serde_urlencoded = "0.7" - -[patch.crates-io] -ssi = { path = "../" }