diff --git a/Cargo.toml b/Cargo.toml index 13190fbd..6c747ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ members = [ "trustchain-cli", "trustchain-ffi" ] +resolver = "2" diff --git a/trustchain-api/Cargo.toml b/trustchain-api/Cargo.toml index efdccd78..46d332ff 100644 --- a/trustchain-api/Cargo.toml +++ b/trustchain-api/Cargo.toml @@ -11,8 +11,8 @@ trustchain-ion = { path = "../trustchain-ion" } async-trait = "0.1" serde_json = "1.0" -ssi = "0.4" -did-ion = "0.1.0" +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} +did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} futures = "0.3.28" [dev-dependencies] diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 1cba78be..601b0706 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -4,6 +4,7 @@ use did_ion::sidetree::DocumentState; use futures::{stream, StreamExt, TryStreamExt}; use ssi::{ did_resolve::DIDResolver, + jsonld::ContextLoader, ldp::LinkedDataDocument, vc::{Credential, CredentialOrJWT, URI}, vc::{LinkedDataProofOptions, Presentation}, @@ -92,11 +93,18 @@ pub trait TrustchainVCAPI { linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, + context_loader: &mut ContextLoader, ) -> Result { credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string()))); let attestor = IONAttestor::new(did); attestor - .sign(&credential, linked_data_proof_options, key_id, resolver) + .sign( + &credential, + linked_data_proof_options, + key_id, + resolver, + context_loader, + ) .await } @@ -106,6 +114,7 @@ pub trait TrustchainVCAPI { linked_data_proof_options: Option, root_event_time: Timestamp, verifier: &U, + context_loader: &mut ContextLoader, ) -> Result where T: DIDResolver + Send, @@ -113,7 +122,11 @@ pub trait TrustchainVCAPI { { // Verify signature let result = credential - .verify(linked_data_proof_options, verifier.resolver()) + .verify( + linked_data_proof_options, + verifier.resolver(), + context_loader, + ) .await; if !result.errors.is_empty() { return Err(CredentialError::VerificationResultError(result)); @@ -135,11 +148,18 @@ pub trait TrustchainVPAPI { key_id: Option<&str>, endpoint: &str, linked_data_proof_options: Option, + context_loader: &mut ContextLoader, ) -> Result { let resolver = get_ion_resolver(endpoint); let attestor = IONAttestor::new(did); Ok(attestor - .sign_presentation(&presentation, linked_data_proof_options, key_id, &resolver) + .sign_presentation( + &presentation, + linked_data_proof_options, + key_id, + &resolver, + context_loader, + ) .await?) } /// Verifies a verifiable presentation. @@ -148,6 +168,7 @@ pub trait TrustchainVPAPI { ldp_options: Option, root_event_time: Timestamp, verifier: &IONVerifier, + context_loader: &mut ContextLoader, ) -> Result<(), PresentationError> { // Check credentials are present in presentation let credentials = presentation @@ -158,50 +179,59 @@ pub trait TrustchainVPAPI { // Verify signatures and issuers for each credential included in the presentation // TODO: consider concurrency limit (as rate limiting for verifier requests) let limit = Some(5); - let ldp_options_vec: Vec> = (0..credentials.len()) - .map(|_| ldp_options.clone()) + let ldp_opts_and_context_loader: Vec<(Option, ContextLoader)> = (0 + ..credentials.len()) + .map(|_| (ldp_options.clone(), context_loader.clone())) .collect(); - stream::iter(credentials.into_iter().zip(ldp_options_vec)) + stream::iter(credentials.into_iter().zip(ldp_opts_and_context_loader)) .map(Ok) - .try_for_each_concurrent(limit, |(credential_or_jwt, ldp_options)| async move { - match credential_or_jwt { - CredentialOrJWT::Credential(credential) => TrustchainAPI::verify_credential( - credential, - ldp_options, - root_event_time, - verifier, - ) - .await - .map(|_| ()), - CredentialOrJWT::JWT(jwt) => { - // decode and verify for credential jwts - match Credential::decode_verify_jwt( - jwt, - ldp_options.clone(), - verifier.resolver(), - ) - .await - .0 - .ok_or(CredentialError::FailedToDecodeJWT) - { - Ok(credential) => TrustchainAPI::verify_credential( - &credential, - ldp_options, + .try_for_each_concurrent( + limit, + |(credential_or_jwt, (ldp_opts, mut context_loader))| async move { + match credential_or_jwt { + CredentialOrJWT::Credential(credential) => { + TrustchainAPI::verify_credential( + credential, + ldp_opts, root_event_time, verifier, + &mut context_loader, + ) + .await + .map(|_| ()) + } + CredentialOrJWT::JWT(jwt) => { + // decode and verify for credential jwts + match Credential::decode_verify_jwt( + jwt, + ldp_opts.clone(), + verifier.resolver(), + &mut context_loader, ) .await - .map(|_| ()), - Err(e) => Err(e), + .0 + .ok_or(CredentialError::FailedToDecodeJWT) + { + Ok(credential) => TrustchainAPI::verify_credential( + &credential, + ldp_opts, + root_event_time, + verifier, + &mut context_loader, + ) + .await + .map(|_| ()), + Err(e) => Err(e), + } } } - } - }) + }, + ) .await?; // Verify signature by holder to authenticate let result = presentation - .verify(ldp_options.clone(), verifier.resolver()) + .verify(ldp_options.clone(), verifier.resolver(), context_loader) .await; if !result.errors.is_empty() { return Err(PresentationError::VerifiedHolderUnauthenticated(result)); @@ -214,7 +244,8 @@ pub trait TrustchainVPAPI { mod tests { use crate::api::{TrustchainVCAPI, TrustchainVPAPI}; use crate::TrustchainAPI; - use ssi::ldp::now_ms; + use ssi::jsonld::ContextLoader; + use ssi::ldp::now_ns; use ssi::one_or_many::OneOrMany; use ssi::vc::{Credential, CredentialOrJWT, Presentation, VCDateTime}; use trustchain_core::utils::init; @@ -262,17 +293,19 @@ mod tests { let issuer = IONAttestor::new(issuer_did); let mut vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); + let mut context_loader = ContextLoader::default(); let res = TrustchainAPI::verify_credential( &vc_with_proof, None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), + &mut context_loader, ) .await; assert!(res.is_ok()); // Change credential to make signature invalid - vc_with_proof.expiration_date = Some(VCDateTime::try_from(now_ms()).unwrap()); + vc_with_proof.expiration_date = Some(VCDateTime::try_from(now_ns()).unwrap()); // Verify: expect no warnings and a signature error as VC has changed let resolver = get_ion_resolver("http://localhost:3000/"); @@ -281,6 +314,7 @@ mod tests { None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), + &mut context_loader, ) .await; if let CredentialError::VerificationResultError(ver_res) = res.err().unwrap() { @@ -302,6 +336,7 @@ mod tests { let vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); + let mut context_loader = ContextLoader::default(); // let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); // let root_plus_1_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0","d":"HbjLQf4tnwJR6861-91oGpERu8vmxDpW8ZroDCkmFvY"}"#; @@ -349,7 +384,7 @@ mod tests { }; presentation = holder - .sign_presentation(&presentation, None, None, &resolver) + .sign_presentation(&presentation, None, None, &resolver, &mut context_loader) .await .unwrap(); println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); @@ -358,6 +393,7 @@ mod tests { None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), + &mut context_loader, ) .await; println!("{:?}", res); @@ -388,6 +424,7 @@ mod tests { None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), + &mut ContextLoader::default() ) .await, Err(PresentationError::VerifiedHolderUnauthenticated(..)) @@ -398,6 +435,9 @@ mod tests { async fn signed_credential(attestor: IONAttestor) -> Credential { let resolver = get_ion_resolver("http://localhost:3000/"); let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); - attestor.sign(&vc, None, None, &resolver).await.unwrap() + attestor + .sign(&vc, None, None, &resolver, &mut ContextLoader::default()) + .await + .unwrap() } } diff --git a/trustchain-cli/Cargo.toml b/trustchain-cli/Cargo.toml index 7fdf6916..9f352bd7 100644 --- a/trustchain-cli/Cargo.toml +++ b/trustchain-cli/Cargo.toml @@ -14,10 +14,10 @@ trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } clap = { version = "4.0.32", features=["derive", "cargo"] } -did-ion = "0.1.0" +did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} lazy_static="1.4.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -ssi = "0.4" +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} tokio = {version = "1.20.1", features = ["full"]} toml="0.7.2" diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index dade67e5..0dc4868c 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -1,7 +1,7 @@ //! Trustchain CLI binary use clap::{arg, ArgAction, Command}; use serde_json::to_string_pretty; -use ssi::{ldp::LinkedDataDocument, vc::Credential}; +use ssi::{jsonld::ContextLoader, ldp::LinkedDataDocument, vc::Credential}; use std::{ fs::File, io::{stdin, BufReader}, @@ -87,6 +87,7 @@ async fn main() -> Result<(), Box> { let endpoint = cli_config().ion_endpoint.to_address(); let verifier = IONVerifier::new(get_ion_resolver(&endpoint)); let resolver = verifier.resolver(); + let mut context_loader = ContextLoader::default(); match matches.subcommand() { Some(("did", sub_matches)) => { match sub_matches.subcommand() { @@ -171,10 +172,16 @@ async fn main() -> Result<(), Box> { serde_json::from_reader(buffer).unwrap() }; - let credential_with_proof = - TrustchainAPI::sign(credential, did, None, key_id, resolver) - .await - .expect("Failed to issue credential."); + let credential_with_proof = TrustchainAPI::sign( + credential, + did, + None, + key_id, + resolver, + &mut context_loader, + ) + .await + .expect("Failed to issue credential."); println!("{}", &to_string_pretty(&credential_with_proof).unwrap()); } Some(("verify", sub_matches)) => { @@ -197,6 +204,7 @@ async fn main() -> Result<(), Box> { None, root_event_time, &verifier, + &mut context_loader, ) .await; // Handle result diff --git a/trustchain-core/Cargo.toml b/trustchain-core/Cargo.toml index 8c5f6333..c20aed77 100644 --- a/trustchain-core/Cargo.toml +++ b/trustchain-core/Cargo.toml @@ -10,13 +10,14 @@ async-trait = "0.1" base64 = "0.13" canonical_json = "0.4.0" chrono = "0.4" +did-method-key = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} futures = "0.3.21" petgraph = {version = "0.6"} serde = { version = "1.0", features = ["derive"] } serde_jcs = "0.1.0" serde_json = "1.0" sha2 = "0.10.7" -ssi = { version = "0.4", features = ["http-did", "secp256k1"] } +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} tempfile = { version = "3.3" } thiserror = "1.0" tokio = {version = "1.20.1", features = ["full"]} diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index 2f78b15b..70ab9269 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -3,15 +3,19 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; +use ssi::jsonld::ContextLoader; use ssi::vc::{LinkedDataProofOptions, Presentation}; use thiserror::Error; /// An error relating to a Trustchain holder. #[derive(Error, Debug)] pub enum HolderError { - /// Wrapped error for SSI error. - #[error("A wrapped variant for an SSI error: {0}")] - SSI(ssi::error::Error), + /// Wrapped error for ssi-vc error. + #[error("A wrapped variant for an SSI VC error: {0}")] + VC(ssi::vc::Error), + /// Wrapped error for ssi-ldp error. + #[error("A wrapped variant for an SSI LDP error: {0}")] + LDP(ssi::ldp::Error), /// Wrapped error for key manager error. #[error("A wrapped variant for a key manager error: {0}")] KeyManager(KeyManagerError), @@ -20,9 +24,15 @@ pub enum HolderError { MismatchedHolder, } -impl From for HolderError { - fn from(err: ssi::error::Error) -> Self { - HolderError::SSI(err) +impl From for HolderError { + fn from(err: ssi::vc::Error) -> Self { + HolderError::VC(err) + } +} + +impl From for HolderError { + fn from(err: ssi::ldp::Error) -> Self { + HolderError::LDP(err) } } @@ -44,5 +54,6 @@ pub trait Holder: Subject { linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, + context_loader: &mut ContextLoader, ) -> Result; } diff --git a/trustchain-core/src/issuer.rs b/trustchain-core/src/issuer.rs index 20c8d967..21811d89 100644 --- a/trustchain-core/src/issuer.rs +++ b/trustchain-core/src/issuer.rs @@ -3,23 +3,33 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; +use ssi::jsonld::ContextLoader; use ssi::vc::{Credential, LinkedDataProofOptions}; use thiserror::Error; /// An error relating to a Trustchain Issuer. #[derive(Error, Debug)] pub enum IssuerError { - /// Wrapped error for SSI error. - #[error("A wrapped variant for an SSI error: {0}")] - SSI(ssi::error::Error), + /// Wrapped error for ssi-vc error. + #[error("A wrapped variant for an SSI VC error: {0}")] + VC(ssi::vc::Error), + /// Wrapped error for ssi-ldp error. + #[error("A wrapped variant for an SSI LDP error: {0}")] + LDP(ssi::ldp::Error), /// Wrapped error for key manager error. #[error("A wrapped variant for a key manager error: {0}")] KeyManager(KeyManagerError), } -impl From for IssuerError { - fn from(err: ssi::error::Error) -> Self { - IssuerError::SSI(err) +impl From for IssuerError { + fn from(err: ssi::vc::Error) -> Self { + IssuerError::VC(err) + } +} + +impl From for IssuerError { + fn from(err: ssi::ldp::Error) -> Self { + IssuerError::LDP(err) } } @@ -39,5 +49,6 @@ pub trait Issuer: Subject { linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, + context_loader: &mut ContextLoader, ) -> Result; } diff --git a/trustchain-core/src/resolver.rs b/trustchain-core/src/resolver.rs index 8544e183..a49f55ba 100644 --- a/trustchain-core/src/resolver.rs +++ b/trustchain-core/src/resolver.rs @@ -1,5 +1,6 @@ //! DID resolution and `DIDResolver` implementation. use async_trait::async_trait; +use did_method_key::DIDKey; use serde_json::Value; use ssi::did::{DIDMethod, Document, Service, ServiceEndpoint}; use ssi::did_resolve::{ @@ -108,6 +109,13 @@ impl DIDResolver for Resolver { Option, Option, ) { + // TODO: remove upon handling with DIDMethods + if did.starts_with("did:key:") { + let did_key_resolver = DIDKey; + return did_key_resolver + .resolve(did, &ResolutionInputMetadata::default()) + .await; + } // Consider using ResolutionInputMetadata to optionally not perform transform. // Resolve with the wrapped DIDResolver and then transform to Trustchain format. self.transform(self.wrapped_resolver.resolve(did, input_metadata).await) diff --git a/trustchain-core/src/utils.rs b/trustchain-core/src/utils.rs index 1881c841..872b0298 100644 --- a/trustchain-core/src/utils.rs +++ b/trustchain-core/src/utils.rs @@ -144,12 +144,12 @@ pub fn hash(data: &str) -> String { } /// Extracts payload from JWT and verifies signature. -pub fn decode_verify(jwt: &str, key: &JWK) -> Result { +pub fn decode_verify(jwt: &str, key: &JWK) -> Result { ssi::jwt::decode_verify(jwt, key) } /// Extracts and decodes the payload from the JWT. -pub fn decode(jwt: &str) -> Result { +pub fn decode(jwt: &str) -> Result { ssi::jwt::decode_unverified(jwt) } diff --git a/trustchain-core/src/verifier.rs b/trustchain-core/src/verifier.rs index 166fdf21..a985558c 100644 --- a/trustchain-core/src/verifier.rs +++ b/trustchain-core/src/verifier.rs @@ -133,13 +133,13 @@ pub enum VerifierError { #[error("A commitment error during verification: {0}")] CommitmentFailure(CommitmentError), /// Wrapped resolver error. - #[error("A resolver error during verification.")] + #[error("A resolver error during verification: {0}")] ResolverFailure(ResolverError), /// Wrapped chain error. - #[error("A chain error during verification.")] + #[error("A chain error during verification: {0}")] ChainFailure(ChainError), /// Wrapped serde JSON deserialization error. - #[error("Failed to deserialize.")] + #[error("Failed to deserialize: {0}")] FailedToDeserialize(serde_json::Error), } diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index 8b3861bf..1e0316d9 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -21,6 +21,12 @@ pub enum PresentationError { /// Wrapped variant for Verifier Error. #[error("A wrapped Verifier error: {0}")] VerifierError(VerifierError), + /// Wrapped error for ssi-vc error. + #[error("A wrapped variant for an SSI VC error: {0}")] + VC(ssi::vc::Error), + /// Wrapped error for ssi-ldp error. + #[error("A wrapped variant for an SSI LDP error: {0}")] + LDP(ssi::ldp::Error), /// Credentials verified, but holder failed to authenticate with invalid or missing presentation /// proof. #[error("Credentials verified for an unauthenticated holder: {0:?}")] @@ -44,3 +50,15 @@ impl From for PresentationError { PresentationError::VerifierError(err) } } + +impl From for PresentationError { + fn from(err: ssi::vc::Error) -> Self { + PresentationError::VC(err) + } +} + +impl From for PresentationError { + fn from(err: ssi::ldp::Error) -> Self { + PresentationError::LDP(err) + } +} diff --git a/trustchain-ffi/Cargo.toml b/trustchain-ffi/Cargo.toml index 451a8f39..278ed386 100644 --- a/trustchain-ffi/Cargo.toml +++ b/trustchain-ffi/Cargo.toml @@ -15,12 +15,14 @@ trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } anyhow = "1.0" -did-ion="0.1.0" -flutter_rust_bridge = "1" +chrono = "0.4.26" +did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} +# Fixed to same version used to generate bridge: `flutter_rust_bridge_codegen@1.64.0` +flutter_rust_bridge = "=1.64.0" lazy_static="1.4.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -ssi = "0.4" +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} thiserror="1.0" tokio = { version = "1.14.0", features = ["rt-multi-thread"] } toml="0.7.2" diff --git a/trustchain-ffi/README.md b/trustchain-ffi/README.md new file mode 100644 index 00000000..68592b05 --- /dev/null +++ b/trustchain-ffi/README.md @@ -0,0 +1,3 @@ +# Trustchain FFI + +FFI via [`flutter_rust_bridge`](https://cjycode.com/flutter_rust_bridge/index.html). Example usage can be found in [`trustchain-mobile`](https://github.com/alan-turing-institute/trustchain-mobile/blob/dev/install_trustchain_mobile.md#9-build-trustchain-targets). \ No newline at end of file diff --git a/trustchain-ffi/src/config.rs b/trustchain-ffi/src/config.rs index cdf5cd0c..a63bef46 100644 --- a/trustchain-ffi/src/config.rs +++ b/trustchain-ffi/src/config.rs @@ -2,10 +2,12 @@ use anyhow::anyhow; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use ssi::vc::LinkedDataProofOptions; -use std::fs; +use std::{fs, str::FromStr}; use trustchain_core::TRUSTCHAIN_CONFIG; use trustchain_ion::{Endpoint, URL}; +use crate::mobile::FFIMobileError; + lazy_static! { /// Lazy static reference to ION configuration loaded from `trustchain_config.toml`. pub static ref FFI_CONFIG: FFIConfig = parse_toml( @@ -14,7 +16,7 @@ lazy_static! { } /// Parses and maps ION subfields to a new type. -fn parse_toml(toml_str: &str) -> FFIConfig { +pub(crate) fn parse_toml(toml_str: &str) -> FFIConfig { toml::from_str::(toml_str) .expect("Error parsing trustchain_config.toml") .ffi @@ -76,19 +78,33 @@ pub struct FFIConfig { impl FFIConfig { pub fn endpoint(&self) -> anyhow::Result<&EndpointOptions> { - self.endpoint_options + Ok(self + .endpoint_options .as_ref() - .ok_or_else(|| anyhow!("Expected endpoint options.")) + .ok_or(anyhow!("Expected endpoint options.")) + .map_err(FFIMobileError::NoConfig)?) } pub fn trustchain(&self) -> anyhow::Result<&TrustchainOptions> { - self.trustchain_options + Ok(self + .trustchain_options .as_ref() - .ok_or_else(|| anyhow!("Expected trustchain options.")) + .ok_or(anyhow!("Expected trustchain options.")) + .map_err(FFIMobileError::NoConfig)?) } pub fn linked_data_proof(&self) -> anyhow::Result<&LinkedDataProofOptions> { - self.linked_data_proof_options + Ok(self + .linked_data_proof_options .as_ref() - .ok_or_else(|| anyhow!("Expected linked data proof options.")) + .ok_or(anyhow!("Expected linked data proof options.")) + .map_err(FFIMobileError::NoConfig)?) + } +} + +impl FromStr for FFIConfig { + type Err = FFIMobileError; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(FFIMobileError::FailedToDeserialize) } } @@ -160,20 +176,19 @@ mod tests { let test_mobile_options: String = format!( r#" {{ - "endpointOptions": {}, - "trustchainOptions": {}, - "linkedDataProofOptions": {} + "endpointOptions": {TEST_ENDPOINT_OPTIONS}, + "trustchainOptions": {TEST_TRUSTCHAIN_OPTIONS}, + "linkedDataProofOptions": {TEST_LINKED_DATA_PROOF_OPTIONS} }} "#, - TEST_ENDPOINT_OPTIONS, TEST_TRUSTCHAIN_OPTIONS, TEST_LINKED_DATA_PROOF_OPTIONS ); serde_json::from_str::(&test_mobile_options).unwrap(); } #[test] fn test_ffi_options_from_toml() { - println!("{:?}", parse_toml(&TEST_FFI_OPTIONS)); + println!("{:?}", parse_toml(TEST_FFI_OPTIONS)); assert_eq!( - parse_toml(&TEST_FFI_OPTIONS) + parse_toml(TEST_FFI_OPTIONS) .endpoint() .unwrap() .trustchain_endpoint() @@ -182,7 +197,7 @@ mod tests { 8081 ); assert_eq!( - parse_toml(&TEST_FFI_OPTIONS) + parse_toml(TEST_FFI_OPTIONS) .linked_data_proof() .unwrap() .proof_purpose diff --git a/trustchain-ffi/src/gui.rs b/trustchain-ffi/src/gui.rs index 4de43482..31524552 100644 --- a/trustchain-ffi/src/gui.rs +++ b/trustchain-ffi/src/gui.rs @@ -55,9 +55,9 @@ fn attest(did: String, controlled_did: String, verbose: bool) -> anyhow::Result< /// Resolves a given DID using a resolver available at localhost:3000 fn resolve(did: String, verbose: bool) -> anyhow::Result { let rt = Runtime::new().unwrap(); + let resolver = get_ion_resolver("http://localhost:3000/"); rt.block_on(async { - let (res_meta, doc, doc_meta) = - TrustchainAPI::resolve(&did, "http://localhost:3000/".into()).await?; + let (res_meta, doc, doc_meta) = TrustchainAPI::resolve(&did, &resolver).await?; // TODO: refactor conversion into trustchain-core resolve module Ok(serde_json::to_string_pretty(&ResolutionResult { context: Some(serde_json::Value::String( diff --git a/trustchain-ffi/src/lib.rs b/trustchain-ffi/src/lib.rs index 2425ea4f..183a549f 100644 --- a/trustchain-ffi/src/lib.rs +++ b/trustchain-ffi/src/lib.rs @@ -1,4 +1,4 @@ pub mod config; -// TODO: uncomment on gui and mobile feature branches // pub mod gui; -// pub mod mobile; +pub mod mobile; +mod mobile_bridge; diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index a97612a1..1dc3f8e1 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -1,65 +1,293 @@ -use anyhow::{anyhow, Result}; -use serde_json::to_string_pretty; +//! Mobile FFI. +use crate::config::FFIConfig; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use ssi::{ + jsonld::ContextLoader, + jwk::JWK, + ldp::{now_ns, Proof}, + one_or_many::OneOrMany, + vc::{Credential, LinkedDataProofOptions, Presentation}, +}; +use thiserror::Error; use tokio::runtime::Runtime; -use trustchain_api::{api::TrustchainDIDAPI, TrustchainAPI}; -use trustchain_ion::get_ion_resolver; +use trustchain_api::{ + api::{TrustchainDIDAPI, TrustchainVCAPI}, + TrustchainAPI, +}; +use trustchain_core::{ + resolver::ResolverError, vc::CredentialError, verifier::VerifierError, vp::PresentationError, +}; +use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; -/// Android localhost endpoint. -const ANDROID_ENDPOINT: &str = "http://10.0.2.2:3000/"; +/// A speicfic error for FFI mobile making handling easier. +#[derive(Error, Debug)] +pub enum FFIMobileError { + /// Failed to deserialize JSON. + #[error("JSON Deserialization Error: {0}.")] + FailedToDeserialize(serde_json::Error), + /// Failed to serialize. + #[error("JSON Serialization Error: {0}.")] + FailedToSerialize(serde_json::Error), + #[error("Missing config error: {0}")] + NoConfig(anyhow::Error), + #[error("DID Resolve Error: {0}.")] + FailedToResolveDID(ResolverError), + #[error("DID Verify Error: {0}.")] + FailedToVerifyDID(VerifierError), + #[error("Failed to verify credential error: {0}.")] + FailedToVerifyCredential(CredentialError), + #[error("Credential proof created time ({0}) is in the future relative to now ({1}).")] + FutureProofCreatedTime(DateTime, DateTime), + #[error("Failed to issue presentation error: {0}.")] + FailedToIssuePresentation(PresentationError), +} /// Example greet function. pub fn greet() -> String { - "Hello from Rust! 🦀".into() + format!("Hello from Rust at time: {}", now_ns()) } -// TODO: update to use TrustchainCLI once endpoint can be passed -/// Example resolve interface. -pub fn resolve(did: String) -> Result { +/// Resolves a given DID document returning the serialized DID document as a JSON string. +pub fn did_resolve(did: String, opts: String) -> Result { + let mobile_opts: FFIConfig = opts.parse()?; + let endpoint_opts = mobile_opts.endpoint()?; + let resolver = get_ion_resolver(&endpoint_opts.ion_endpoint().to_address()); let rt = Runtime::new().unwrap(); rt.block_on(async { - // Trustchain Resolver with android localhost - let resolver = get_ion_resolver(ANDROID_ENDPOINT); - // Result metadata, Document, Document metadata - let (_, doc, _) = resolver.resolve_as_result(&did).await.unwrap(); - Ok(to_string_pretty(&doc.unwrap())?) + Ok(TrustchainAPI::resolve(&did, &resolver) + .await + .map_err(FFIMobileError::FailedToResolveDID) + .and_then(|(_, doc, _)| { + serde_json::to_string_pretty(&doc).map_err(FFIMobileError::FailedToSerialize) + })?) }) } -/// Resolves a given DID document assuming trust in endpoint. -pub fn did_resolve(did: String) -> Result { +/// Verifies a given DID returning the serialized DIDChain as a JSON string. +pub fn did_verify(did: String, opts: String) -> Result { + let mobile_opts: FFIConfig = opts.parse()?; + let endpoint_opts = mobile_opts.endpoint()?; + let trustchain_opts = mobile_opts.trustchain()?; + let root_event_time = trustchain_opts.root_event_time; let rt = Runtime::new().unwrap(); rt.block_on(async { - // Trustchain Resolver with android localhost - TrustchainAPI::resolve(&did, ANDROID_ENDPOINT.into()) + let verifier = IONVerifier::with_endpoint( + get_ion_resolver(&endpoint_opts.ion_endpoint().to_address()), + endpoint_opts.trustchain_endpoint()?.to_address(), + ); + Ok(TrustchainAPI::verify(&did, root_event_time, &verifier) .await - .map_err(|e| anyhow!(e)) - .and_then(|(_, doc, _)| serde_json::to_string_pretty(&doc).map_err(|e| anyhow!(e))) + .map_err(FFIMobileError::FailedToVerifyDID) + .and_then(|did_chain| { + serde_json::to_string_pretty(&did_chain).map_err(FFIMobileError::FailedToSerialize) + })?) }) } -/// Verifies a given DID assuming trust in endpoint. -pub fn did_verify(did: String) -> Result { - todo!() -} -/// Verifies a given DID bundle providing complete verification without trust in endpoint. -pub fn did_verify_bundle(bundle_json: String) -> Result { - todo!() -} -/// Verifies a verifiable credential. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_credential.html). -pub fn vc_verify_credential(credential_json: String, proof_options_json: String) -> Result { - todo!() + +/// Verifies a verifiable credential returning the serialized DIDChain as a JSON string. +pub fn vc_verify_credential(credential: String, opts: String) -> Result { + let mobile_opts: FFIConfig = opts.parse()?; + let endpoint_opts = mobile_opts.endpoint()?; + let trustchain_opts = mobile_opts.trustchain()?; + let ldp_opts = mobile_opts.linked_data_proof().cloned().ok(); + let credential: Credential = serde_json::from_str(&credential)?; + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let verifier = IONVerifier::with_endpoint( + get_ion_resolver(&endpoint_opts.ion_endpoint().to_address()), + endpoint_opts.trustchain_endpoint()?.to_address(), + ); + let root_event_time = trustchain_opts.root_event_time; + + // When using android emulator, the time can be less than the created time in the proof if + // the clock is not correctly synchronised. This leads to a failure upon the proofs being + // checked: + // https://docs.rs/ssi/0.4.0/src/ssi/vc.rs.html#1243 (filtered here) + // https://docs.rs/ssi/0.4.0/src/ssi/vc.rs.html#1973-1975 (created time checked here) + // + // To recover, check that a time later than when the created time on the credential is used. + if let Some(OneOrMany::One(Proof { + created: Some(created_time), + .. + })) = credential.proof.as_ref() + { + let now = now_ns(); + if &now < created_time { + return Err( + FFIMobileError::FutureProofCreatedTime(created_time.to_owned(), now).into(), + ); + } + } + Ok(TrustchainAPI::verify_credential( + &credential, + ldp_opts, + root_event_time, + &verifier, + &mut ContextLoader::default(), + ) + .await + .map_err(FFIMobileError::FailedToVerifyCredential) + .and_then(|did_chain| { + serde_json::to_string_pretty(&did_chain).map_err(FFIMobileError::FailedToSerialize) + })?) + }) } + /// Issues a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_issue_presentation.html). -pub fn vc_issue_presentation( - presentation_json: String, - proof_options_json: String, - key_json: String, -) { - todo!() -} -/// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). -pub fn vc_verify_presentation( - presentation_json: String, - proof_options_json: String, +pub fn vp_issue_presentation( + presentation: String, + opts: String, + jwk_json: String, ) -> Result { - todo!() + let mobile_opts: FFIConfig = opts.parse()?; + let endpoint_opts = mobile_opts.endpoint()?; + let ldp_opts = + mobile_opts + .linked_data_proof() + .cloned() + .ok() + .unwrap_or(LinkedDataProofOptions { + proof_purpose: Some(ssi::vc::ProofPurpose::Authentication), + ..Default::default() + }); + let mut presentation: Presentation = + serde_json::from_str(&presentation).map_err(FFIMobileError::FailedToDeserialize)?; + let jwk: JWK = serde_json::from_str(&jwk_json)?; + let resolver = get_ion_resolver(&endpoint_opts.ion_endpoint().to_address()); + let rt = Runtime::new().unwrap(); + let proof = rt + .block_on(async { + presentation + .generate_proof(&jwk, &ldp_opts, &resolver, &mut ContextLoader::default()) + .await + }) + .map_err(|err| FFIMobileError::FailedToIssuePresentation(PresentationError::VC(err)))?; + presentation.add_proof(proof); + Ok(serde_json::to_string_pretty(&presentation).map_err(FFIMobileError::FailedToSerialize)?) +} + +// // TODO: implement once verifiable presentations are included in API +// /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). +// pub fn vc_verify_presentation(presentation: String, opts: String) -> Result { +// todo!() +// } + +#[cfg(test)] +mod tests { + use ssi::vc::CredentialOrJWT; + + use crate::config::parse_toml; + + use super::*; + const TEST_FFI_CONFIG: &str = r#" + [ffi.trustchainOptions] + rootEventTime = 1666265405 + signatureOnly = false + + [ffi.endpointOptions] + ionEndpoint.host = "127.0.0.1" + ionEndpoint.port = 3000 + trustchainEndpoint.host = "127.0.0.1" + trustchainEndpoint.port = 8081 + "#; + + const TEST_CREDENTIAL: &str = r#" + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential" + ], + "credentialSubject": { + "familyName": "Bloggs", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Arts", + "college": "University of Oxbridge" + }, + "givenName": "Jane" + }, + "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "proof": { + "type": "EcdsaSecp256k1Signature2019", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI", + "created": "2023-07-28T12:53:28.645Z", + "jws": "eyJhbGciOiJFUzI1NksiLCJjcml0IjpbImI2NCJdLCJiNjQiOmZhbHNlfQ..a3bK-CKwhX0jIKNAv_aBjHxBNe3qf_Szc6aUTFagYa8ipWV2a13wipHNxfP3Nq5bM10P3khgdH4hR0d45s1qDA" + } + } + "#; + + #[test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + fn test_did_resolve() { + let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string(); + let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); + did_resolve(did, ffi_opts).unwrap(); + } + + #[test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + fn test_did_verify() { + let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string(); + let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); + did_verify(did, ffi_opts).unwrap(); + } + + #[test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + fn test_vc_verify_credential() { + let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); + let credential: Credential = serde_json::from_str(TEST_CREDENTIAL).unwrap(); + vc_verify_credential(serde_json::to_string(&credential).unwrap(), ffi_opts).unwrap(); + } + + #[test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + fn test_vp_issue_presentation() { + let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); + let credential: Credential = serde_json::from_str(TEST_CREDENTIAL).unwrap(); + let root_plus_1_did: &str = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + let presentation: Presentation = Presentation { + verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(credential))), + holder: Some(ssi::vc::URI::String(root_plus_1_did.to_string())), + ..Default::default() + }; + let root_plus_1_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0","d":"HbjLQf4tnwJR6861-91oGpERu8vmxDpW8ZroDCkmFvY"}"#; + let presentation = vp_issue_presentation( + serde_json::to_string(&presentation).unwrap(), + ffi_opts, + root_plus_1_signing_key.to_string(), + ); + println!("{}", presentation.unwrap()); + // assert!(presentation.is_ok()); + } + + #[test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + fn test_vp_issue_presentation_ed25519() { + let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); + let credential: Credential = serde_json::from_str(TEST_CREDENTIAL).unwrap(); + let did: &str = "did:key:z6MkhG98a8j2d3jqia13vrWqzHwHAgKTv9NjYEgdV3ndbEdD"; + let key: &str = r#"{"kty":"OKP","crv":"Ed25519","x":"Kbnao1EkojaLeZ135PuIf28opnQybD0lB-_CQxuvSDg","d":"vwJwnuhHd4J0UUvjfYr8YxYwvNLU_GVkdqEbC3sUtAY"}"#; + let presentation: Presentation = Presentation { + verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(credential))), + holder: Some(ssi::vc::URI::String(did.to_string())), + ..Default::default() + }; + + let presentation = vp_issue_presentation( + serde_json::to_string(&presentation).unwrap(), + ffi_opts, + key.to_string(), + ); + println!("{}", presentation.unwrap()); + } + + // // TODO: implement once verifiable presentations are included in API + // #[test] + // fn test_vc_verify_presentation() {} } diff --git a/trustchain-ffi/src/mobile_bridge.io.rs b/trustchain-ffi/src/mobile_bridge.io.rs new file mode 100644 index 00000000..0eb55178 --- /dev/null +++ b/trustchain-ffi/src/mobile_bridge.io.rs @@ -0,0 +1,104 @@ +use super::*; +// Section: wire functions + +#[no_mangle] +pub extern "C" fn wire_greet(port_: i64) { + wire_greet_impl(port_) +} + +#[no_mangle] +pub extern "C" fn wire_did_resolve( + port_: i64, + did: *mut wire_uint_8_list, + opts: *mut wire_uint_8_list, +) { + wire_did_resolve_impl(port_, did, opts) +} + +#[no_mangle] +pub extern "C" fn wire_did_verify( + port_: i64, + did: *mut wire_uint_8_list, + opts: *mut wire_uint_8_list, +) { + wire_did_verify_impl(port_, did, opts) +} + +#[no_mangle] +pub extern "C" fn wire_vc_verify_credential( + port_: i64, + credential: *mut wire_uint_8_list, + opts: *mut wire_uint_8_list, +) { + wire_vc_verify_credential_impl(port_, credential, opts) +} + +#[no_mangle] +pub extern "C" fn wire_vp_issue_presentation( + port_: i64, + presentation: *mut wire_uint_8_list, + opts: *mut wire_uint_8_list, + jwk_json: *mut wire_uint_8_list, +) { + wire_vp_issue_presentation_impl(port_, presentation, opts, jwk_json) +} + +// Section: allocate functions + +#[no_mangle] +pub extern "C" fn new_uint_8_list_0(len: i32) -> *mut wire_uint_8_list { + let ans = wire_uint_8_list { + ptr: support::new_leak_vec_ptr(Default::default(), len), + len, + }; + support::new_leak_box_ptr(ans) +} + +// Section: related functions + +// Section: impl Wire2Api + +impl Wire2Api for *mut wire_uint_8_list { + fn wire2api(self) -> String { + let vec: Vec = self.wire2api(); + String::from_utf8_lossy(&vec).into_owned() + } +} + +impl Wire2Api> for *mut wire_uint_8_list { + fn wire2api(self) -> Vec { + unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + } + } +} +// Section: wire structs + +#[repr(C)] +#[derive(Clone)] +pub struct wire_uint_8_list { + ptr: *mut u8, + len: i32, +} + +// Section: impl NewWithNullPtr + +pub trait NewWithNullPtr { + fn new_with_null_ptr() -> Self; +} + +impl NewWithNullPtr for *mut T { + fn new_with_null_ptr() -> Self { + std::ptr::null_mut() + } +} + +// Section: sync execution mode utility + +#[no_mangle] +pub extern "C" fn free_WireSyncReturn(ptr: support::WireSyncReturn) { + unsafe { + let _ = support::box_from_leak_ptr(ptr); + }; +} diff --git a/trustchain-ffi/src/mobile_bridge.rs b/trustchain-ffi/src/mobile_bridge.rs new file mode 100644 index 00000000..f18a179f --- /dev/null +++ b/trustchain-ffi/src/mobile_bridge.rs @@ -0,0 +1,149 @@ +#![allow( + non_camel_case_types, + unused, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::double_parens, + non_snake_case, + clippy::too_many_arguments +)] +// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`@ 1.64.0. + +use crate::mobile::*; +use core::panic::UnwindSafe; +use flutter_rust_bridge::*; +use std::ffi::c_void; +use std::sync::Arc; + +// Section: imports + +// Section: wire functions + +fn wire_greet_impl(port_: MessagePort) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "greet", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || move |task_callback| Ok(greet()), + ) +} +fn wire_did_resolve_impl( + port_: MessagePort, + did: impl Wire2Api + UnwindSafe, + opts: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "did_resolve", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_did = did.wire2api(); + let api_opts = opts.wire2api(); + move |task_callback| did_resolve(api_did, api_opts) + }, + ) +} +fn wire_did_verify_impl( + port_: MessagePort, + did: impl Wire2Api + UnwindSafe, + opts: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "did_verify", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_did = did.wire2api(); + let api_opts = opts.wire2api(); + move |task_callback| did_verify(api_did, api_opts) + }, + ) +} +fn wire_vc_verify_credential_impl( + port_: MessagePort, + credential: impl Wire2Api + UnwindSafe, + opts: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "vc_verify_credential", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_credential = credential.wire2api(); + let api_opts = opts.wire2api(); + move |task_callback| vc_verify_credential(api_credential, api_opts) + }, + ) +} +fn wire_vp_issue_presentation_impl( + port_: MessagePort, + presentation: impl Wire2Api + UnwindSafe, + opts: impl Wire2Api + UnwindSafe, + jwk_json: impl Wire2Api + UnwindSafe, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap( + WrapInfo { + debug_name: "vp_issue_presentation", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || { + let api_presentation = presentation.wire2api(); + let api_opts = opts.wire2api(); + let api_jwk_json = jwk_json.wire2api(); + move |task_callback| vp_issue_presentation(api_presentation, api_opts, api_jwk_json) + }, + ) +} +// Section: wrapper structs + +// Section: static checks + +// Section: allocate functions + +// Section: related functions + +// Section: impl Wire2Api + +pub trait Wire2Api { + fn wire2api(self) -> T; +} + +impl Wire2Api> for *mut S +where + *mut S: Wire2Api, +{ + fn wire2api(self) -> Option { + (!self.is_null()).then(|| self.wire2api()) + } +} + +impl Wire2Api for u8 { + fn wire2api(self) -> u8 { + self + } +} + +// Section: impl IntoDart + +// Section: executor + +support::lazy_static! { + pub static ref FLUTTER_RUST_BRIDGE_HANDLER: support::DefaultHandler = Default::default(); +} + +#[cfg(not(target_family = "wasm"))] +#[path = "mobile_bridge.io.rs"] +mod io; +#[cfg(not(target_family = "wasm"))] +pub use io::*; diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 28ddb727..fad55fc8 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -13,12 +13,14 @@ path = "src/bin/main.rs" [dependencies] trustchain-core = { path = "../trustchain-core" } trustchain-ion = { path = "../trustchain-ion" } +trustchain-api = { path = "../trustchain-api" } async-trait = "0.1" axum = "0.6" +axum-server = {version="0.5.1", features = ["tls-rustls"] } base64 = "0.21.0" chrono = "^0.4" clap = { version = "^4", features=["derive", "env", "cargo"] } -did-ion="0.1.0" +did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} execute = "0.2.11" gloo-console = "0.2.3" hyper = "0.14.26" @@ -30,7 +32,8 @@ reqwest = "0.11.16" serde = { version = "1.0", features = ["derive"] } serde_jcs = "0.1.0" serde_json = "1.0" -ssi = "0.4" +shellexpand = "3.1.0" +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} thiserror="1.0" tokio = {version = "1.20.1", features = ["full"]} tower = "0.4" diff --git a/trustchain-http/src/bin/main.rs b/trustchain-http/src/bin/main.rs index 53def871..9a661002 100644 --- a/trustchain-http/src/bin/main.rs +++ b/trustchain-http/src/bin/main.rs @@ -1,5 +1,5 @@ use log::info; -use trustchain_http::config::HTTP_CONFIG; +use trustchain_http::config::{http_config, HTTP_CONFIG}; use trustchain_http::server; #[tokio::main] @@ -12,13 +12,12 @@ async fn main() -> std::io::Result<()> { // Print config info!("{}", config); - let addr = config.to_address(); - // Init server - server::server(config).await.unwrap(); - - // Logging - tracing::debug!("listening on {}", addr); + // Run server + match http_config().https { + false => server::http_server(config).await.unwrap(), + true => server::https_server(config).await.unwrap(), + } Ok(()) } diff --git a/trustchain-http/src/config.rs b/trustchain-http/src/config.rs index b8075c43..67cf238c 100644 --- a/trustchain-http/src/config.rs +++ b/trustchain-http/src/config.rs @@ -6,22 +6,35 @@ use std::{ str::FromStr, }; use toml; +use trustchain_core::verifier::Timestamp; use trustchain_core::TRUSTCHAIN_CONFIG; const DEFAULT_HOST: &str = "127.0.0.1"; const DEFAULT_PORT: u16 = 8081; -/// Server config. +/// HTTP configuration. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct HTTPConfig { - /// Hostname for server + /// Host address for server. + /// machine running emulator. pub host: IpAddr, - /// Hostname reference. For example, Android emulator 10.0.2.2 refers to 127.0.0.1 of machine running emulator. - pub host_reference: IpAddr, + /// Hostname display in QR codes. For example, if using local server with an Android emulator + /// `10.0.2.2` refers to `127.0.0.1` of machine + /// running emulator. + pub host_display: String, /// Port for server pub port: u16, - /// Optional issuer DID - pub issuer_did: Option, + /// Optional server DID if issuing or verifying + pub server_did: Option, + /// Flag indicating whether server uses https + pub https: bool, + /// Path containing certificate and key necessary for https + pub https_path: Option, + /// Display downstream DIDs (instead of URLs) in QR codes for verifiable endpoint retrieval + /// (`None` by default and unwrapped as `true`) + pub verifiable_endpoints: Option, + /// Root event time for verifier. + pub root_event_time: Option, } impl std::fmt::Display for HTTPConfig { @@ -34,9 +47,13 @@ impl Default for HTTPConfig { fn default() -> Self { Self { host: IpAddr::from_str(DEFAULT_HOST).unwrap(), - host_reference: IpAddr::from_str(DEFAULT_HOST).unwrap(), + host_display: DEFAULT_HOST.to_string(), port: DEFAULT_PORT, - issuer_did: None, + server_did: None, + https: false, + https_path: None, + verifiable_endpoints: None, + root_event_time: None, } } } @@ -52,6 +69,14 @@ impl HTTPConfig { .parse::() .unwrap() } + /// Provide "http" or "https" according to config. + pub fn http_scheme(&self) -> &str { + if self.https { + "https" + } else { + "http" + } + } } lazy_static! { @@ -73,7 +98,7 @@ pub fn http_config() -> &'static HTTP_CONFIG { &HTTP_CONFIG } -/// Wrapper struct for parsing the `http` table. +/// Wrapper struct for parsing the `http` config table. #[derive(Serialize, Deserialize, Debug, Clone)] struct Config { /// HTTP configuration data. @@ -86,23 +111,24 @@ mod tests { #[test] fn test_deserialize() { - let config_string = r##" + let config_string = r#" [http] host = "127.0.0.1" - host_reference = "127.0.0.1" + host_display = "127.0.0.1" port = 8081 - issuer_did = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA" + server_did = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA" + https = false [non_http] key = "value" - "##; + "#; let config: HTTPConfig = parse_toml(config_string); - + assert!(config.verifiable_endpoints.is_none()); assert_eq!( config, HTTPConfig { - issuer_did: Some( + server_did: Some( "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA".to_string() ), ..HTTPConfig::default() diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index 308776b1..87b0d61f 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -3,8 +3,8 @@ use hyper::StatusCode; use serde_json::json; use thiserror::Error; use trustchain_core::{ - commitment::CommitmentError, issuer::IssuerError, resolver::ResolverError, - verifier::VerifierError, + commitment::CommitmentError, issuer::IssuerError, resolver::ResolverError, vc::CredentialError, + verifier::VerifierError, vp::PresentationError, }; use trustchain_ion::root::TrustchainRootError; @@ -23,10 +23,22 @@ pub enum TrustchainHTTPError { IssuerError(IssuerError), #[error("Trustchain root error: {0}")] RootError(TrustchainRootError), + #[error("Trustchain presentation error: {0}")] + PresentationError(PresentationError), #[error("Credential does not exist.")] CredentialDoesNotExist, #[error("No issuer available.")] NoCredentialIssuer, + #[error("Failed to verify credential.")] + FailedToVerifyCredential, + #[error("Invalid signature.")] + InvalidSignature, + #[error("Request does not exist.")] + RequestDoesNotExist, + #[error("Could not deserialize data: {0}")] + FailedToDeserialize(serde_json::Error), + #[error("Root event time not configured for verification.")] + RootEventTimeNotSet, } impl From for TrustchainHTTPError { @@ -56,6 +68,9 @@ impl From for TrustchainHTTPError { impl From for TrustchainHTTPError { fn from(err: TrustchainRootError) -> Self { TrustchainHTTPError::RootError(err) +impl From for TrustchainHTTPError { + fn from(err: PresentationError) -> Self { + TrustchainHTTPError::PresentationError(err) } } @@ -69,10 +84,8 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::InternalError => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } - err @ TrustchainHTTPError::VerifierError(VerifierError::InvalidRoot(_)) => { - (StatusCode::OK, err.to_string()) - } - err @ TrustchainHTTPError::VerifierError(VerifierError::CommitmentFailure(_)) => { + err @ TrustchainHTTPError::VerifierError(VerifierError::InvalidRoot(_)) + | err @ TrustchainHTTPError::VerifierError(VerifierError::CommitmentFailure(_)) => { (StatusCode::OK, err.to_string()) } err @ TrustchainHTTPError::VerifierError(_) => { @@ -87,6 +100,15 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::ResolverError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } + err @ TrustchainHTTPError::PresentationError(PresentationError::CredentialError( + CredentialError::VerifierError(VerifierError::CommitmentFailure(_)), + )) + | err @ TrustchainHTTPError::PresentationError(PresentationError::CredentialError( + CredentialError::VerifierError(VerifierError::InvalidRoot(_)), + )) => (StatusCode::OK, err.to_string()), + err @ TrustchainHTTPError::PresentationError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } err @ TrustchainHTTPError::CredentialDoesNotExist => { (StatusCode::BAD_REQUEST, err.to_string()) } @@ -94,6 +116,21 @@ impl IntoResponse for TrustchainHTTPError { (StatusCode::BAD_REQUEST, err.to_string()) } err @ TrustchainHTTPError::RootError(_) => (StatusCode::BAD_REQUEST, err.to_string()), + err @ TrustchainHTTPError::FailedToVerifyCredential => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } + err @ TrustchainHTTPError::InvalidSignature => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } + err @ TrustchainHTTPError::RequestDoesNotExist => { + (StatusCode::BAD_REQUEST, err.to_string()) + } + err @ TrustchainHTTPError::FailedToDeserialize(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } + err @ TrustchainHTTPError::RootEventTimeNotSet => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } }; let body = Json(json!({ "error": err_message })); (status, body).into_response() diff --git a/trustchain-http/src/issuer.rs b/trustchain-http/src/issuer.rs index 21c2fd0d..379a937f 100644 --- a/trustchain-http/src/issuer.rs +++ b/trustchain-http/src/issuer.rs @@ -1,5 +1,6 @@ +use crate::config::http_config; use crate::errors::TrustchainHTTPError; -use crate::qrcode::str_to_qr_code_html; +use crate::qrcode::{str_to_qr_code_html, DIDQRCode}; use crate::state::AppState; use async_trait::async_trait; use axum::extract::{Path, State}; @@ -10,6 +11,7 @@ use chrono::Utc; use log::info; use serde::{Deserialize, Serialize}; use ssi::did_resolve::DIDResolver; +use ssi::jsonld::ContextLoader; use ssi::one_or_many::OneOrMany; use ssi::vc::Credential; use ssi::vc::VCDateTime; @@ -40,7 +42,10 @@ impl CredentialOffer { /// Generates credential offer. pub fn generate(credential: &Credential, id: &str) -> Self { let mut credential: Credential = credential.to_owned(); - credential.id = Some(ssi::vc::URI::String(format!("urn:uuid:{}", id))); + credential.id = Some(ssi::vc::StringOrURI::URI(ssi::vc::URI::String(format!( + "urn:uuid:{}", + id + )))); Self::new(credential) } } @@ -104,22 +109,42 @@ impl TrustchainIssuerHTTP for TrustchainIssuerHTTPHandler { } } let issuer = IONAttestor::new(issuer_did); - Ok(issuer.sign(&credential, None, None, resolver).await?) + Ok(issuer + .sign( + &credential, + None, + None, + resolver, + // TODO: add context loader to app_state + &mut ContextLoader::default(), + ) + .await?) } } impl TrustchainIssuerHTTPHandler { /// Generates QR code to display to holder to receive requested credential. - pub async fn get_issuer_qrcode(State(app_state): State>) -> Html { - // TODO: update to take query param entered by user. - let id = "7426a2e8-f932-11ed-968a-4bb02079f142".to_string(); - // Generate a QR code for server address and combination of name and UUID - let address_str = format!( - "http://{}:{}/vc/issuer/{id}", - app_state.config.host_reference, app_state.config.port - ); + pub async fn get_issuer_qrcode( + State(app_state): State>, + Path(id): Path, + ) -> Result, TrustchainHTTPError> { + let qr_code_str = if http_config().verifiable_endpoints.unwrap_or(true) { + serde_json::to_string(&DIDQRCode { + did: app_state.config.server_did.as_ref().unwrap().to_owned(), + route: "/vc/issuer/".to_string(), + id, + }) + .unwrap() + } else { + format!( + "{}://{}:{}/vc/issuer/{id}", + http_config().http_scheme(), + app_state.config.host_display, + app_state.config.port + ) + }; // Respond with the QR code as a png embedded in html - Html(str_to_qr_code_html(&address_str, "Issuer")) + Ok(Html(str_to_qr_code_html(&qr_code_str, "Issuer"))) } /// API endpoint taking the UUID of a VC. Response is the VC JSON. @@ -129,7 +154,7 @@ impl TrustchainIssuerHTTPHandler { ) -> impl IntoResponse { let issuer_did = app_state .config - .issuer_did + .server_did .as_ref() .ok_or(TrustchainHTTPError::NoCredentialIssuer)?; @@ -156,7 +181,7 @@ impl TrustchainIssuerHTTPHandler { info!("Received VC info: {:?}", vc_info); let issuer_did = app_state .config - .issuer_did + .server_did .as_ref() .ok_or(TrustchainHTTPError::NoCredentialIssuer)?; match app_state.credentials.get(&credential_id) { @@ -186,17 +211,18 @@ mod tests { use lazy_static::lazy_static; use serde_json::json; use ssi::{ + jsonld::ContextLoader, one_or_many::OneOrMany, vc::{Credential, CredentialSubject, Issuer, URI}, }; - use std::sync::Arc; + use std::{collections::HashMap, sync::Arc}; use trustchain_core::{utils::canonicalize, verifier::Verifier}; use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; lazy_static! { /// Lazy static reference to core configuration loaded from `trustchain_config.toml`. pub static ref TEST_HTTP_CONFIG: HTTPConfig = HTTPConfig { - issuer_did: Some("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string()), + server_did: Some("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string()), ..Default::default() }; } @@ -231,6 +257,7 @@ mod tests { let state = Arc::new(AppState::new_with_cache( TEST_HTTP_CONFIG.to_owned(), serde_json::from_str(CREDENTIALS).unwrap(), + HashMap::new(), )); let app = TrustchainRouter::from(state.clone()).into_router(); // Get offer for valid credential @@ -242,7 +269,7 @@ mod tests { let mut actual_offer = response.json::().await; let mut credential = state.credentials.get(&uid).unwrap().clone(); credential.issuer = Some(ssi::vc::Issuer::URI(ssi::vc::URI::String( - state.config.issuer_did.as_ref().unwrap().to_string(), + state.config.server_did.as_ref().unwrap().to_string(), ))); let mut expected_offer = CredentialOffer::generate(&credential, &uid); @@ -257,11 +284,11 @@ mod tests { ); // Try to get an offer for non-existent credential - let uid = "46cb84e2-fa10-11ed-a0d4-bbb4e61d1555".to_string(); - let uri = format!("/vc/issuer/{uid}"); + let id = "46cb84e2-fa10-11ed-a0d4-bbb4e61d1555".to_string(); + let path = format!("/vc/issuer/{id}"); let app = TrustchainRouter::from(state.clone()).into_router(); let client = TestClient::new(app); - let response = client.get(&uri).send().await; + let response = client.get(&path).send().await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert_eq!( response.text().await, @@ -275,14 +302,15 @@ mod tests { let app = TrustchainRouter::from(Arc::new(AppState::new_with_cache( TEST_HTTP_CONFIG.to_owned(), serde_json::from_str(CREDENTIALS).unwrap(), + HashMap::new(), ))) .into_router(); - let uid = "46cb84e2-fa10-11ed-a0d4-bbb4e61d1556".to_string(); + let id = "46cb84e2-fa10-11ed-a0d4-bbb4e61d1556".to_string(); let expected_subject_id = "did:example:284b3f34fad911ed9aea439566dd422a".to_string(); - let uri = format!("/vc/issuer/{uid}"); + let path = format!("/vc/issuer/{id}"); let client = TestClient::new(app); let response = client - .post(&uri) + .post(&path) .json(&VcInfo { subject_id: expected_subject_id.to_string(), }) @@ -303,7 +331,9 @@ mod tests { // Test signature let verifier = IONVerifier::new(get_ion_resolver("http://localhost:3000/")); - let verify_credential_result = credential.verify(None, verifier.resolver()).await; + let verify_credential_result = credential + .verify(None, verifier.resolver(), &mut ContextLoader::default()) + .await; assert!(verify_credential_result.errors.is_empty()); // Test valid Trustchain issuer DID diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index 6f208018..6f99abf0 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -9,24 +9,3 @@ pub mod server; pub mod state; pub mod static_handlers; pub mod verifier; - -/// Example VP request. -pub const EXAMPLE_VP_REQUEST: &str = r#"{ - "type": "VerifiablePresentationRequest", - "query": [ - { - "type": "QueryByExample", - "credentialQuery": { - "reason": "Request credential", - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": "VerifiableCredential" - } - } - } - ], - "challenge": "a877fb0a-11dd-11ee-9df7-9be7abdeee2d", - "domain": "https://alan-turing-institute.github.io/trustchain" -}"#; diff --git a/trustchain-http/src/middleware.rs b/trustchain-http/src/middleware.rs index 5400f36f..5a7a1095 100644 --- a/trustchain-http/src/middleware.rs +++ b/trustchain-http/src/middleware.rs @@ -1,3 +1,4 @@ +//! Middleware for Trustchain HTTP. use axum::{ body::Body, extract::Path, @@ -6,39 +7,156 @@ use axum::{ response::IntoResponse, Json, }; +use did_ion::{ + sidetree::{DIDSuffix, Sidetree}, + ION, +}; +use lazy_static::lazy_static; use serde_json::json; use trustchain_ion::config::ion_config; +use trustchain_ion::ion::IONTest; + +lazy_static! { + static ref ION_DID_PREFIX: String = format!("did:{}", ION::METHOD); + static ref ION_DID_TEST_PREFIX: String = + format!("{}:{}", &*ION_DID_PREFIX, IONTest::NETWORK.unwrap()); +} + +/// Generates an error message given DID and expected prefix. +fn error_message(did: &str, expected_prefix: &str) -> serde_json::Value { + json!({ + "error": + format!( + "DID: {} does not match expected prefix: {}", + did, + expected_prefix + ) + }) +} + +fn validate_did_str( + did: &str, + mongo_database_ion_core: &str, +) -> Result<(), (StatusCode, Json)> { + let did_split = did.rsplit_once(':'); + if did_split.is_none() { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({"error": format!("InvalidDID: {}", did)})), + )); + } + let (did_prefix, ion_did_suffix) = did_split.unwrap(); + + // Only validate ION DIDs. Allow others to pass. + if did_prefix != *ION_DID_PREFIX && did_prefix != *ION_DID_TEST_PREFIX { + return Ok(()); + } + + // Validate the DID suffix given established DID method is ION. + let ion_did_suffix = DIDSuffix(ion_did_suffix.to_string()); + if let Err(err) = ION::validate_did_suffix(&ion_did_suffix) { + return Err(( + StatusCode::BAD_REQUEST, + Json( + json!({"error": format!("DID: {did} does not have a valid ION suffix with error: {err}")}), + ), + )); + }; -// See example from axum: https://github.com/tokio-rs/axum/blob/v0.6.x/examples/consume-body-in-extractor-or-middleware/src/main.rs -// middleware that shows how to consume the request body upfront -// TODO: refactor using [did-ion method](https://docs.rs/did-ion/latest/did_ion/sidetree/trait.Sidetree.html#method.validate_did_suffix) + // Validate the ION network prefix if testnet. + if mongo_database_ion_core.contains("testnet") && did_prefix != *ION_DID_TEST_PREFIX { + return Err(( + StatusCode::BAD_REQUEST, + Json(error_message(did, &ION_DID_TEST_PREFIX)), + )); + } + + // Validate the ION network prefix if mainnet. + if mongo_database_ion_core.contains("mainnet") && did_prefix != *ION_DID_PREFIX { + return Err(( + StatusCode::BAD_REQUEST, + Json(error_message(did, &ION_DID_PREFIX)), + )); + } + Ok(()) +} +// See [example](https://github.com/tokio-rs/axum/blob/v0.6.x/examples/consume-body-in-extractor-or-middleware/src/main.rs) +// from axum with middleware that shows how to consume the request body upfront pub async fn validate_did( Path(did): Path, request: Request, next: Next, -) -> Result { +) -> impl IntoResponse { tracing::info!(did); - // Validate length is 59 (testnet) or 54 (mainnet) - if ion_config().mongo_database_ion_core.contains("testnet") && did.len() != 59 { - let message = json!({ - "error": - format!( - "DID: {} is incorrect length {}. Should be length 59.", - did, - did.len() - ) - }); - return Err((StatusCode::BAD_REQUEST, Json(message))); - } else if ion_config().mongo_database_ion_core.contains("mainnet") && did.len() != 54 { - let message = json!({ - "error": - format!( - "DID: {} is incorrect length {}. Should be length 54.", - did, - did.len() - ) - }); - return Err((StatusCode::BAD_REQUEST, Json(message))); + match validate_did_str(&did, &ion_config().mongo_database_ion_core) { + Ok(_) => Ok(next.run(request).await), + Err(e) => Err(e), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strings() { + assert_eq!("did:ion", *ION_DID_PREFIX); + assert_eq!("did:ion:test", *ION_DID_TEST_PREFIX); + } + + #[test] + fn test_valid_did() { + // Ok cases + for (did, network) in [ + ( + "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "testnet", + ), + ( + "did:ion:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "mainnet", + ), + ( + "did:key:z6MkhG98a8j2d3jqia13vrWqzHwHAgKTv9NjYEgdV3ndbEdD", + "testnet", + ), + ] { + assert!(validate_did_str(did, network).is_ok()); + } + // Error cases + for (did, network) in [ + // Invalid length + ( + "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65", + "testnet", + ), + // Invalid suffix + ( + "did:ion:test:1iAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "testnet", + ), + // Invalid network + ( + "did:ion:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "testnet", + ), + // Invalid length + ( + "did:ion:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65", + "mainnet", + ), + // Invalid suffix + ( + "did:ion:1iAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "mainnet", + ), + // Invalid network + ( + "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "mainnet", + ), + ] { + assert!(validate_did_str(did, network).is_err()); + } } - Ok(next.run(request).await) } diff --git a/trustchain-http/src/qrcode.rs b/trustchain-http/src/qrcode.rs index 46ca64dd..1b6908df 100644 --- a/trustchain-http/src/qrcode.rs +++ b/trustchain-http/src/qrcode.rs @@ -3,6 +3,19 @@ use base64::write::EncoderWriter; use image::Luma; use image::{DynamicImage, ImageOutputFormat}; use qrcode::QrCode; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// QR code JSON type of verifiable content. +pub(crate) struct DIDQRCode { + /// DID for issuance and/or verifier endpoint resolution. + pub did: String, + /// HTTP route for making issuer or verifer requests. + pub route: String, + /// ID for identifying credential offer or presentation request. + pub id: String, +} pub fn image_to_base64_string(image: &DynamicImage) -> String { let mut buf = Vec::new(); diff --git a/trustchain-http/src/resolver.rs b/trustchain-http/src/resolver.rs index 22c1777c..1bf70d5a 100644 --- a/trustchain-http/src/resolver.rs +++ b/trustchain-http/src/resolver.rs @@ -88,10 +88,10 @@ impl TrustchainHTTP for TrustchainHTTPHandler { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] /// Struct for deserializing `root_event_time` from handler's query param. pub struct RootEventTime { - root_event_time: Timestamp, + pub root_event_time: Timestamp, } impl TrustchainHTTPHandler { @@ -205,11 +205,11 @@ mod tests { let invalid_uri = "/did/did:ion:test:invalid_did__AsM3tgCut3OiBY4ekHTf__invalid_did".to_string(); let response = client.get(&invalid_uri).send().await; - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert_eq!( response.text().await, - r#"{"error":"Trustchain Resolver error: DID: did:ion:test:invalid_did__AsM3tgCut3OiBY4ekHTf__invalid_did does not exist."}"# + r#"{"error":"DID: did:ion:test:invalid_did__AsM3tgCut3OiBY4ekHTf__invalid_did does not have a valid ION suffix with error: Decode Base64"}"# ) } @@ -265,7 +265,7 @@ mod tests { assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!( response.text().await, - r#"{"error":"Trustchain Verifier error: A resolver error during verification."}"# + r#"{"error":"Trustchain Verifier error: A resolver error during verification: DID: did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65A is not found."}"# .to_string() ); } @@ -295,7 +295,7 @@ mod tests { // Make a verifier instance and fetch bundle from server bundle endpoint let verifier = IONVerifier::with_endpoint( get_ion_resolver("http://localhost:3000/"), - format!("http://127.0.0.1:{}/did/bundle/", port), + format!("http://127.0.0.1:{}/", port), ); let did = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA"; // Check verification diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 6faec676..94950572 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -1,10 +1,13 @@ +use crate::config::http_config; use crate::middleware::validate_did; use crate::{ config::HTTPConfig, issuer, resolver, root, state::AppState, static_handlers, verifier, }; use axum::routing::IntoMakeService; use axum::{middleware, routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; use hyper::server::conn::AddrIncoming; +use std::path::PathBuf; use std::sync::Arc; use tower::ServiceBuilder; @@ -33,7 +36,7 @@ impl TrustchainRouter { router: Router::new() .route("/", get(static_handlers::index)) .route( - "/issuer", + "/issuer/:id", get(issuer::TrustchainIssuerHTTPHandler::get_issuer_qrcode), ) .route( @@ -50,22 +53,39 @@ impl TrustchainRouter { }), ) .route( - "/vc/verifier", - get(verifier::TrustchainVerifierHTTPHandler::get_verifier) - .post(verifier::TrustchainVerifierHTTPHandler::post_verifier), + "/vc/verifier/:id", + get(verifier::TrustchainVerifierHTTPHandler::get_verifier).post({ + let state = shared_state.clone(); + move |verification_info| { + verifier::TrustchainVerifierHTTPHandler::post_verifier( + verification_info, + state, + ) + } + }), ) .route( "/did/:id", get(resolver::TrustchainHTTPHandler::get_did_resolution) .layer(ServiceBuilder::new().layer(middleware::from_fn(validate_did))), ) + // Duplicate `did` and `identifier` routes as the resolver expects a + // `SidetreeClient` that can resolve at route `/identifiers/`: + // See [here](https://docs.rs/did-ion/0.1.0/src/did_ion/sidetree.rs.html#1392-1400). + .route( + "/identifiers/:id", + get(resolver::TrustchainHTTPHandler::get_did_resolution) + .layer(ServiceBuilder::new().layer(middleware::from_fn(validate_did))), + ) .route( "/did/chain/:id", - get(resolver::TrustchainHTTPHandler::get_chain_resolution), + get(resolver::TrustchainHTTPHandler::get_chain_resolution) + .layer(ServiceBuilder::new().layer(middleware::from_fn(validate_did))), ) .route( "/did/bundle/:id", - get(resolver::TrustchainHTTPHandler::get_verification_bundle), + get(resolver::TrustchainHTTPHandler::get_verification_bundle) + .layer(ServiceBuilder::new().layer(middleware::from_fn(validate_did))), ) .route( "/root", @@ -85,10 +105,34 @@ impl TrustchainRouter { } } -/// General method to spawn a Trustchain server given ServerConfig. -pub fn server(config: HTTPConfig) -> axum::Server> { +/// Spawns a Trustchain server given `HTTPConfig` with http. +pub fn http_server(config: HTTPConfig) -> axum::Server> { let addr = config.to_socket_address(); let shared_state = Arc::new(AppState::new(config)); let app = TrustchainRouter::from(shared_state).into_router(); axum::Server::bind(&addr).serve(app.into_make_service()) } + +/// Spawns a Trustchain server given `HTTPConfig` with https. +pub async fn https_server(config: HTTPConfig) -> std::io::Result<()> { + let addr = config.to_socket_address(); + let shared_state = Arc::new(AppState::new(config)); + let app = TrustchainRouter::from(shared_state).into_router(); + let tls_config = rustls_config(http_config().https_path.as_ref().unwrap()).await; + axum_server::bind_rustls(addr, tls_config) + .serve(app.into_make_service()) + .await +} + +/// Generates a `RustlsConfig` for https servers given a path with certificate and key. Based on +/// axum [example](https://github.com/tokio-rs/axum/blob/d30375925dd22cc44aeaae2871f8ead1630fadf8/examples/tls-rustls/src/main.rs). +async fn rustls_config(path: &str) -> RustlsConfig { + // Configure certificate and private key used by https + let path = shellexpand::tilde(path); + RustlsConfig::from_pem_file( + PathBuf::from(path.as_ref()).join("cert.pem"), + PathBuf::from(path.as_ref()).join("key.pem"), + ) + .await + .expect("Failed to create Rustls config.") +} diff --git a/trustchain-http/src/state.rs b/trustchain-http/src/state.rs index 9183ed40..ee65fa27 100644 --- a/trustchain-http/src/state.rs +++ b/trustchain-http/src/state.rs @@ -1,6 +1,6 @@ -use crate::config::HTTPConfig; use crate::root::RootCandidatesResult; use chrono::NaiveDate; +use crate::{config::HTTPConfig, verifier::PresentationRequest}; use ssi::vc::Credential; use std::{collections::HashMap, sync::Mutex}; use trustchain_core::{resolver::Resolver, TRUSTCHAIN_DATA}; @@ -14,6 +14,7 @@ pub struct AppState { pub verifier: IONVerifier, pub credentials: HashMap, pub root_candidates: Mutex>, + pub presentation_requests: HashMap, } impl AppState { @@ -22,19 +23,32 @@ impl AppState { let path = std::env::var(TRUSTCHAIN_DATA).expect("TRUSTCHAIN_DATA env not set."); let credentials: HashMap = serde_json::from_reader( std::fs::read(std::path::Path::new(&path).join("credentials/offers/cache.json")) - .expect("Credential cache does not exist.") + // If no cache, default to empty + .unwrap_or_default() .as_slice(), ) .expect("Credential cache could not be deserialized."); let root_candidates = Mutex::new(HashMap::new()); + let presentation_requests: HashMap = serde_json::from_reader( + std::fs::read(std::path::Path::new(&path).join("presentations/requests/cache.json")) + // If no cache, default to empty + .unwrap_or_default() + .as_slice(), + ) + .expect("Presentation cache could not be deserialized."); Self { config, verifier, credentials, root_candidates, + presentation_requests, } } - pub fn new_with_cache(config: HTTPConfig, credentials: HashMap) -> Self { + pub fn new_with_cache( + config: HTTPConfig, + credentials: HashMap, + presentation_requests: HashMap, + ) -> Self { let verifier = IONVerifier::new(Resolver::new(get_ion_resolver(DEFAULT_VERIFIER_ENDPOINT))); let root_candidates = Mutex::new(HashMap::new()); Self { @@ -42,6 +56,7 @@ impl AppState { verifier, credentials, root_candidates, + presentation_requests, } } } @@ -55,6 +70,6 @@ mod tests { #[ignore = "requires TRUSTCHAIN_DATA and TRUSTCHAIN_CONFIG environment variables"] fn test_create_app_state() { AppState::new(HTTPConfig::default()); - AppState::new_with_cache(HTTPConfig::default(), HashMap::new()); + AppState::new_with_cache(HTTPConfig::default(), HashMap::new(), HashMap::new()); } } diff --git a/trustchain-http/src/verifier.rs b/trustchain-http/src/verifier.rs index fecff42b..d2477987 100644 --- a/trustchain-http/src/verifier.rs +++ b/trustchain-http/src/verifier.rs @@ -1,94 +1,391 @@ -use crate::qrcode::str_to_qr_code_html; +use crate::config::http_config; +use crate::errors::TrustchainHTTPError; +use crate::qrcode::{str_to_qr_code_html, DIDQRCode}; use crate::state::AppState; -use crate::EXAMPLE_VP_REQUEST; -use axum::extract::State; +use async_trait::async_trait; +use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::response::{Html, IntoResponse}; use axum::Json; use log::info; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use ssi::did_resolve::DIDResolver; +use ssi::jsonld::ContextLoader; +use ssi::ldp::LinkedDataDocument; use ssi::vc::{Credential, Presentation}; use std::sync::Arc; +use trustchain_api::api::TrustchainVPAPI; +use trustchain_api::TrustchainAPI; +use trustchain_core::verifier::{Timestamp, Verifier}; +use trustchain_ion::verifier::IONVerifier; -// TODO: implement in core? -pub struct PresentationRequest; - -// TODO: implement in core? -/// An error type for presentation failures -pub enum PresentationError { - FailedToVerify, - // TODO: add other variants -} +/// A type for presentation requests. See [VP request spec](https://w3c-ccg.github.io/vp-request-spec/) +/// for further details. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct PresentationRequest(Value); /// An API for a Trustchain verifier server. +#[async_trait] pub trait TrustchainVerifierHTTP { - /// Constructs a presentation request (given some `presentiation_id`) to send to a credential holder from request wallet by ID - fn generate_presentation_request(presentation_id: &str) -> PresentationRequest; - /// Verifies verifiable presentation - fn verify_presentation(presentation: &Presentation) -> Result<(), PresentationError>; - /// Verifies verifiable credential - fn verify_credential(credential: &Credential) -> Result<(), PresentationError>; + /// Verifies verifiable presentation. + async fn verify_presentation( + presentation: &Presentation, + root_event_time: Timestamp, + verifier: &IONVerifier, + ) -> Result<(), TrustchainHTTPError> { + Ok(TrustchainAPI::verify_presentation( + presentation, + None, + root_event_time, + verifier, + // TODO [#128]: move into API upon context loader added to app_state + &mut ContextLoader::default(), + ) + .await?) + } + /// Verifies verifiable credential. + async fn verify_credential( + credential: &Credential, + root_event_time: Timestamp, + verifier: &IONVerifier, + ) -> Result<(), TrustchainHTTPError> { + let verify_credential_result = credential + .verify(None, verifier.resolver(), &mut ContextLoader::default()) + .await; + if !verify_credential_result.errors.is_empty() { + return Err(TrustchainHTTPError::InvalidSignature); + } + match credential.get_issuer() { + Some(issuer) => Ok(verifier.verify(issuer, root_event_time).await.map(|_| ())?), + _ => Err(TrustchainHTTPError::NoCredentialIssuer), + } + } } +/// Handler for verification of credentials and presentations. pub struct TrustchainVerifierHTTPHandler; -impl TrustchainVerifierHTTP for TrustchainVerifierHTTPHandler { - fn generate_presentation_request(_presentation_id: &str) -> PresentationRequest { - todo!() +impl TrustchainVerifierHTTP for TrustchainVerifierHTTPHandler {} + +/// Struct for deserializing credential and corresponding root event time. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostVerifier { + pub presentation_or_credential: PresentationOrCredential, + // TODO [#130]: update field upon root event time changing to date and confirmation code. + pub root_event_time: Timestamp, +} + +/// Enum for indicating whether verification information is a presentation or credential. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PresentationOrCredential { + Presentation(Presentation), + Credential(Credential), +} + +impl TrustchainVerifierHTTPHandler { + /// API endpoint taking the UUID of a presentation request. + pub async fn get_verifier( + Path(id): Path, + State(app_state): State>, + ) -> impl IntoResponse { + app_state + .presentation_requests + .get(&id) + .ok_or(TrustchainHTTPError::RequestDoesNotExist) + .map(|request| (StatusCode::OK, Json(request.to_owned()))) } + /// Handler for presentation or credential received from POST. + pub async fn post_verifier( + Json(verification_info): Json, + app_state: Arc, + ) -> impl IntoResponse { + let verification_info_json = serde_json::to_string_pretty(&verification_info) + .map_err(TrustchainHTTPError::FailedToDeserialize)?; + info!("Received verification information:\n{verification_info_json}",); - fn verify_presentation(_presentation: &Presentation) -> Result<(), PresentationError> { - todo!() + match verification_info.presentation_or_credential { + PresentationOrCredential::Presentation(ref presentation) => { + TrustchainVerifierHTTPHandler::verify_presentation( + presentation, + app_state + .config + .root_event_time + .ok_or(TrustchainHTTPError::RootEventTimeNotSet)?, + &app_state.verifier, + ) + .await + .map(|_| { + info!("Presentation verification...ok ✅:\n{verification_info_json}"); + (StatusCode::OK, Html("Presentation received and verified!")) + }) + .map_err(|err| { + info!("Presentation verification...error ❌:\n{err}"); + err + }) + } + PresentationOrCredential::Credential(ref credential) => { + TrustchainVerifierHTTPHandler::verify_credential( + credential, + app_state + .config + .root_event_time + .ok_or(TrustchainHTTPError::RootEventTimeNotSet)?, + &app_state.verifier, + ) + .await + .map(|_| { + info!("Credential verification...ok ✅:\n{verification_info_json}"); + (StatusCode::OK, Html("Credential received and verified!")) + }) + .map_err(|err| { + info!("Credential verification...error ❌:\n{err}"); + err + }) + } + } } - fn verify_credential(_credential: &Credential) -> Result<(), PresentationError> { - todo!() + /// Generates a QR code for receiving requests, default to first request in cache + pub async fn get_verifier_qrcode(State(app_state): State>) -> impl IntoResponse { + app_state + .presentation_requests + .iter() + .next() + .ok_or(TrustchainHTTPError::RequestDoesNotExist) + .map(|(uid, _)| { + let qr_code_str = if http_config().verifiable_endpoints.unwrap_or(true) { + serde_json::to_string(&DIDQRCode { + did: app_state.config.server_did.as_ref().unwrap().to_owned(), + route: "/vc/verifier/".to_string(), + id: uid.to_owned(), + }) + .unwrap() + } else { + format!( + "{}://{}:{}/vc/verifier/{uid}", + http_config().http_scheme(), + app_state.config.host_display, + app_state.config.port + ) + }; + ( + StatusCode::OK, + Html(str_to_qr_code_html(&qr_code_str, "Verifier")), + ) + }) } } -impl TrustchainVerifierHTTPHandler { - /// API endpoint taking the UUID of a VC. Response is the VC JSON. - // TODO: identify how to handle multiple string variables - pub async fn get_verifier() -> Html { - // Return the presentation request - // (StatusCode::OK, Json(EXAMPLE_VP_REQUEST)) - Html(EXAMPLE_VP_REQUEST.to_string()) +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + config::HTTPConfig, errors::TrustchainHTTPError, server::TrustchainRouter, state::AppState, + }; + use axum_test_helper::TestClient; + use hyper::StatusCode; + use lazy_static::lazy_static; + use serde_json::json; + use std::{collections::HashMap, sync::Arc}; + + lazy_static! { + /// Lazy static reference to core configuration loaded from `trustchain_config.toml`. + pub static ref TEST_HTTP_CONFIG: HTTPConfig = HTTPConfig { + server_did: Some("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string()), + root_event_time: Some(1666265405), + ..Default::default() + }; } - pub async fn post_verifier(Json(info): Json) -> impl IntoResponse { - info!( - "Received credential at presentation:\n{}", - serde_json::to_string_pretty(&info).unwrap() - ); - // TODO: check whether a specific response body is required - // See [here](https://w3c-ccg.github.io/vc-api/#prove-presentation) - (StatusCode::OK, "Received!") + const REQUESTS: &str = r#" + { + "b9519df2-35c1-11ee-8314-7f66e4585b4f": { + "type": "VerifiablePresentationRequest", + "query": [ + { + "type": "QueryByExample", + "credentialQuery": { + "reason": "Request credential", + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": "VerifiableCredential" + } + } + } + ], + "challenge": "a877fb0a-11dd-11ee-9df7-9be7abdeee2d", + "domain": "https://alan-turing-institute.github.io/trustchain" + } + } + "#; + + const TEST_POST_VERIFIER_CREDENTIAL: &str = r#" + { + "presentationOrCredential": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "urn:uuid:46cb84e2-fa10-11ed-a0d4-bbb4e61d1556", + "type": ["VerifiableCredential"], + "credentialSubject": { + "id": "did:example:284b3f34fad911ed9aea439566dd422a", + "familyName": "Bloggs", + "degree": { + "college": "University of Oxbridge", + "name": "Bachelor of Arts", + "type": "BachelorDegree" + }, + "givenName": "Jane" + }, + "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "issuanceDate": "2023-08-08T08:59:21.458576Z", + "proof": { + "type": "EcdsaSecp256k1Signature2019", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI", + "created": "2023-08-08T08:59:21.461Z", + "jws": "eyJhbGciOiJFUzI1NksiLCJjcml0IjpbImI2NCJdLCJiNjQiOmZhbHNlfQ..LqLHztj2djQ9aWDGFjm3ZaOzDFIVKnOyZQVvE7CMDbYV5POYz6IejwnRkcqRf7uPYc2QbJAqCjj20PfwTOPJEw" + } + } + }, + "rootEventTime": 1666265405 } + "#; + + const TEST_POST_VERIFIER_PRESENTATION: &str = r#" + { + "presentationOrCredential": { + "presentation": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/citizenship/v1" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "familyName": "Doe", + "givenName": "Jane", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + "college": "College of Engineering" + } + }, + "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "issuanceDate": "2023-09-06T12:15:08.630033Z", + "proof": { + "type": "EcdsaSecp256k1Signature2019", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ", + "created": "2023-09-08T07:50:31.529Z", + "jws": "eyJhbGciOiJFUzI1NksiLCJjcml0IjpbImI2NCJdLCJiNjQiOmZhbHNlfQ..AOodNoJ20UJtVK1UFsMXxr2kVpurIGjLCvTmwZKs_ahVO9GWPH05ZpM14VLanCK33K0AR6mlSna5y7DwfojDEw" + }, + "credentialSchema": { + "id": "did:example:cdf:35LB7w9ueWbagPL94T9bMLtyXDj9pX5o", + "type": "did:example:schema:22KpkXgecryx9k7N6XN1QoN3gXwBkSU8SfyyYQG" + }, + "image": "some_base64_representation" + } + ], + "proof": { + "type": "EcdsaSecp256k1Signature2019", + "proofPurpose": "authentication", + "verificationMethod": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI", + "created": "2023-09-08T07:50:31.619Z", + "jws": "eyJhbGciOiJFUzI1NksiLCJjcml0IjpbImI2NCJdLCJiNjQiOmZhbHNlfQ..tXGzMYY9jdyK_fy-h99XbmUNM-V3LOtNgP_0LfhVPHBHH57TKzqAv7AWPUl4Jhqvc1L3RrvJcdwyHnZnubccvg" + }, + "holder": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q" + } + }, + "rootEventTime": 1666265405 + } + "#; + + // Verifier integration tests + #[tokio::test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + async fn test_get_verifier_request() { + let state = Arc::new(AppState::new_with_cache( + TEST_HTTP_CONFIG.to_owned(), + HashMap::new(), + serde_json::from_str(REQUESTS).unwrap(), + )); + // Test response for request in cache + let app = TrustchainRouter::from(state.clone()).into_router(); + let id = "b9519df2-35c1-11ee-8314-7f66e4585b4f"; + let path = format!("/vc/verifier/{id}"); + let client = TestClient::new(app); + let response = client.get(&path).send().await; + + // Test response is OK + assert_eq!(response.status(), StatusCode::OK); - pub async fn get_verifier_qrcode(State(app_state): State>) -> Html { - // Generate a QR code for server address and combination of name and UUID - let address_str = format!( - "http://{}:{}/vc/verifier", - app_state.config.host_reference, app_state.config.port + // Test response json same as cache + let expected_request = state.presentation_requests.get(id).unwrap(); + let actual_request = response.json::().await; + assert_eq!(&actual_request, expected_request); + + // Test response for non-existent request + let app = TrustchainRouter::from(state.clone()).into_router(); + let id = "dd2f6d68-35c5-11ee-98c7-d317dc01648b"; + let path = format!("/vc/verifier/{id}"); + let client = TestClient::new(app); + let response = client.get(&path).send().await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.text().await, + json!({"error":TrustchainHTTPError::RequestDoesNotExist.to_string()}).to_string() ); + } - // Respond with the QR code as a png embedded in html - Html(str_to_qr_code_html(&address_str, "Verifier")) + #[tokio::test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + async fn test_post_verifier_credential() { + let state = Arc::new(AppState::new_with_cache( + TEST_HTTP_CONFIG.to_owned(), + HashMap::new(), + serde_json::from_str(REQUESTS).unwrap(), + )); + // Test post of credential to verifier + let app = TrustchainRouter::from(state.clone()).into_router(); + let id = "b9519df2-35c1-11ee-8314-7f66e4585b4f"; + let path = format!("/vc/verifier/{id}"); + let client = TestClient::new(app); + let post_verifier: PostVerifier = + serde_json::from_str(TEST_POST_VERIFIER_CREDENTIAL).unwrap(); + let response = client.post(&path).json(&post_verifier).send().await; + assert_eq!(response.status(), StatusCode::OK); + assert_eq!("Credential received and verified!", response.text().await); } -} -#[cfg(test)] -mod tests { - // TODO: complete tests as part of verifier completion (#56) - // // Verifier integration tests - // #[tokio::test] - // #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] - // async fn test_get_verifier_request() { - // todo!() - // } - - // #[tokio::test] - // #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] - // async fn test_post_verifier_credential() { - // todo!() - // } + #[tokio::test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + async fn test_post_verifier_presentation() { + let state = Arc::new(AppState::new_with_cache( + TEST_HTTP_CONFIG.to_owned(), + HashMap::new(), + serde_json::from_str(REQUESTS).unwrap(), + )); + // Test post of presentation to verifier + let app = TrustchainRouter::from(state.clone()).into_router(); + let id = "b9519df2-35c1-11ee-8314-7f66e4585b4f"; + let path = format!("/vc/verifier/{id}"); + let client = TestClient::new(app); + let post_verifier: PostVerifier = + serde_json::from_str(TEST_POST_VERIFIER_PRESENTATION).unwrap(); + let response = client.post(&path).json(&post_verifier).send().await; + assert_eq!(response.status(), StatusCode::OK); + assert_eq!("Presentation received and verified!", response.text().await); + } } diff --git a/trustchain-http/static/index.html b/trustchain-http/static/index.html index 93238111..8ddd08da 100644 --- a/trustchain-http/static/index.html +++ b/trustchain-http/static/index.html @@ -5,15 +5,32 @@ VC server - Receive a credential: -
- +
+
+
+
+
+

Receive a credential

+ + + +
+
+

- Present a credential: +
+ +
+
+
+

Present a credential

-
+
+
+
+
diff --git a/trustchain-ion/Cargo.toml b/trustchain-ion/Cargo.toml index f882bd07..db1d2a26 100644 --- a/trustchain-ion/Cargo.toml +++ b/trustchain-ion/Cargo.toml @@ -6,14 +6,16 @@ edition = "2021" [dependencies] +flutter_rust_bridge = "1" trustchain-core = { path = "../trustchain-core" } +anyhow = "1.0" async-trait = "0.1" bitcoin = "0.29.2" bitcoincore-rpc = "0.16.0" canonical_json = "0.4.0" -clap = { version = "~4.0", features=["derive", "cargo"] } chrono = "0.4" -did-ion="0.1.0" +clap = { version = "^4.1", features=["derive", "cargo"] } +did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} flate2 = "1.0.24" futures = "0.3.21" hex = "0.4.3" @@ -26,7 +28,7 @@ serde = { version = "1.0", features = ["derive"] } serde_jcs = "0.1.0" serde_json = "1.0" sha2 = "0.10.7" -ssi = { version = "0.4", features = ["http-did", "secp256k1"] } +ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} thiserror = "1.0" toml="0.7.2" tokio = {version = "1.20.1", features = ["full"]} diff --git a/trustchain-ion/src/attest.rs b/trustchain-ion/src/attest.rs index e880b958..1481856c 100644 --- a/trustchain-ion/src/attest.rs +++ b/trustchain-ion/src/attest.rs @@ -1,8 +1,8 @@ //! ION operation for DID attestation. +use crate::ion::IONTest as ION; use did_ion::sidetree::DIDStatePatch; use did_ion::sidetree::PublicKeyJwk; use did_ion::sidetree::{DIDSuffix, Operation, Sidetree}; -use did_ion::ION; use serde_json::to_string_pretty as to_json; use std::convert::TryFrom; use trustchain_core::controller::Controller; diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index e6131195..958a2eab 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -1,9 +1,10 @@ //! Implementation of `Attestor` API for ION DID method. +use crate::ion::IONTest as ION; use async_trait::async_trait; use did_ion::sidetree::Sidetree; -use did_ion::ION; use ssi::did::Document; use ssi::did_resolve::DIDResolver; +use ssi::jsonld::ContextLoader; use ssi::vc::{Credential, LinkedDataProofOptions, Presentation, URI}; use ssi::{jwk::JWK, one_or_many::OneOrMany}; use std::convert::TryFrom; @@ -149,6 +150,7 @@ impl Issuer for IONAttestor { linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, + context_loader: &mut ContextLoader, ) -> Result { // Get the signing key. let signing_key = self.signing_key(key_id)?; @@ -157,8 +159,9 @@ impl Issuer for IONAttestor { let proof = credential .generate_proof( &signing_key, - &linked_data_proof_options.unwrap_or_default(), + &linked_data_proof_options.unwrap_or(LinkedDataProofOptions::default()), resolver, + context_loader, ) .await?; @@ -185,6 +188,7 @@ impl Holder for IONAttestor { linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, + context_loader: &mut ContextLoader, ) -> Result { // If no ldp options passed, use default with ProofPurpose::Authentication. let options = linked_data_proof_options.unwrap_or(LinkedDataProofOptions { @@ -207,7 +211,9 @@ impl Holder for IONAttestor { }; // Generate proof - let proof = vp.generate_proof(&signing_key, &options, resolver).await?; + let proof = vp + .generate_proof(&signing_key, &options, resolver, context_loader) + .await?; // Add proof to credential vp.add_proof(proof); Ok(vp) @@ -267,9 +273,9 @@ mod tests { // Check signature let proof_result = result?; - let valid_decoded: Result = + let valid_decoded: Result = ssi::jwt::decode_verify(&proof_result, valid_key); - let invalid_decoded: Result = + let invalid_decoded: Result = ssi::jwt::decode_verify(&proof_result, invalid_key); assert!(valid_decoded.is_ok()); assert!(invalid_decoded.is_err()); @@ -309,7 +315,9 @@ mod tests { let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); // Attest to doc - let vc_with_proof = target.sign(&vc, None, None, &resolver).await; + let vc_with_proof = target + .sign(&vc, None, None, &resolver, &mut ContextLoader::default()) + .await; // Check attest was ok assert!(vc_with_proof.is_ok()); @@ -343,13 +351,18 @@ mod tests { // Sign credential (expect failure). // Note: Signing a vc with a Some() issuer field requires a running ion node - let vc_with_proof = attestor.sign(&vc, None, None, &resolver).await; + let vc_with_proof = attestor + .sign(&vc, None, None, &resolver, &mut ContextLoader::default()) + .await; assert!(vc_with_proof.is_err()); + // Check error matches assert!(matches!( vc_with_proof, - Err(IssuerError::SSI(ssi::error::Error::KeyMismatch)) - )); + Err(IssuerError::LDP(ssi::ldp::Error::DID( + ssi::did::Error::KeyMismatch + ))) + )) } #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] @@ -363,7 +376,10 @@ mod tests { let holder = IONAttestor::new(holder_did); let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); - let vc_with_proof = issuer.sign(&vc, None, None, &resolver).await.unwrap(); + let vc_with_proof = issuer + .sign(&vc, None, None, &resolver, &mut ContextLoader::default()) + .await + .unwrap(); // Create Presentation, initially with holder field defaulting to None let presentation = Presentation { @@ -374,7 +390,13 @@ mod tests { // Holder field set to the DID of the signing holder by 'sign_presentation' // The DID is resolved during signing, which requires a running ion node. let vp = holder - .sign_presentation(&presentation, None, None, &resolver) + .sign_presentation( + &presentation, + None, + None, + &resolver, + &mut ContextLoader::default(), + ) .await; assert!(vp.is_ok()); diff --git a/trustchain-ion/src/controller.rs b/trustchain-ion/src/controller.rs index ffcd9f55..45235969 100644 --- a/trustchain-ion/src/controller.rs +++ b/trustchain-ion/src/controller.rs @@ -1,8 +1,8 @@ //! Implementation of `Controller` API for ION DID method. use crate::attestor::IONAttestor; +use crate::ion::IONTest as ION; use crate::TrustchainIONError; use did_ion::sidetree::{DIDStatePatch, PublicKeyJwk, ServiceEndpointEntry, Sidetree}; -use did_ion::ION; use serde_json::{Map, Value}; use ssi::did::ServiceEndpoint; use ssi::did_resolve::{DocumentMetadata, Metadata}; diff --git a/trustchain-ion/src/create.rs b/trustchain-ion/src/create.rs index 4d3436d5..6118e657 100644 --- a/trustchain-ion/src/create.rs +++ b/trustchain-ion/src/create.rs @@ -1,10 +1,10 @@ //! ION operation for DID creation. use crate::attestor::{AttestorData, IONAttestor}; use crate::controller::{ControllerData, IONController}; +use crate::ion::IONTest as ION; use did_ion::sidetree::DIDStatePatch; use did_ion::sidetree::{DocumentState, PublicKeyEntry, PublicKeyJwk}; use did_ion::sidetree::{Operation, Sidetree, SidetreeDID, SidetreeOperation}; -use did_ion::ION; use serde_json::to_string_pretty as to_json; use ssi::jwk::JWK; use ssi::one_or_many::OneOrMany; diff --git a/trustchain-ion/src/ion.rs b/trustchain-ion/src/ion.rs new file mode 100644 index 00000000..2e9900fe --- /dev/null +++ b/trustchain-ion/src/ion.rs @@ -0,0 +1,32 @@ +use anyhow::{anyhow, Context, Result}; +use did_ion::sidetree::{is_secp256k1, Sidetree, SidetreeClient, SidetreeError}; +use ssi::jwk::{Algorithm, JWK}; + +pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +/// Type for ION test network given [did-ion-0.2.0](https://github.com/spruceid/ssi/blob/79ad4a679933d1b4f6af93193408cd0a24e68f74/did-ion/src/lib.rs) +/// now uses main network. +#[derive(Clone)] +pub struct IONTest; + +/// did:ion:test Method +pub type DIDIONTest = SidetreeClient; + +impl Sidetree for IONTest { + fn generate_key() -> Result { + let key = JWK::generate_secp256k1().context("Generate secp256k1 key")?; + Ok(key) + } + + fn validate_key(key: &JWK) -> Result<(), SidetreeError> { + if !is_secp256k1(key) { + return Err(anyhow!("Key must be Secp256k1").into()); + } + Ok(()) + } + + const SIGNATURE_ALGORITHM: Algorithm = Algorithm::ES256K; + const METHOD: &'static str = "ion"; + // Specify "test" network. + const NETWORK: Option<&'static str> = Some("test"); +} diff --git a/trustchain-ion/src/lib.rs b/trustchain-ion/src/lib.rs index 9489f784..258523df 100644 --- a/trustchain-ion/src/lib.rs +++ b/trustchain-ion/src/lib.rs @@ -7,11 +7,13 @@ pub mod controller; pub mod create; pub mod data; pub mod root; +pub mod ion; pub mod sidetree; pub mod utils; pub mod verifier; -use did_ion::{sidetree::SidetreeClient, ION}; +use crate::ion::IONTest as ION; +use did_ion::sidetree::SidetreeClient; use serde::{Deserialize, Serialize}; use std::{io, num::ParseIntError}; use thiserror::Error; @@ -21,7 +23,7 @@ use trustchain_core::resolver::{DIDMethodWrapper, Resolver}; pub type IONResolver = Resolver>>; /// Type alias for URL -// TODO: remove in favour of new type pattern (e.g. URL(String)) or use https://crates.io/crates/url +// TODO [#126]: remove in favour of new type pattern (e.g. URL(String)) or use https://crates.io/crates/url // for better handling of URLs. pub type URL = String; @@ -37,10 +39,13 @@ impl Endpoint { pub fn new(url: String, port: u16) -> Self { Self { host: url, port } } + // TODO: add more flexible address handling pub fn to_address(&self) -> String { - format!("http://{}:{}/", self.host, self.port) + match self.host.starts_with("http") { + true => format!("{}:{}/", self.host, self.port), + false => format!("http://{}:{}/", self.host, self.port), + } } - // TODO: add more flexible address methods } /// Test resolver diff --git a/trustchain-ion/src/sidetree.rs b/trustchain-ion/src/sidetree.rs index d776f16c..04250868 100644 --- a/trustchain-ion/src/sidetree.rs +++ b/trustchain-ion/src/sidetree.rs @@ -1,8 +1,6 @@ //! Data structures for deserializing Sidetree IPFS data. -use did_ion::{ - sidetree::{Delta, Sidetree, SuffixData}, - ION, -}; +use crate::ion::IONTest as ION; +use did_ion::sidetree::{Delta, Sidetree, SuffixData}; use serde::{Deserialize, Serialize}; use trustchain_core::{commitment::CommitmentError, utils::get_did_suffix}; diff --git a/trustchain-ion/src/verifier.rs b/trustchain-ion/src/verifier.rs index 41482ff1..11107bc8 100644 --- a/trustchain-ion/src/verifier.rs +++ b/trustchain-ion/src/verifier.rs @@ -370,7 +370,7 @@ where // in-memory verifier HashMap. // If running on a Trustchain light client, make an API call to a full node to request the bundle. pub async fn fetch_bundle(&self, did: &str) -> Result<(), VerifierError> { - let response = reqwest::get(format!("{}{did}", self.endpoint())) + let response = reqwest::get(format!("{}did/bundle/{did}", self.endpoint())) .await .map_err(|e| { VerifierError::ErrorFetchingVerificationMaterial(