From aaf24bb6df6ec870d4254fda15f9f35812f3dc59 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 28 Jul 2023 17:27:39 +0100 Subject: [PATCH 01/26] Initial TrustchainVPAPI, holder trait, presentation error --- Cargo.toml | 2 +- trustchain-api/src/api.rs | 63 ++++++++++++++++++++++++++++++---- trustchain-core/src/holder.rs | 44 ++++++++++++++++++++++++ trustchain-core/src/issuer.rs | 2 +- trustchain-core/src/lib.rs | 2 ++ trustchain-core/src/vc.rs | 2 +- trustchain-core/src/vp.rs | 24 +++++++++++++ trustchain-ion/src/attestor.rs | 51 ++++++++++++++++++++++++++- trustchain-ion/tests/vc.rs | 7 ++++ 9 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 trustchain-core/src/holder.rs create mode 100644 trustchain-core/src/vp.rs diff --git a/Cargo.toml b/Cargo.toml index 13190fbd..e1e6951f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,6 @@ members = [ "trustchain-ion", "trustchain-http", "trustchain-api", - "trustchain-cli", + # "trustchain-cli", "trustchain-ffi" ] diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index a7d88dc3..a1550c36 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -3,16 +3,18 @@ use did_ion::sidetree::DocumentState; use ssi::{ did_resolve::DIDResolver, ldp::LinkedDataDocument, - vc::LinkedDataProofOptions, - vc::{Credential, URI}, + vc::{Credential, CredentialOrJWT, URI}, + vc::{LinkedDataProofOptions, Presentation}, }; use std::error::Error; use trustchain_core::{ chain::DIDChain, + holder::Holder, issuer::{Issuer, IssuerError}, resolver::ResolverResult, vc::CredentialError, verifier::{Timestamp, Verifier, VerifierError}, + vp::PresentationError, }; use trustchain_ion::{ attest::attest_operation, attestor::IONAttestor, create::create_operation, get_ion_resolver, @@ -24,7 +26,7 @@ use trustchain_ion::{ pub trait TrustchainDIDAPI { /// Creates a controlled DID from a passed document state, writing the associated create operation /// to file in the operations path returning the file name including the created DID suffix. - // TODO: make pecific error? + // TODO: make specific error? fn create( document_state: Option, verbose: bool, @@ -92,13 +94,13 @@ pub trait TrustchainVCAPI { attestor.sign(&credential, key_id, &resolver).await } - /// Verifies a credential + /// Verifies a credential and returns a `DIDChain` if valid. async fn verify_credential( credential: &Credential, ldp_options: Option, root_event_time: Timestamp, verifier: &IONVerifier, - ) -> Result<(), CredentialError> { + ) -> Result { // Verify signature let result = credential.verify(ldp_options, verifier.resolver()).await; if !result.errors.is_empty() { @@ -108,7 +110,56 @@ pub trait TrustchainVCAPI { let issuer = credential .get_issuer() .ok_or(CredentialError::NoIssuerPresent)?; - verifier.verify(issuer, root_event_time).await?; + Ok(verifier.verify(issuer, root_event_time).await?) + } +} +#[async_trait] +pub trait TrustchainVPAPI { + /// As a holder issue a verifiable presentation. + async fn sign_presentation( + mut presentation: Presentation, + did: &str, + key_id: Option<&str>, + endpoint: &str, + ) -> Result { + let resolver = get_ion_resolver(endpoint); + let attestor = IONAttestor::new(did); + Ok(attestor + .sign_presentation(&presentation, key_id, &resolver) + .await?) + } + /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). + async fn verify_presentation( + presentation: &Presentation, + ldp_options: Option, + root_event_time: Timestamp, + verifier: &IONVerifier, + ) -> Result<(), PresentationError> { + // let credentials = presentation + // .verifiable_credential + // .ok_or(PresentationError::NoCredentialsPresent)?; + // let valid = credentials + // .into_iter() + // .map(|credential_or_jwt| { + // let valid = match credential_or_jwt { + // CredentialOrJWT::Credential(credential) => TrustchainVCAPI::verify_credential( + // &credential, + // ldp_options, + // root_event_time, + // verifier, + // ) + // .await + // .is_ok(), + // CredentialOrJWT::JWT(jwt) => Credential::verify_jwt(jwt, options_opt, verifier) + // .await + // .errors + // .is_empty(), + // }; + // }) + // .all(|res| res.is_ok()); + // if valid { + // Ok(()) + // } Ok(()) } } diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs new file mode 100644 index 00000000..53e301f0 --- /dev/null +++ b/trustchain-core/src/holder.rs @@ -0,0 +1,44 @@ +//! DID issuer API. +use crate::key_manager::KeyManagerError; +use crate::subject::Subject; +use async_trait::async_trait; +use ssi::did_resolve::DIDResolver; +use ssi::vc::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.")] + SSI(ssi::error::Error), + /// Wrapped error for key manager error. + #[error("A wrapped variant for a key manager error.")] + KeyManager(KeyManagerError), +} + +impl From for HolderError { + fn from(err: ssi::error::Error) -> Self { + HolderError::SSI(err) + } +} + +impl From for HolderError { + fn from(err: KeyManagerError) -> Self { + HolderError::KeyManager(err) + } +} + +/// A holder signs a credential to generate a verifiable credential. +#[async_trait] +pub trait Holder: Subject { + /// Attests to a given presentation of one or many credentials returning the presentation with a + /// proof. The `@context` of the presentation has linked-data fields strictly checked as part of + /// proof generation. + async fn sign_presentation( + &self, + presentation: &Presentation, + key_id: Option<&str>, + resolver: &T, + ) -> Result; +} diff --git a/trustchain-core/src/issuer.rs b/trustchain-core/src/issuer.rs index 6777aeba..2b7f271a 100644 --- a/trustchain-core/src/issuer.rs +++ b/trustchain-core/src/issuer.rs @@ -3,7 +3,7 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; -use ssi::vc::Credential; +use ssi::vc::{Credential, Presentation}; use thiserror::Error; /// An error relating to a Trustchain Issuer. diff --git a/trustchain-core/src/lib.rs b/trustchain-core/src/lib.rs index 8766cc08..7fa44522 100644 --- a/trustchain-core/src/lib.rs +++ b/trustchain-core/src/lib.rs @@ -6,6 +6,7 @@ pub mod controller; pub mod data; pub mod display; pub mod graph; +pub mod holder; pub mod issuer; pub mod key_manager; pub mod resolver; @@ -13,6 +14,7 @@ pub mod subject; pub mod utils; pub mod vc; pub mod verifier; +pub mod vp; /// Environment variable name for Trustchain data. pub const TRUSTCHAIN_DATA: &str = "TRUSTCHAIN_DATA"; diff --git a/trustchain-core/src/vc.rs b/trustchain-core/src/vc.rs index 390b4f4d..2ce30f72 100644 --- a/trustchain-core/src/vc.rs +++ b/trustchain-core/src/vc.rs @@ -1,4 +1,4 @@ -//! Verifiable credential and presentation functionality for Trustchain. +//! Verifiable credential functionality for Trustchain. use crate::verifier::VerifierError; use ssi::vc::VerificationResult; use thiserror::Error; diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs new file mode 100644 index 00000000..d153f216 --- /dev/null +++ b/trustchain-core/src/vp.rs @@ -0,0 +1,24 @@ +//! Verifiable presentation functionality for Trustchain. +use thiserror::Error; + +use crate::holder::HolderError; + +/// An error relating to verifiable credentials and presentations. +#[derive(Error, Debug)] +pub enum PresentationError { + /// No credentials present in presentation. + #[error("No credentials.")] + NoCredentialsPresent, + /// Wrapped variant for Trustchain holder. + #[error("A wrapped Trustchain holder error: {0}")] + HolderError(HolderError), +} + +impl From for PresentationError { + fn from(err: HolderError) -> Self { + PresentationError::HolderError(err) + } +} + +#[cfg(test)] +mod tests {} diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index e7d37a66..01c837f5 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -4,9 +4,10 @@ use did_ion::sidetree::Sidetree; use did_ion::ION; use ssi::did::Document; use ssi::did_resolve::DIDResolver; -use ssi::vc::{Credential, LinkedDataProofOptions}; +use ssi::vc::{Credential, LinkedDataProofOptions, Presentation}; use ssi::{jwk::JWK, one_or_many::OneOrMany}; use std::convert::TryFrom; +use trustchain_core::holder::{Holder, HolderError}; use trustchain_core::issuer::{Issuer, IssuerError}; use trustchain_core::key_manager::KeyType; use trustchain_core::{ @@ -163,13 +164,38 @@ impl Issuer for IONAttestor { } } +#[async_trait] +impl Holder for IONAttestor { + async fn sign_presentation( + &self, + presentation: &Presentation, + key_id: Option<&str>, + resolver: &T, + ) -> Result { + // Get the signing key. + let signing_key = self.signing_key(key_id)?; + + // Generate proof + let proof = presentation + .generate_proof(&signing_key, &LinkedDataProofOptions::default(), resolver) + .await?; + + // Add proof to credential + let mut vp = presentation.clone(); + vp.add_proof(proof); + Ok(vp) + } +} + #[cfg(test)] mod tests { use super::*; use crate::get_ion_resolver; use ssi::did::Document; + use ssi::vc::CredentialOrJWT; use trustchain_core::data::{TEST_CREDENTIAL, TEST_SIGNING_KEYS, TEST_TRUSTCHAIN_DOCUMENT}; use trustchain_core::utils::init; + use trustchain_core::vp; #[test] fn test_try_from() -> Result<(), Box> { @@ -263,6 +289,29 @@ mod tests { assert!(vc_with_proof.is_ok()); } + #[tokio::test] + async fn test_attest_presentation() { + init(); + let resolver = get_ion_resolver("http://localhost:3000/"); + let did = "did:example:test_attest_presentation"; + let target = IONAttestor::try_from(AttestorData::new( + did.to_string(), + serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), + )) + .unwrap(); + let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); + let vc_with_proof = target.sign(&vc, None, &resolver).await.unwrap(); + let presentation = Presentation { + verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc_with_proof))), + ..Default::default() + }; + // Attest to vp: + assert!(target + .sign_presentation(&presentation, None, &resolver) + .await + .is_ok()); + } + #[test] fn test_signing_key() -> Result<(), Box> { // Initialize temp path for saving keys diff --git a/trustchain-ion/tests/vc.rs b/trustchain-ion/tests/vc.rs index 8fa42488..289d0686 100644 --- a/trustchain-ion/tests/vc.rs +++ b/trustchain-ion/tests/vc.rs @@ -95,3 +95,10 @@ async fn test_sign_credential_failure() { Err(IssuerError::SSI(ssi::error::Error::KeyMismatch)) )); } + +// TODO: add VP integration test +#[ignore = "requires a running Sidetree node listening on http://localhost:3000"] +#[tokio::test] +async fn test_sign_presentation() { + todo!() +} From 079699e35ddbe3326e53d084584d2714c684d8b1 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Tue, 1 Aug 2023 16:01:17 +0100 Subject: [PATCH 02/26] concurrently verify iterator over CredentialOrJWT with type annotation bug --- trustchain-api/Cargo.toml | 1 + trustchain-api/src/api.rs | 83 ++++++++++++++++++++++++++++------- trustchain-core/src/issuer.rs | 2 +- trustchain-core/src/vp.rs | 12 ++++- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/trustchain-api/Cargo.toml b/trustchain-api/Cargo.toml index 99217beb..9acce939 100644 --- a/trustchain-api/Cargo.toml +++ b/trustchain-api/Cargo.toml @@ -16,3 +16,4 @@ ssi = "0.4" clap = { version = "~4.0", features=["derive", "cargo"] } serde_json = "1.0" did-ion = "0.1.0" +futures = "0.3.28" diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index a1550c36..4fbedd96 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use did_ion::sidetree::DocumentState; +use futures::{stream, StreamExt, TryStreamExt}; use ssi::{ did_resolve::DIDResolver, ldp::LinkedDataDocument, @@ -135,32 +136,84 @@ pub trait TrustchainVPAPI { root_event_time: Timestamp, verifier: &IONVerifier, ) -> Result<(), PresentationError> { - // let credentials = presentation - // .verifiable_credential - // .ok_or(PresentationError::NoCredentialsPresent)?; - // let valid = credentials - // .into_iter() - // .map(|credential_or_jwt| { - // let valid = match credential_or_jwt { + let credentials = presentation + .verifiable_credential + .as_ref() + .ok_or(PresentationError::NoCredentialsPresent)?; + + // Attempt 2: + // stream.map(Ok).try_for_each_concurrent().await + // https://docs.rs/futures-util/latest/futures_util/stream/trait.TryStreamExt.html#method.try_for_each_concurrent + // TODO consider concurrency limit (as rate limiting for verifier requests) + // let limit = None; + // stream::iter(credentials.into_iter()) + // .map(Ok) + // .try_for_each_concurrent(limit, |credential_or_jwt| async { + // match credential_or_jwt { // CredentialOrJWT::Credential(credential) => TrustchainVCAPI::verify_credential( - // &credential, + // credential, // ldp_options, // root_event_time, // verifier, // ) // .await - // .is_ok(), - // CredentialOrJWT::JWT(jwt) => Credential::verify_jwt(jwt, options_opt, verifier) + // .map(|_| ()) + // .map_err(|err| err.into()), + // CredentialOrJWT::JWT(jwt) => { + // let result = + // Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; + // if !result.errors.is_empty() { + // Err(PresentationError::CredentialError( + // CredentialError::VerificationResultError(result), + // )) + // } else { + // Ok(()) + // } + // } + // } + // }) + // .await + Ok(()) + + // // Attempt 1: + // // let valid = stream.then().all().await + + // // .then() returns an iterator over future, each of which is awaited in turn + // // [stream::].all() returns a future + // // disadvantages: + // // - all credentials are checked before returning + // // - difficult/not possible? to propagate errors out of closures to know which credential + // // failed + // let valid = stream::iter(credentials.into_iter()) + // .then(|credential_or_jwt| async { + // match credential_or_jwt { + // CredentialOrJWT::Credential(credential) => { + // TrustchainVCAPI::verify_credential::( + // credential, + // ldp_options, + // root_event_time, + // verifier, + // ) // .await - // .errors - // .is_empty(), - // }; + // .map(|_| true)? + // } + // CredentialOrJWT::JWT(jwt) => { + // let result = + // Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; + // if !result.errors.is_empty() { + // return Err(PresentationError::VerificationResultError(result)); + // } + // } + // } // }) - // .all(|res| res.is_ok()); + // .all(|validity| async { validity == true }) + // .await; + // if valid { // Ok(()) + // } else { + // Err(PresentationError::NoCredentialsPresent) // } - Ok(()) } } diff --git a/trustchain-core/src/issuer.rs b/trustchain-core/src/issuer.rs index 2b7f271a..6777aeba 100644 --- a/trustchain-core/src/issuer.rs +++ b/trustchain-core/src/issuer.rs @@ -3,7 +3,7 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; -use ssi::vc::{Credential, Presentation}; +use ssi::vc::Credential; use thiserror::Error; /// An error relating to a Trustchain Issuer. diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index d153f216..6bdf6e52 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -1,8 +1,7 @@ //! Verifiable presentation functionality for Trustchain. +use crate::{holder::HolderError, vc::CredentialError}; use thiserror::Error; -use crate::holder::HolderError; - /// An error relating to verifiable credentials and presentations. #[derive(Error, Debug)] pub enum PresentationError { @@ -12,6 +11,9 @@ pub enum PresentationError { /// Wrapped variant for Trustchain holder. #[error("A wrapped Trustchain holder error: {0}")] HolderError(HolderError), + /// Wrapped variant for Crediential Error. + #[error("A wrapped Credential error: {0}")] + CredentialError(CredentialError), } impl From for PresentationError { @@ -20,5 +22,11 @@ impl From for PresentationError { } } +impl From for PresentationError { + fn from(err: CredentialError) -> Self { + PresentationError::CredentialError(err) + } +} + #[cfg(test)] mod tests {} From bccb29e24bf2478878d2e6eb32d402f84c680516 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Wed, 2 Aug 2023 15:46:29 +0100 Subject: [PATCH 03/26] bug fix stream processing for verify_presentation Co-authored-by: Sam Greenbury Co-authored-by: lukehare --- trustchain-api/Cargo.toml | 3 + trustchain-api/src/api.rs | 115 +++++++++++++++----------------------- 2 files changed, 48 insertions(+), 70 deletions(-) diff --git a/trustchain-api/Cargo.toml b/trustchain-api/Cargo.toml index 9acce939..410fc8ec 100644 --- a/trustchain-api/Cargo.toml +++ b/trustchain-api/Cargo.toml @@ -17,3 +17,6 @@ clap = { version = "~4.0", features=["derive", "cargo"] } serde_json = "1.0" did-ion = "0.1.0" futures = "0.3.28" + +[dev-dependencies] +tokio = {version = "1.20.1", features = ["full"]} diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 4fbedd96..d8b00a9f 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -22,6 +22,8 @@ use trustchain_ion::{ verifier::IONVerifier, }; +use crate::TrustchainAPI; + /// API for Trustchain CLI DID functionality. #[async_trait] pub trait TrustchainDIDAPI { @@ -141,81 +143,54 @@ pub trait TrustchainVPAPI { .as_ref() .ok_or(PresentationError::NoCredentialsPresent)?; - // Attempt 2: - // stream.map(Ok).try_for_each_concurrent().await + // https://gendignoux.com/blog/2021/04/01/rust-async-streams-futures-part1.html#unordered-buffering-1 // https://docs.rs/futures-util/latest/futures_util/stream/trait.TryStreamExt.html#method.try_for_each_concurrent // TODO consider concurrency limit (as rate limiting for verifier requests) - // let limit = None; - // stream::iter(credentials.into_iter()) - // .map(Ok) - // .try_for_each_concurrent(limit, |credential_or_jwt| async { - // match credential_or_jwt { - // CredentialOrJWT::Credential(credential) => TrustchainVCAPI::verify_credential( - // credential, - // ldp_options, - // root_event_time, - // verifier, - // ) - // .await - // .map(|_| ()) - // .map_err(|err| err.into()), - // CredentialOrJWT::JWT(jwt) => { - // let result = - // Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; - // if !result.errors.is_empty() { - // Err(PresentationError::CredentialError( - // CredentialError::VerificationResultError(result), - // )) - // } else { - // Ok(()) - // } - // } - // } - // }) - // .await - Ok(()) - - // // Attempt 1: - // // let valid = stream.then().all().await + let limit = Some(5); + let ldp_options_vec: Vec> = (0..credentials.len()) + .map(|_| ldp_options.clone()) + .collect(); - // // .then() returns an iterator over future, each of which is awaited in turn - // // [stream::].all() returns a future - // // disadvantages: - // // - all credentials are checked before returning - // // - difficult/not possible? to propagate errors out of closures to know which credential - // // failed - // let valid = stream::iter(credentials.into_iter()) - // .then(|credential_or_jwt| async { - // match credential_or_jwt { - // CredentialOrJWT::Credential(credential) => { - // TrustchainVCAPI::verify_credential::( - // credential, - // ldp_options, - // root_event_time, - // verifier, - // ) - // .await - // .map(|_| true)? - // } - // CredentialOrJWT::JWT(jwt) => { - // let result = - // Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; - // if !result.errors.is_empty() { - // return Err(PresentationError::VerificationResultError(result)); - // } - // } - // } - // }) - // .all(|validity| async { validity == true }) - // .await; + stream::iter(credentials.into_iter().zip(ldp_options_vec)) + .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(|_| ()) + .map_err(|err| err.into()), - // if valid { - // Ok(()) - // } else { - // Err(PresentationError::NoCredentialsPresent) - // } + CredentialOrJWT::JWT(jwt) => { + let result = + Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; + if !result.errors.is_empty() { + Err(PresentationError::CredentialError( + CredentialError::VerificationResultError(result), + )) + } else { + Ok(()) + } + } + } + }) + .await } } +// TODO: add unit test for verify_credential and verify_presentation #[cfg(test)] -mod tests {} +mod tests { + #[tokio::test] + async fn test_verify_credential() { + todo!() + } + #[tokio::test] + async fn test_verify_presentation() { + todo!() + } +} From a2177911cf499114582d958240208a3d1d3933dd Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Thu, 3 Aug 2023 19:29:23 +0100 Subject: [PATCH 04/26] unit test verify_credential and verify_presentation --- trustchain-api/src/api.rs | 119 +++++++++++++++++++++++++++++++++----- trustchain-api/src/lib.rs | 3 +- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index d8b00a9f..f835c21a 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -120,7 +120,7 @@ pub trait TrustchainVCAPI { pub trait TrustchainVPAPI { /// As a holder issue a verifiable presentation. async fn sign_presentation( - mut presentation: Presentation, + presentation: Presentation, did: &str, key_id: Option<&str>, endpoint: &str, @@ -131,6 +131,7 @@ pub trait TrustchainVPAPI { .sign_presentation(&presentation, key_id, &resolver) .await?) } + // TODO: verify holder's signature /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). async fn verify_presentation( presentation: &Presentation, @@ -155,15 +156,20 @@ pub trait TrustchainVPAPI { .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(|_| ()) - .map_err(|err| err.into()), + CredentialOrJWT::Credential(credential) => { + println!("start"); + let v = TrustchainAPI::verify_credential( + credential, + ldp_options, + root_event_time, + verifier, + ) + .await + .map(|_| ()) + .map_err(|err| err.into()); + println!("done"); + v + } CredentialOrJWT::JWT(jwt) => { let result = @@ -182,15 +188,102 @@ pub trait TrustchainVPAPI { } } -// TODO: add unit test for verify_credential and verify_presentation #[cfg(test)] mod tests { + use crate::api::{TrustchainVCAPI, TrustchainVPAPI}; + use crate::TrustchainAPI; + use ssi::one_or_many::OneOrMany; + use ssi::vc::{Context, Contexts, Credential, CredentialOrJWT, JWTClaims, Presentation, URI}; + use trustchain_core::issuer::Issuer; + use trustchain_ion::attestor::IONAttestor; + use trustchain_ion::get_ion_resolver; + use trustchain_ion::verifier::IONVerifier; + + // The root event time of DID documents in `trustchain-ion/src/data.rs` used for unit tests and the test below. + const ROOT_EVENT_TIME_1: u32 = 1666265405; + + const TEST_UNSIGNED_VC: &str = r##"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/citizenship/v1" + ], + "credentialSchema": { + "id": "did:example:cdf:35LB7w9ueWbagPL94T9bMLtyXDj9pX5o", + "type": "did:example:schema:22KpkXgecryx9k7N6XN1QoN3gXwBkSU8SfyyYQG" + }, + "type": ["VerifiableCredential"], + "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "image": "some_base64_representation", + "credentialSubject": { + "givenName": "Jane", + "familyName": "Doe", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + "college": "College of Engineering" + } + } + } + "##; + #[tokio::test] async fn test_verify_credential() { - todo!() + let vc_with_proof = signed_credential().await; + let resolver = get_ion_resolver("http://localhost:3000/"); + let res = TrustchainAPI::verify_credential( + &vc_with_proof, + None, + ROOT_EVENT_TIME_1, + &IONVerifier::new(resolver), + ) + .await; + assert!(res.is_ok()); } #[tokio::test] async fn test_verify_presentation() { - todo!() + let vc_with_proof = signed_credential().await; + let resolver = get_ion_resolver("http://localhost:3000/"); + let presentation = Presentation { + context: Contexts::One(Context::URI(URI::String(String::from("test")))), + holder: None, + verifiable_credential: Some(OneOrMany::Many(vec![ + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof.clone()), + CredentialOrJWT::Credential(vc_with_proof), + ])), + id: None, + type_: OneOrMany::One(String::from("test")), + proof: None, + property_set: None, + }; + let res = TrustchainAPI::verify_presentation( + &presentation, + None, + ROOT_EVENT_TIME_1, + &IONVerifier::new(resolver), + ) + .await; + assert!(res.is_ok()); + } + + async fn signed_credential() -> Credential { + // 1. Set-up + let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + // Make resolver + let resolver = get_ion_resolver("http://localhost:3000/"); + // 2. Load Attestor + let attestor = IONAttestor::new(did); + // 3. Read credential + let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); + // Use attest_credential method instead of generating and adding proof + attestor.sign(&vc, None, &resolver).await.unwrap() } } diff --git a/trustchain-api/src/lib.rs b/trustchain-api/src/lib.rs index 4b5b4764..9c144be2 100644 --- a/trustchain-api/src/lib.rs +++ b/trustchain-api/src/lib.rs @@ -1,8 +1,9 @@ pub mod api; -use crate::api::{TrustchainDIDAPI, TrustchainVCAPI}; +use crate::api::{TrustchainDIDAPI, TrustchainVCAPI, TrustchainVPAPI}; /// A type for implementing CLI traits on. pub struct TrustchainAPI; impl TrustchainDIDAPI for TrustchainAPI {} impl TrustchainVCAPI for TrustchainAPI {} +impl TrustchainVPAPI for TrustchainAPI {} From 95763c0d57d4374bee423c954d384e495f4a0c9f Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Fri, 4 Aug 2023 15:56:36 +0100 Subject: [PATCH 05/26] Add verification for holders signature Co-authored-by: Sam Greenbury Co-authored-by: pwochner --- trustchain-api/src/api.rs | 145 +++++++++++++++++++++------------ trustchain-ion/src/attestor.rs | 14 +++- 2 files changed, 106 insertions(+), 53 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index f835c21a..436a85f1 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -116,6 +116,8 @@ pub trait TrustchainVCAPI { Ok(verifier.verify(issuer, root_event_time).await?) } } + +use ssi::ldp::now_ms; #[async_trait] pub trait TrustchainVPAPI { /// As a holder issue a verifiable presentation. @@ -131,7 +133,6 @@ pub trait TrustchainVPAPI { .sign_presentation(&presentation, key_id, &resolver) .await?) } - // TODO: verify holder's signature /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). async fn verify_presentation( presentation: &Presentation, @@ -139,6 +140,22 @@ pub trait TrustchainVPAPI { root_event_time: Timestamp, verifier: &IONVerifier, ) -> Result<(), PresentationError> { + // Verify signature + let result = presentation + .verify(ldp_options.clone(), verifier.resolver()) + .await; + println!("{:?}", result); + if !result.errors.is_empty() { + return Err(PresentationError::CredentialError( + CredentialError::VerificationResultError(result), + )); + } + // TODO: Verify holder's did + // let issuer = presentation + // .get_holder() + // .ok_or(CredentialError::NoIssuerPresent)?; + + // Verify contained credentials let credentials = presentation .verifiable_credential .as_ref() @@ -151,40 +168,47 @@ pub trait TrustchainVPAPI { let ldp_options_vec: Vec> = (0..credentials.len()) .map(|_| ldp_options.clone()) .collect(); - - stream::iter(credentials.into_iter().zip(ldp_options_vec)) + let start = now_ms(); + let out = stream::iter(credentials.into_iter().zip(ldp_options_vec)) + .enumerate() .map(Ok) - .try_for_each_concurrent(limit, |(credential_or_jwt, ldp_options)| async move { - match credential_or_jwt { - CredentialOrJWT::Credential(credential) => { - println!("start"); - let v = TrustchainAPI::verify_credential( - credential, - ldp_options, - root_event_time, - verifier, - ) - .await - .map(|_| ()) - .map_err(|err| err.into()); - println!("done"); - v - } + .try_for_each_concurrent( + limit, + |(idx, (credential_or_jwt, ldp_options))| async move { + match credential_or_jwt { + CredentialOrJWT::Credential(credential) => { + println!("start {}: {}", idx, now_ms()); + let v = TrustchainAPI::verify_credential( + credential, + ldp_options, + root_event_time, + verifier, + ) + .await + .map(|_| ()) + .map_err(|err| err.into()); + println!("done {}: {}", idx, now_ms()); + v + } - CredentialOrJWT::JWT(jwt) => { - let result = - Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; - if !result.errors.is_empty() { - Err(PresentationError::CredentialError( - CredentialError::VerificationResultError(result), - )) - } else { - Ok(()) + CredentialOrJWT::JWT(jwt) => { + let result = + Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; + if !result.errors.is_empty() { + Err(PresentationError::CredentialError( + CredentialError::VerificationResultError(result), + )) + } else { + Ok(()) + } } } - } - }) - .await + }, + ) + .await; + let end = now_ms(); + println!("Full time: {}", end - start); + out } } @@ -193,8 +217,10 @@ mod tests { use crate::api::{TrustchainVCAPI, TrustchainVPAPI}; use crate::TrustchainAPI; use ssi::one_or_many::OneOrMany; - use ssi::vc::{Context, Contexts, Credential, CredentialOrJWT, JWTClaims, Presentation, URI}; - use trustchain_core::issuer::Issuer; + use ssi::vc::{ + Credential, CredentialOrJWT, LinkedDataProofOptions, Presentation, ProofPurpose, URI, + }; + use trustchain_core::{holder::Holder, issuer::Issuer}; use trustchain_ion::attestor::IONAttestor; use trustchain_ion::get_ion_resolver; use trustchain_ion::verifier::IONVerifier; @@ -229,7 +255,9 @@ mod tests { #[tokio::test] async fn test_verify_credential() { - let vc_with_proof = signed_credential().await; + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + let issuer = IONAttestor::new(issuer_did); + let vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); let res = TrustchainAPI::verify_credential( &vc_with_proof, @@ -240,13 +268,20 @@ mod tests { .await; assert!(res.is_ok()); } + #[tokio::test] async fn test_verify_presentation() { - let vc_with_proof = signed_credential().await; + // root+1 + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + // root+2 + let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + + let issuer = IONAttestor::new(issuer_did); + let holder = IONAttestor::new(holder_did); + + let vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); - let presentation = Presentation { - context: Contexts::One(Context::URI(URI::String(String::from("test")))), - holder: None, + let mut presentation = Presentation { verifiable_credential: Some(OneOrMany::Many(vec![ CredentialOrJWT::Credential(vc_with_proof.clone()), CredentialOrJWT::Credential(vc_with_proof.clone()), @@ -259,28 +294,36 @@ mod tests { CredentialOrJWT::Credential(vc_with_proof.clone()), CredentialOrJWT::Credential(vc_with_proof), ])), - id: None, - type_: OneOrMany::One(String::from("test")), - proof: None, - property_set: None, + // NB. Holder must be specified in order to retrieve verification method to verify + // presentation. Otherwise must be specified in LinkedDataProofOptions. + holder: Some(URI::String(String::from(holder_did))), + ..Default::default() }; - let res = TrustchainAPI::verify_presentation( + + presentation = holder + .sign_presentation(&presentation, None, &resolver) + .await + .unwrap(); + println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); + // NB. If specifying a VM method + // let vm = String::from("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI"); + assert!(TrustchainAPI::verify_presentation( &presentation, - None, + // Must be specified to override default proof_purpose, which is ProofPurpose::Authentication + Some(LinkedDataProofOptions { + proof_purpose: Some(ProofPurpose::AssertionMethod), + ..Default::default() + }), ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), ) - .await; - assert!(res.is_ok()); + .await + .is_ok()); } - async fn signed_credential() -> Credential { - // 1. Set-up - let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + async fn signed_credential(attestor: IONAttestor) -> Credential { // Make resolver let resolver = get_ion_resolver("http://localhost:3000/"); - // 2. Load Attestor - let attestor = IONAttestor::new(did); // 3. Read credential let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); // Use attest_credential method instead of generating and adding proof diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 01c837f5..8ccc3437 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -171,15 +171,25 @@ impl Holder for IONAttestor { presentation: &Presentation, key_id: Option<&str>, resolver: &T, + // TODO add argument for ldp options with LDP options for proof purpose such as authentication ) -> Result { // Get the signing key. let signing_key = self.signing_key(key_id)?; // Generate proof + // Example of VM derivation + // let vm = format!("{}#{}", self.did(), signing_key.thumbprint().unwrap()); let proof = presentation - .generate_proof(&signing_key, &LinkedDataProofOptions::default(), resolver) + .generate_proof( + &signing_key, + &LinkedDataProofOptions { + // verification_method: Some(ssi::vc::URI::String(vm)), + ..LinkedDataProofOptions::default() + }, + resolver, + ) .await?; - + println!("{}", serde_json::to_string_pretty(&proof).unwrap()); // Add proof to credential let mut vp = presentation.clone(); vp.add_proof(proof); From 467ffd4210259e3484f935f65ee91dcd53273015 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Fri, 4 Aug 2023 16:05:30 +0100 Subject: [PATCH 06/26] add TODO for holder field on Presentation before signing --- trustchain-ion/src/attestor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 8ccc3437..0c8f1517 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -179,6 +179,10 @@ impl Holder for IONAttestor { // Generate proof // Example of VM derivation // let vm = format!("{}#{}", self.did(), signing_key.thumbprint().unwrap()); + + // TODO: check/add holder did to holder field of Presentation + // Must happen before .generate_proof() if that method hashes the presentation? + let proof = presentation .generate_proof( &signing_key, From c3c7c5997434502d0e263515924ea16d2b1c9223 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Mon, 7 Aug 2023 10:51:06 +0100 Subject: [PATCH 07/26] verify holder's DID --- trustchain-api/src/api.rs | 13 +++++++++---- trustchain-core/src/vp.rs | 14 +++++++++++++- trustchain-ion/src/attestor.rs | 1 - 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 436a85f1..bfc60dda 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -150,10 +150,15 @@ pub trait TrustchainVPAPI { CredentialError::VerificationResultError(result), )); } - // TODO: Verify holder's did - // let issuer = presentation - // .get_holder() - // .ok_or(CredentialError::NoIssuerPresent)?; + // Verify holder's DID + let holder = match presentation + .holder + .as_ref() + .ok_or(PresentationError::NoHolderPresent)? + { + URI::String(holder) => holder, + }; + verifier.verify(holder, root_event_time).await?; // Verify contained credentials let credentials = presentation diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index 6bdf6e52..c432e062 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -1,5 +1,5 @@ //! Verifiable presentation functionality for Trustchain. -use crate::{holder::HolderError, vc::CredentialError}; +use crate::{holder::HolderError, vc::CredentialError, verifier::VerifierError}; use thiserror::Error; /// An error relating to verifiable credentials and presentations. @@ -8,12 +8,18 @@ pub enum PresentationError { /// No credentials present in presentation. #[error("No credentials.")] NoCredentialsPresent, + /// No holder present in presentation. + #[error("No holder.")] + NoHolderPresent, /// Wrapped variant for Trustchain holder. #[error("A wrapped Trustchain holder error: {0}")] HolderError(HolderError), /// Wrapped variant for Crediential Error. #[error("A wrapped Credential error: {0}")] CredentialError(CredentialError), + /// Wrapped variant for Verifier Error. + #[error("A wrapped Verfier error: {0}")] + VerifierError(VerifierError), } impl From for PresentationError { @@ -28,5 +34,11 @@ impl From for PresentationError { } } +impl From for PresentationError { + fn from(err: VerifierError) -> Self { + PresentationError::VerifierError(err) + } +} + #[cfg(test)] mod tests {} diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 0c8f1517..bd314dc0 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -193,7 +193,6 @@ impl Holder for IONAttestor { resolver, ) .await?; - println!("{}", serde_json::to_string_pretty(&proof).unwrap()); // Add proof to credential let mut vp = presentation.clone(); vp.add_proof(proof); From e01aa5dbd07ab68f50574b956c75492d1ea91160 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Tue, 8 Aug 2023 11:30:04 +0100 Subject: [PATCH 08/26] check if holder field is populated on presentation before signing --- trustchain-ion/src/attestor.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index bd314dc0..cd2a5f1a 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -4,7 +4,7 @@ use did_ion::sidetree::Sidetree; use did_ion::ION; use ssi::did::Document; use ssi::did_resolve::DIDResolver; -use ssi::vc::{Credential, LinkedDataProofOptions, Presentation}; +use ssi::vc::{Credential, LinkedDataProofOptions, Presentation, URI}; use ssi::{jwk::JWK, one_or_many::OneOrMany}; use std::convert::TryFrom; use trustchain_core::holder::{Holder, HolderError}; @@ -176,14 +176,21 @@ impl Holder for IONAttestor { // Get the signing key. let signing_key = self.signing_key(key_id)?; + let mut vp = presentation.clone(); + // Check holder field is correctly populated + match presentation.holder.as_ref() { + Some(URI::String(holder)) => { + if holder != &self.did { + vp.holder = Some(URI::String(self.did.clone())) + } + } + None => vp.holder = Some(URI::String(self.did.clone())), + }; + // Generate proof // Example of VM derivation // let vm = format!("{}#{}", self.did(), signing_key.thumbprint().unwrap()); - - // TODO: check/add holder did to holder field of Presentation - // Must happen before .generate_proof() if that method hashes the presentation? - - let proof = presentation + let proof = vp .generate_proof( &signing_key, &LinkedDataProofOptions { @@ -194,7 +201,7 @@ impl Holder for IONAttestor { ) .await?; // Add proof to credential - let mut vp = presentation.clone(); + vp.add_proof(proof); Ok(vp) } From 07d41d880a459b69b8e5ea04944059195fe44270 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Tue, 8 Aug 2023 15:01:35 +0100 Subject: [PATCH 09/26] attempt to support unauthorised or unverified holders --- trustchain-api/src/api.rs | 115 +++++++++++++++++++++++---------- trustchain-core/src/holder.rs | 3 +- trustchain-core/src/issuer.rs | 3 +- trustchain-core/src/vp.rs | 7 ++ trustchain-http/src/issuer.rs | 2 +- trustchain-ion/src/attestor.rs | 74 +++++++++++++-------- trustchain-ion/tests/vc.rs | 4 +- 7 files changed, 141 insertions(+), 67 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index bfc60dda..0eb7e454 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -94,7 +94,7 @@ pub trait TrustchainVCAPI { let resolver = get_ion_resolver(endpoint); credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string()))); let attestor = IONAttestor::new(did); - attestor.sign(&credential, key_id, &resolver).await + attestor.sign(&credential, key_id, &resolver, None).await } /// Verifies a credential and returns a `DIDChain` if valid. @@ -126,11 +126,12 @@ pub trait TrustchainVPAPI { did: &str, key_id: Option<&str>, endpoint: &str, + ldp_options: Option, ) -> Result { let resolver = get_ion_resolver(endpoint); let attestor = IONAttestor::new(did); Ok(attestor - .sign_presentation(&presentation, key_id, &resolver) + .sign_presentation(&presentation, key_id, &resolver, ldp_options) .await?) } /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). @@ -140,26 +141,6 @@ pub trait TrustchainVPAPI { root_event_time: Timestamp, verifier: &IONVerifier, ) -> Result<(), PresentationError> { - // Verify signature - let result = presentation - .verify(ldp_options.clone(), verifier.resolver()) - .await; - println!("{:?}", result); - if !result.errors.is_empty() { - return Err(PresentationError::CredentialError( - CredentialError::VerificationResultError(result), - )); - } - // Verify holder's DID - let holder = match presentation - .holder - .as_ref() - .ok_or(PresentationError::NoHolderPresent)? - { - URI::String(holder) => holder, - }; - verifier.verify(holder, root_event_time).await?; - // Verify contained credentials let credentials = presentation .verifiable_credential @@ -174,7 +155,7 @@ pub trait TrustchainVPAPI { .map(|_| ldp_options.clone()) .collect(); let start = now_ms(); - let out = stream::iter(credentials.into_iter().zip(ldp_options_vec)) + stream::iter(credentials.into_iter().zip(ldp_options_vec)) .enumerate() .map(Ok) .try_for_each_concurrent( @@ -210,10 +191,32 @@ pub trait TrustchainVPAPI { } }, ) - .await; + .await?; let end = now_ms(); println!("Full time: {}", end - start); - out + + // Verify signature + let result = presentation + .verify(ldp_options.clone(), verifier.resolver()) + .await; + if !result.errors.is_empty() { + return Err(PresentationError::VerifiedHolderUnauthenticated(result)); + } + + // Verify holder's DID + let holder = match presentation + .holder + .as_ref() + .ok_or(PresentationError::NoHolderPresent)? + { + URI::String(holder) => holder, + }; + let holder_verification = verifier.verify(holder, root_event_time).await; + if let Err(verification_err) = holder_verification { + Err(PresentationError::VerifiedHolderUnverfied(verification_err)) + } else { + Ok(()) + } } } @@ -225,8 +228,10 @@ mod tests { use ssi::vc::{ Credential, CredentialOrJWT, LinkedDataProofOptions, Presentation, ProofPurpose, URI, }; + use trustchain_core::data::TEST_SIGNING_KEYS; + use trustchain_core::vp::PresentationError; use trustchain_core::{holder::Holder, issuer::Issuer}; - use trustchain_ion::attestor::IONAttestor; + use trustchain_ion::attestor::{AttestorData, IONAttestor}; use trustchain_ion::get_ion_resolver; use trustchain_ion::verifier::IONVerifier; @@ -301,24 +306,21 @@ mod tests { ])), // NB. Holder must be specified in order to retrieve verification method to verify // presentation. Otherwise must be specified in LinkedDataProofOptions. - holder: Some(URI::String(String::from(holder_did))), + // If the holder field is left unpopulated here, it is automatically populated during + // signing (with the did of the presentation signer) ..Default::default() }; presentation = holder - .sign_presentation(&presentation, None, &resolver) + .sign_presentation(&presentation, None, &resolver, None) .await .unwrap(); println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); - // NB. If specifying a VM method + // NB. If specifying a verification_method // let vm = String::from("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI"); assert!(TrustchainAPI::verify_presentation( &presentation, - // Must be specified to override default proof_purpose, which is ProofPurpose::Authentication - Some(LinkedDataProofOptions { - proof_purpose: Some(ProofPurpose::AssertionMethod), - ..Default::default() - }), + None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), ) @@ -326,12 +328,55 @@ mod tests { .is_ok()); } + #[tokio::test] + async fn test_verify_presentation_unauthenticated() { + // root+1 + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + let issuer = IONAttestor::new(issuer_did); + + // root+2 + // Currently can't sign presentation with an unresolvable did because + // presentation.generate_proof() attempts to resolve the did + // let holder_did = "did:ion:unresolvable"; + // let holder = IONAttestor::new(holder_did); + // let holder = IONAttestor::try_from(AttestorData::new( + // holder_did.to_string(), + // serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), + // )) + // .unwrap(); + + let vc_with_proof = signed_credential(issuer).await; + let resolver = get_ion_resolver("http://localhost:3000/"); + let mut presentation = Presentation { + verifiable_credential: Some(OneOrMany::Many(vec![CredentialOrJWT::Credential( + vc_with_proof, + )])), + ..Default::default() + }; + + // presentation = holder + // .sign_presentation(&presentation, None, &resolver, None) + // .await + // .unwrap(); + // println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); + assert!(matches!( + TrustchainAPI::verify_presentation( + &presentation, + None, + ROOT_EVENT_TIME_1, + &IONVerifier::new(resolver), + ) + .await, + Err(PresentationError::VerifiedHolderUnauthenticated(..)) + )); + } + async fn signed_credential(attestor: IONAttestor) -> Credential { // Make resolver let resolver = get_ion_resolver("http://localhost:3000/"); // 3. Read credential let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); // Use attest_credential method instead of generating and adding proof - attestor.sign(&vc, None, &resolver).await.unwrap() + attestor.sign(&vc, None, &resolver, None).await.unwrap() } } diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index 53e301f0..d0577fdd 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -3,7 +3,7 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; -use ssi::vc::Presentation; +use ssi::vc::{LinkedDataProofOptions, Presentation}; use thiserror::Error; /// An error relating to a Trustchain holder. @@ -40,5 +40,6 @@ pub trait Holder: Subject { presentation: &Presentation, key_id: Option<&str>, resolver: &T, + ldp_options: Option, ) -> Result; } diff --git a/trustchain-core/src/issuer.rs b/trustchain-core/src/issuer.rs index 6777aeba..3524d09d 100644 --- a/trustchain-core/src/issuer.rs +++ b/trustchain-core/src/issuer.rs @@ -3,7 +3,7 @@ use crate::key_manager::KeyManagerError; use crate::subject::Subject; use async_trait::async_trait; use ssi::did_resolve::DIDResolver; -use ssi::vc::Credential; +use ssi::vc::{Credential, LinkedDataProofOptions}; use thiserror::Error; /// An error relating to a Trustchain Issuer. @@ -38,5 +38,6 @@ pub trait Issuer: Subject { credential: &Credential, key_id: Option<&str>, resolver: &T, + ldp_options: Option, ) -> Result; } diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index c432e062..f4f04219 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -1,5 +1,6 @@ //! Verifiable presentation functionality for Trustchain. use crate::{holder::HolderError, vc::CredentialError, verifier::VerifierError}; +use ssi::vc::VerificationResult; use thiserror::Error; /// An error relating to verifiable credentials and presentations. @@ -20,6 +21,12 @@ pub enum PresentationError { /// Wrapped variant for Verifier Error. #[error("A wrapped Verfier error: {0}")] VerifierError(VerifierError), + /// Credentials verified, but holder failed to authenticate. + #[error("Credentials verified for an unauthenticated holder.")] + VerifiedHolderUnauthenticated(VerificationResult), + /// Credentials verified, but holder DID failed verification. + #[error("Credentials verified for an unverified holder.")] + VerifiedHolderUnverfied(VerifierError), } impl From for PresentationError { diff --git a/trustchain-http/src/issuer.rs b/trustchain-http/src/issuer.rs index 2a7673d2..f949b03f 100644 --- a/trustchain-http/src/issuer.rs +++ b/trustchain-http/src/issuer.rs @@ -104,7 +104,7 @@ impl TrustchainIssuerHTTP for TrustchainIssuerHTTPHandler { } } let issuer = IONAttestor::new(issuer_did); - Ok(issuer.sign(&credential, None, resolver).await?) + Ok(issuer.sign(&credential, None, resolver, None).await?) } } diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index cd2a5f1a..06d69b3a 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -148,13 +148,17 @@ impl Issuer for IONAttestor { credential: &Credential, key_id: Option<&str>, resolver: &T, + ldp_options: Option, ) -> Result { + // If no ldp options passed, use default (in which ProofPurpose::AssertionMethod). + let options = ldp_options.unwrap_or(LinkedDataProofOptions::default()); + // Get the signing key. let signing_key = self.signing_key(key_id)?; // Generate proof let proof = credential - .generate_proof(&signing_key, &LinkedDataProofOptions::default(), resolver) + .generate_proof(&signing_key, &options, resolver) .await?; // Add proof to credential @@ -171,8 +175,14 @@ impl Holder for IONAttestor { presentation: &Presentation, key_id: Option<&str>, resolver: &T, - // TODO add argument for ldp options with LDP options for proof purpose such as authentication + ldp_options: Option, ) -> Result { + // If no ldp options passed, use default with ProofPurpose::Authentication. + let options = ldp_options.unwrap_or(LinkedDataProofOptions { + proof_purpose: Some(ssi::vc::ProofPurpose::Authentication), + ..Default::default() + }); + // Get the signing key. let signing_key = self.signing_key(key_id)?; @@ -188,20 +198,10 @@ impl Holder for IONAttestor { }; // Generate proof - // Example of VM derivation + // Example of verification_method derivation (optionally passed as a LinkedDataProofOption) // let vm = format!("{}#{}", self.did(), signing_key.thumbprint().unwrap()); - let proof = vp - .generate_proof( - &signing_key, - &LinkedDataProofOptions { - // verification_method: Some(ssi::vc::URI::String(vm)), - ..LinkedDataProofOptions::default() - }, - resolver, - ) - .await?; + let proof = vp.generate_proof(&signing_key, &options, resolver).await?; // Add proof to credential - vp.add_proof(proof); Ok(vp) } @@ -303,7 +303,7 @@ mod tests { let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); // Attest to doc - let vc_with_proof = target.sign(&vc, None, &resolver).await; + let vc_with_proof = target.sign(&vc, None, &resolver, None).await; // Check attest was ok assert!(vc_with_proof.is_ok()); @@ -311,25 +311,45 @@ mod tests { #[tokio::test] async fn test_attest_presentation() { - init(); + // Note: removed tmp directory overwrite for TRUSTCHAIN_DATA, to have access to + // the signing keys in .trustchain + // init(); let resolver = get_ion_resolver("http://localhost:3000/"); - let did = "did:example:test_attest_presentation"; - let target = IONAttestor::try_from(AttestorData::new( - did.to_string(), - serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), - )) - .unwrap(); + // root+1 + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + // root+2 + let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + + let issuer = IONAttestor::new(issuer_did); + let holder = IONAttestor::new(holder_did); + // let target = IONAttestor::try_from(AttestorData::new( + // did.to_string(), + // serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), + // )) + // .unwrap(); let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); - let vc_with_proof = target.sign(&vc, None, &resolver).await.unwrap(); + let vc_with_proof = issuer.sign(&vc, None, &resolver, None).await.unwrap(); let presentation = Presentation { verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc_with_proof))), ..Default::default() }; + // assert holder field is not initially populated + assert!(presentation.holder.is_none()); + // Attest to vp: - assert!(target - .sign_presentation(&presentation, None, &resolver) - .await - .is_ok()); + // .sign_presenatation now checks and sets the holder field of the presentation + // This has implications for the proof generation which is handled by the ssi library: + // - ssi::ldp::ensure_or_pick_verification_relationship calls presentation.get_issuer() + // which returns the holder (if Some) + // - ensure_or_pick_verification_relationship tries to resolve the did and check it's + // verification methods + let vp = holder + .sign_presentation(&presentation, None, &resolver, None) + .await; + assert!(vp.is_ok()); + + // Check holder field has been correctly populated during signing + assert_eq!(vp.unwrap().holder.unwrap().to_string(), holder.did); } #[test] diff --git a/trustchain-ion/tests/vc.rs b/trustchain-ion/tests/vc.rs index 289d0686..43169437 100644 --- a/trustchain-ion/tests/vc.rs +++ b/trustchain-ion/tests/vc.rs @@ -54,7 +54,7 @@ async fn test_sign_credential() { // 4. Generate VC and verify // Use attest_credential method instead of generating and adding proof - let mut vc_with_proof = attestor.sign(&vc, None, &resolver).await.unwrap(); + let mut vc_with_proof = attestor.sign(&vc, None, &resolver, None).await.unwrap(); // Verify: expect no warnings or errors let verification_result = vc_with_proof.verify(None, &resolver).await; @@ -88,7 +88,7 @@ async fn test_sign_credential_failure() { // 4. Generate VC and verify // Sign credential (expect failure). - let vc_with_proof = attestor.sign(&vc, None, &resolver).await; + let vc_with_proof = attestor.sign(&vc, None, &resolver, None).await; assert!(vc_with_proof.is_err()); assert!(matches!( vc_with_proof, From 6c42fd7a36ed69e00edb8cbaf92e6031397bd259 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 25 Aug 2023 15:54:29 +0100 Subject: [PATCH 10/26] Remove trustchain verification on holder --- trustchain-api/src/api.rs | 55 +++++++-------------------------------- trustchain-core/src/vp.rs | 5 ++-- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 0eb7e454..c8320cc1 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use did_ion::sidetree::DocumentState; use futures::{stream, StreamExt, TryStreamExt}; +use ssi::ldp::now_ms; use ssi::{ did_resolve::DIDResolver, ldp::LinkedDataDocument, @@ -117,7 +118,6 @@ pub trait TrustchainVCAPI { } } -use ssi::ldp::now_ms; #[async_trait] pub trait TrustchainVPAPI { /// As a holder issue a verifiable presentation. @@ -195,28 +195,14 @@ pub trait TrustchainVPAPI { let end = now_ms(); println!("Full time: {}", end - start); - // Verify signature + // Only verify signature by holder to authenticate let result = presentation .verify(ldp_options.clone(), verifier.resolver()) .await; if !result.errors.is_empty() { return Err(PresentationError::VerifiedHolderUnauthenticated(result)); } - - // Verify holder's DID - let holder = match presentation - .holder - .as_ref() - .ok_or(PresentationError::NoHolderPresent)? - { - URI::String(holder) => holder, - }; - let holder_verification = verifier.verify(holder, root_event_time).await; - if let Err(verification_err) = holder_verification { - Err(PresentationError::VerifiedHolderUnverfied(verification_err)) - } else { - Ok(()) - } + Ok(()) } } @@ -225,13 +211,10 @@ mod tests { use crate::api::{TrustchainVCAPI, TrustchainVPAPI}; use crate::TrustchainAPI; use ssi::one_or_many::OneOrMany; - use ssi::vc::{ - Credential, CredentialOrJWT, LinkedDataProofOptions, Presentation, ProofPurpose, URI, - }; - use trustchain_core::data::TEST_SIGNING_KEYS; + use ssi::vc::{Credential, CredentialOrJWT, Presentation}; use trustchain_core::vp::PresentationError; use trustchain_core::{holder::Holder, issuer::Issuer}; - use trustchain_ion::attestor::{AttestorData, IONAttestor}; + use trustchain_ion::attestor::IONAttestor; use trustchain_ion::get_ion_resolver; use trustchain_ion::verifier::IONVerifier; @@ -307,7 +290,7 @@ mod tests { // NB. Holder must be specified in order to retrieve verification method to verify // presentation. Otherwise must be specified in LinkedDataProofOptions. // If the holder field is left unpopulated here, it is automatically populated during - // signing (with the did of the presentation signer) + // signing (with the did of the presentation signer) in `holder.sign_presentation()` ..Default::default() }; @@ -316,8 +299,6 @@ mod tests { .await .unwrap(); println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); - // NB. If specifying a verification_method - // let vm = String::from("did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI"); assert!(TrustchainAPI::verify_presentation( &presentation, None, @@ -329,36 +310,22 @@ mod tests { } #[tokio::test] + // No signature from holder in presentation (unauthenticated) async fn test_verify_presentation_unauthenticated() { // root+1 let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; let issuer = IONAttestor::new(issuer_did); - // root+2 - // Currently can't sign presentation with an unresolvable did because - // presentation.generate_proof() attempts to resolve the did - // let holder_did = "did:ion:unresolvable"; - // let holder = IONAttestor::new(holder_did); - // let holder = IONAttestor::try_from(AttestorData::new( - // holder_did.to_string(), - // serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), - // )) - // .unwrap(); - let vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); - let mut presentation = Presentation { + let presentation = Presentation { verifiable_credential: Some(OneOrMany::Many(vec![CredentialOrJWT::Credential( vc_with_proof, )])), ..Default::default() }; - // presentation = holder - // .sign_presentation(&presentation, None, &resolver, None) - // .await - // .unwrap(); - // println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); + println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); assert!(matches!( TrustchainAPI::verify_presentation( &presentation, @@ -371,12 +338,10 @@ mod tests { )); } + // Helper function to create a signed credential given an attesor. async fn signed_credential(attestor: IONAttestor) -> Credential { - // Make resolver let resolver = get_ion_resolver("http://localhost:3000/"); - // 3. Read credential let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); - // Use attest_credential method instead of generating and adding proof attestor.sign(&vc, None, &resolver, None).await.unwrap() } } diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index f4f04219..df84d7dd 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -21,10 +21,11 @@ pub enum PresentationError { /// Wrapped variant for Verifier Error. #[error("A wrapped Verfier error: {0}")] VerifierError(VerifierError), - /// Credentials verified, but holder failed to authenticate. + /// Credentials verified, but holder failed to authenticate with invalid or missing presentation + /// proof. #[error("Credentials verified for an unauthenticated holder.")] VerifiedHolderUnauthenticated(VerificationResult), - /// Credentials verified, but holder DID failed verification. + /// Credentials verified, but holder DID failed verification (not part of valid Trustchain). #[error("Credentials verified for an unverified holder.")] VerifiedHolderUnverfied(VerifierError), } From 710a7e8512adc6bc32e3860c9c19627210165ef3 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 25 Aug 2023 16:41:06 +0100 Subject: [PATCH 11/26] Add todo for init function for tests --- trustchain-core/src/utils.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/trustchain-core/src/utils.rs b/trustchain-core/src/utils.rs index 2fd38dd5..1f6efa9f 100644 --- a/trustchain-core/src/utils.rs +++ b/trustchain-core/src/utils.rs @@ -20,6 +20,16 @@ pub fn init() { // initialization code here let tempdir = tempfile::tempdir().unwrap(); std::env::set_var(TRUSTCHAIN_DATA, Path::new(tempdir.as_ref().as_os_str())); + + // TODO: write the required key_manager path in TRUSTCHAIN_DATA with: root, root-plus-1, + // root-plus-2 signing keys in "signing_key.json" files + // DID suffixes: + // root: EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg + // root-plus-1: EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A + // root-plus-2: EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q + // + // Path for each DID's signing key file: + // env!("TRUSTCHAIN_DATA")/key_manager//signing_key.json }); } From cbc958632a97d0d47555fbf0fb8eade3860e7e77 Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Mon, 4 Sep 2023 14:56:10 +0100 Subject: [PATCH 12/26] Revise doc comments --- trustchain-api/src/api.rs | 4 ++-- trustchain-core/src/holder.rs | 2 +- trustchain-core/src/vp.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 84afa1cb..2b1da6a5 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -130,7 +130,7 @@ pub trait TrustchainVCAPI { #[async_trait] pub trait TrustchainVPAPI { - /// As a holder issue a verifiable presentation. + /// Signs a presentation constructing a verifiable presentation. async fn sign_presentation( presentation: Presentation, did: &str, @@ -144,7 +144,7 @@ pub trait TrustchainVPAPI { .sign_presentation(&presentation, key_id, &resolver, ldp_options) .await?) } - /// Verifies a verifiable presentation. Analogous with [didkit](https://docs.rs/didkit/latest/didkit/c/fn.didkit_vc_verify_presentation.html). + /// Verifies a verifiable presentation. async fn verify_presentation( presentation: &Presentation, ldp_options: Option, diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index d0577fdd..5242d408 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -29,7 +29,7 @@ impl From for HolderError { } } -/// A holder signs a credential to generate a verifiable credential. +/// A holder signs a presentation to generate a verifiable presentation. #[async_trait] pub trait Holder: Subject { /// Attests to a given presentation of one or many credentials returning the presentation with a diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index df84d7dd..e475c011 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -3,7 +3,7 @@ use crate::{holder::HolderError, vc::CredentialError, verifier::VerifierError}; use ssi::vc::VerificationResult; use thiserror::Error; -/// An error relating to verifiable credentials and presentations. +/// An error relating to verifiable presentations. #[derive(Error, Debug)] pub enum PresentationError { /// No credentials present in presentation. From f769495b4d6c71bd11baf038b9f80ad769818ccb Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 4 Sep 2023 15:10:00 +0100 Subject: [PATCH 13/26] Small revisions to verify_presentation in review --- trustchain-api/src/api.rs | 68 ++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 2b1da6a5..8a38f661 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -151,61 +151,49 @@ pub trait TrustchainVPAPI { root_event_time: Timestamp, verifier: &IONVerifier, ) -> Result<(), PresentationError> { - // Verify contained credentials + // Check credentials are present in presentation let credentials = presentation .verifiable_credential .as_ref() .ok_or(PresentationError::NoCredentialsPresent)?; - // https://gendignoux.com/blog/2021/04/01/rust-async-streams-futures-part1.html#unordered-buffering-1 - // https://docs.rs/futures-util/latest/futures_util/stream/trait.TryStreamExt.html#method.try_for_each_concurrent - // TODO consider concurrency limit (as rate limiting for verifier requests) + // 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()) .collect(); - let start = now_ms(); + + // Verify signatures and issuers for each credential included in the presentation stream::iter(credentials.into_iter().zip(ldp_options_vec)) - .enumerate() .map(Ok) - .try_for_each_concurrent( - limit, - |(idx, (credential_or_jwt, ldp_options))| async move { - match credential_or_jwt { - CredentialOrJWT::Credential(credential) => { - println!("start {}: {}", idx, now_ms()); - let v = TrustchainAPI::verify_credential( - credential, - ldp_options, - root_event_time, - verifier, - ) - .await - .map(|_| ()) - .map_err(|err| err.into()); - println!("done {}: {}", idx, now_ms()); - v - } - - CredentialOrJWT::JWT(jwt) => { - let result = - Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; - if !result.errors.is_empty() { - Err(PresentationError::CredentialError( - CredentialError::VerificationResultError(result), - )) - } else { - 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(|_| ()) + .map_err(|err| err.into()), + CredentialOrJWT::JWT(jwt) => { + // TODO: add chain verification for JWT credentials. + let result = + Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; + if !result.errors.is_empty() { + Err(PresentationError::CredentialError( + CredentialError::VerificationResultError(result), + )) + } else { + Ok(()) } } - }, - ) + } + }) .await?; - let end = now_ms(); - println!("Full time: {}", end - start); - // Only verify signature by holder to authenticate + // Verify signature by holder to authenticate let result = presentation .verify(ldp_options.clone(), verifier.resolver()) .await; From 627084986d64090d162d95b70e6c8a90b4dca6cc Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Mon, 4 Sep 2023 16:40:52 +0100 Subject: [PATCH 14/26] refactor vc integration tests into unit tests in ion/attestor and api/api, add appropriate test ignore tags --- trustchain-api/src/api.rs | 28 ++++++++- trustchain-ion/src/attestor.rs | 67 ++++++++++++++++----- trustchain-ion/tests/vc.rs | 104 --------------------------------- 3 files changed, 79 insertions(+), 120 deletions(-) delete mode 100644 trustchain-ion/tests/vc.rs diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 8a38f661..065706d4 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; use did_ion::sidetree::DocumentState; use futures::{stream, StreamExt, TryStreamExt}; -use ssi::ldp::now_ms; use ssi::{ did_resolve::DIDResolver, ldp::LinkedDataDocument, @@ -208,8 +207,10 @@ pub trait TrustchainVPAPI { mod tests { use crate::api::{TrustchainVCAPI, TrustchainVPAPI}; use crate::TrustchainAPI; + use ssi::ldp::now_ms; use ssi::one_or_many::OneOrMany; - use ssi::vc::{Credential, CredentialOrJWT, Presentation}; + use ssi::vc::{Credential, CredentialOrJWT, Presentation, VCDateTime}; + use trustchain_core::vc::CredentialError; use trustchain_core::vp::PresentationError; use trustchain_core::{holder::Holder, issuer::Issuer}; use trustchain_ion::attestor::IONAttestor; @@ -244,11 +245,12 @@ mod tests { } "##; + #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_verify_credential() { let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; let issuer = IONAttestor::new(issuer_did); - let vc_with_proof = signed_credential(issuer).await; + let mut vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); let res = TrustchainAPI::verify_credential( &vc_with_proof, @@ -258,8 +260,27 @@ mod tests { ) .await; assert!(res.is_ok()); + + // Change credential to make signature invalid + vc_with_proof.expiration_date = Some(VCDateTime::try_from(now_ms()).unwrap()); + + // Verify: expect no warnings and a signature error as VC has changed + let resolver = get_ion_resolver("http://localhost:3000/"); + let res = TrustchainAPI::verify_credential( + &vc_with_proof, + None, + ROOT_EVENT_TIME_1, + &IONVerifier::new(resolver), + ) + .await; + if let CredentialError::VerificationResultError(ver_res) = res.err().unwrap() { + assert_eq!(ver_res.errors, vec!["signature error"]); + } else { + panic!("should error with VerificationResultError varient of CredentialError") + } } + #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_verify_presentation() { // root+1 @@ -307,6 +328,7 @@ mod tests { .is_ok()); } + #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] // No signature from holder in presentation (unauthenticated) async fn test_verify_presentation_unauthenticated() { diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 7c01cd5e..a5390b25 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -171,6 +171,14 @@ impl Issuer for IONAttestor { #[async_trait] impl Holder for IONAttestor { + // This implementation ensures that the holder field is set on the Presentation, with the + // following implications: + // - proof generation is handled by the ssi library + // - ssi::ldp::ensure_or_pick_verification_relationship calls presentation.get_issuer() + // which returns the holder (if Some, which is always the case) + // - ensure_or_pick_verification_relationship tries to resolve the holder DID and check it's + // verification methods + // - so the holder's DID must be resolvable async fn sign_presentation( &self, presentation: &Presentation, @@ -309,6 +317,45 @@ mod tests { assert!(vc_with_proof.is_ok()); } + #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] + #[tokio::test] + async fn test_sign_credential_failure() { + // Initialize temp path for saving keys + init(); + + // 1. Set-up (with a DID that will *not* match the issuer field in the credential). + let did = "did:ion:test:EiDMe2SFfJ_7eXVW7RF1ZHOkeu2M-Bre0ak2cXNBH0P-TQ"; + + // Make resolver + let resolver = get_ion_resolver("http://localhost:3000/"); + + // 2. Load Attestor + // let attestor = IONAttestor::new(did); + // Attestor + let attestor = IONAttestor::try_from(AttestorData::new( + did.to_string(), + serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), + )) + .unwrap(); + + // 3. Read credential and set issuer field + let mut vc: Credential = serde_json::from_str(TEST_CREDENTIAL).unwrap(); + vc.issuer = Some(ssi::vc::Issuer::URI(URI::String( + "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string(), + ))); + + // 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; + assert!(vc_with_proof.is_err()); + + assert!(matches!( + vc_with_proof, + Err(IssuerError::SSI(ssi::error::Error::KeyMismatch)) + )); + } + + #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_attest_presentation() { // Note: removed tmp directory overwrite for TRUSTCHAIN_DATA, to have access to @@ -327,29 +374,23 @@ mod tests { // serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), // )) // .unwrap(); + let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); let vc_with_proof = issuer.sign(&vc, None, None, &resolver).await.unwrap(); + + // Create Presentation, initially with holder field defaulting to None let presentation = Presentation { verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc_with_proof))), ..Default::default() }; - // assert holder field is not initially populated - assert!(presentation.holder.is_none()); - - // Attest to vp: - // .sign_presenatation now checks and sets the holder field of the presentation - // This has implications for the proof generation which is handled by the ssi library: - // - ssi::ldp::ensure_or_pick_verification_relationship calls presentation.get_issuer() - // which returns the holder (if Some) - // - ensure_or_pick_verification_relationship tries to resolve the did and check it's - // verification methods + + // 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, &resolver, None) .await; - assert!(vp.is_ok()); - // Check holder field has been correctly populated during signing - assert_eq!(vp.unwrap().holder.unwrap().to_string(), holder.did); + assert!(vp.is_ok()); } #[test] diff --git a/trustchain-ion/tests/vc.rs b/trustchain-ion/tests/vc.rs deleted file mode 100644 index f9727988..00000000 --- a/trustchain-ion/tests/vc.rs +++ /dev/null @@ -1,104 +0,0 @@ -use ssi::ldp::now_ms; -use std::convert::TryFrom; -use trustchain_core::issuer::{Issuer, IssuerError}; -use trustchain_ion::attestor::IONAttestor; -use trustchain_ion::get_ion_resolver; - -use ssi::vc::{Credential, VCDateTime}; - -// Linked @context provides a set of allowed fields for the credential: -// "credentialSubject" key: "https://www.w3.org/2018/credentials/examples/v1" -// "image" key: "https://w3id.org/citizenship/v1" -// -// Other examples: https://www.w3.org/TR/vc-use-cases/ -const TEST_UNSIGNED_VC: &str = r##"{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1", - "https://w3id.org/citizenship/v1" - ], - "credentialSchema": { - "id": "did:example:cdf:35LB7w9ueWbagPL94T9bMLtyXDj9pX5o", - "type": "did:example:schema:22KpkXgecryx9k7N6XN1QoN3gXwBkSU8SfyyYQG" - }, - "type": ["VerifiableCredential"], - "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", - "image": "some_base64_representation", - "credentialSubject": { - "givenName": "Jane", - "familyName": "Doe", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts", - "college": "College of Engineering" - } - } -} -"##; - -#[ignore = "requires a running Sidetree node listening on http://localhost:3000"] -#[tokio::test] -async fn test_sign_credential() { - // 1. Set-up - let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; - - // Make resolver - let resolver = get_ion_resolver("http://localhost:3000/"); - - // 2. Load Attestor - let attestor = IONAttestor::new(did); - - // 3. Read credential - let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); - - // 4. Generate VC and verify - - // Use attest_credential method instead of generating and adding proof - let mut vc_with_proof = attestor.sign(&vc, None, None, &resolver).await.unwrap(); - - // Verify: expect no warnings or errors - let verification_result = vc_with_proof.verify(None, &resolver).await; - assert!(verification_result.warnings.is_empty()); - assert!(verification_result.errors.is_empty()); - - // Change credential to make signature invalid - vc_with_proof.expiration_date = Some(VCDateTime::try_from(now_ms()).unwrap()); - - // Verify: expect no warnings and a signature error as VC has changed - let verification_result = vc_with_proof.verify(None, &resolver).await; - assert!(verification_result.warnings.is_empty()); - assert_eq!(verification_result.errors, vec!["signature error"]); -} - -#[ignore = "requires a running Sidetree node listening on http://localhost:3000"] -#[tokio::test] -async fn test_sign_credential_failure() { - // 1. Set-up (with a DID *not* matching the issuer field in the credential). - let did = "did:ion:test:EiDMe2SFfJ_7eXVW7RF1ZHOkeu2M-Bre0ak2cXNBH0P-TQ"; - - // Make resolver - let resolver = get_ion_resolver("http://localhost:3000/"); - - // 2. Load Attestor - let attestor = IONAttestor::new(did); - - // 3. Read credential - let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap(); - - // 4. Generate VC and verify - - // Sign credential (expect failure). - let vc_with_proof = attestor.sign(&vc, None, None, &resolver).await; - assert!(vc_with_proof.is_err()); - assert!(matches!( - vc_with_proof, - Err(IssuerError::SSI(ssi::error::Error::KeyMismatch)) - )); -} - -// TODO: add VP integration test -#[ignore = "requires a running Sidetree node listening on http://localhost:3000"] -#[tokio::test] -async fn test_sign_presentation() { - todo!() -} From c53e6acf7d60ef7aa7572de9d74db62b6491e8f8 Mon Sep 17 00:00:00 2001 From: Ed Chapman <93717706+edchapman88@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:51:58 +0100 Subject: [PATCH 15/26] remove empty tests module Co-authored-by: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> --- trustchain-core/src/vp.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index e475c011..7142bdd5 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -48,5 +48,3 @@ impl From for PresentationError { } } -#[cfg(test)] -mod tests {} From ace987b439dbf95041fb81f1e391327e9d20efa6 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Mon, 4 Sep 2023 16:58:44 +0100 Subject: [PATCH 16/26] add variables to error display strings --- trustchain-core/src/holder.rs | 4 ++-- trustchain-core/src/issuer.rs | 4 ++-- trustchain-core/src/vp.rs | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index 5242d408..0be87cb6 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -10,10 +10,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum HolderError { /// Wrapped error for SSI error. - #[error("A wrapped variant for an SSI error.")] + #[error("A wrapped variant for an SSI error: {0}")] SSI(ssi::error::Error), /// Wrapped error for key manager error. - #[error("A wrapped variant for a key manager error.")] + #[error("A wrapped variant for a key manager error: {0}")] KeyManager(KeyManagerError), } diff --git a/trustchain-core/src/issuer.rs b/trustchain-core/src/issuer.rs index 03537003..20c8d967 100644 --- a/trustchain-core/src/issuer.rs +++ b/trustchain-core/src/issuer.rs @@ -10,10 +10,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum IssuerError { /// Wrapped error for SSI error. - #[error("A wrapped variant for an SSI error.")] + #[error("A wrapped variant for an SSI error: {0}")] SSI(ssi::error::Error), /// Wrapped error for key manager error. - #[error("A wrapped variant for a key manager error.")] + #[error("A wrapped variant for a key manager error: {0}")] KeyManager(KeyManagerError), } diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index 7142bdd5..02452b17 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -23,10 +23,10 @@ pub enum PresentationError { VerifierError(VerifierError), /// Credentials verified, but holder failed to authenticate with invalid or missing presentation /// proof. - #[error("Credentials verified for an unauthenticated holder.")] + #[error("Credentials verified for an unauthenticated holder: {0:?}")] VerifiedHolderUnauthenticated(VerificationResult), /// Credentials verified, but holder DID failed verification (not part of valid Trustchain). - #[error("Credentials verified for an unverified holder.")] + #[error("Credentials verified for an unverified holder: {0}")] VerifiedHolderUnverfied(VerifierError), } @@ -47,4 +47,3 @@ impl From for PresentationError { PresentationError::VerifierError(err) } } - From f7505c521259643477251dbebf65c453de4f3ff7 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Mon, 4 Sep 2023 17:04:46 +0100 Subject: [PATCH 17/26] reorder arguments in sign_presentation --- trustchain-api/src/api.rs | 6 +++--- trustchain-core/src/holder.rs | 2 +- trustchain-ion/src/attestor.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 065706d4..4835a950 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -135,12 +135,12 @@ pub trait TrustchainVPAPI { did: &str, key_id: Option<&str>, endpoint: &str, - ldp_options: Option, + linked_data_proof_options: Option, ) -> Result { let resolver = get_ion_resolver(endpoint); let attestor = IONAttestor::new(did); Ok(attestor - .sign_presentation(&presentation, key_id, &resolver, ldp_options) + .sign_presentation(&presentation, linked_data_proof_options, key_id, &resolver) .await?) } /// Verifies a verifiable presentation. @@ -314,7 +314,7 @@ mod tests { }; presentation = holder - .sign_presentation(&presentation, None, &resolver, None) + .sign_presentation(&presentation, None, None, &resolver) .await .unwrap(); println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index 0be87cb6..141cd059 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -38,8 +38,8 @@ pub trait Holder: Subject { async fn sign_presentation( &self, presentation: &Presentation, + linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, - ldp_options: Option, ) -> Result; } diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index a5390b25..4dd9c9b5 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -182,12 +182,12 @@ impl Holder for IONAttestor { async fn sign_presentation( &self, presentation: &Presentation, + linked_data_proof_options: Option, key_id: Option<&str>, resolver: &T, - ldp_options: Option, ) -> Result { // If no ldp options passed, use default with ProofPurpose::Authentication. - let options = ldp_options.unwrap_or(LinkedDataProofOptions { + let options = linked_data_proof_options.unwrap_or(LinkedDataProofOptions { proof_purpose: Some(ssi::vc::ProofPurpose::Authentication), ..Default::default() }); @@ -387,7 +387,7 @@ 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, &resolver, None) + .sign_presentation(&presentation, None, None, &resolver) .await; assert!(vp.is_ok()); From 87e2ea160b10073d5c46281604f6abff654c40c6 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Mon, 4 Sep 2023 17:14:24 +0100 Subject: [PATCH 18/26] reafctor sign_presentationto error when holder field is mismatched with attestor DID --- trustchain-core/src/holder.rs | 3 +++ trustchain-ion/src/attestor.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/trustchain-core/src/holder.rs b/trustchain-core/src/holder.rs index 141cd059..2f78b15b 100644 --- a/trustchain-core/src/holder.rs +++ b/trustchain-core/src/holder.rs @@ -15,6 +15,9 @@ pub enum HolderError { /// Wrapped error for key manager error. #[error("A wrapped variant for a key manager error: {0}")] KeyManager(KeyManagerError), + /// Holder field mismatched with attestor DID. + #[error("Holder field mismatched with attestor DID.")] + MismatchedHolder, } impl From for HolderError { diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 4dd9c9b5..b2c217e8 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -200,7 +200,7 @@ impl Holder for IONAttestor { match presentation.holder.as_ref() { Some(URI::String(holder)) => { if holder != &self.did { - vp.holder = Some(URI::String(self.did.clone())) + return Err(HolderError::MismatchedHolder); } } None => vp.holder = Some(URI::String(self.did.clone())), From e6816b898686711cd5ece18869ee005c7c25e956 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 4 Sep 2023 17:32:41 +0100 Subject: [PATCH 19/26] Add signing key to test init for two resolvable test DIDs --- trustchain-api/src/api.rs | 15 ++++++++------- trustchain-core/src/utils.rs | 33 +++++++++++++++++++++++---------- trustchain-ion/src/attestor.rs | 16 +++------------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 4835a950..0a57cb0c 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -210,6 +210,7 @@ mod tests { use ssi::ldp::now_ms; use ssi::one_or_many::OneOrMany; use ssi::vc::{Credential, CredentialOrJWT, Presentation, VCDateTime}; + use trustchain_core::utils::init; use trustchain_core::vc::CredentialError; use trustchain_core::vp::PresentationError; use trustchain_core::{holder::Holder, issuer::Issuer}; @@ -248,7 +249,8 @@ mod tests { #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_verify_credential() { - let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + init(); + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1 let issuer = IONAttestor::new(issuer_did); let mut vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); @@ -283,10 +285,9 @@ mod tests { #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_verify_presentation() { - // root+1 - let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; - // root+2 - let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + init(); + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1 + let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; // root+2 let issuer = IONAttestor::new(issuer_did); let holder = IONAttestor::new(holder_did); @@ -332,8 +333,8 @@ mod tests { #[tokio::test] // No signature from holder in presentation (unauthenticated) async fn test_verify_presentation_unauthenticated() { - // root+1 - let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + init(); + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1 let issuer = IONAttestor::new(issuer_did); let vc_with_proof = signed_credential(issuer).await; diff --git a/trustchain-core/src/utils.rs b/trustchain-core/src/utils.rs index 1f6efa9f..91b0153e 100644 --- a/trustchain-core/src/utils.rs +++ b/trustchain-core/src/utils.rs @@ -12,6 +12,20 @@ pub fn type_of(_: &T) -> String { std::any::type_name::().to_string() } +/// Writes a given signing key for a given DID suffix to the key manager during test init only. +fn write_signing_key( + did_suffix: &str, + signing_key: &str, +) -> Result<(), Box> { + let path = Path::new(&std::env::var(TRUSTCHAIN_DATA)?) + .join("key_manager") + .join(did_suffix); + std::fs::create_dir_all(&path)?; + let path = path.join("signing_key.json"); + std::fs::write(path.clone(), signing_key)?; + Ok(()) +} + /// Set-up tempdir and use as env var for `TRUSTCHAIN_DATA`. // https://stackoverflow.com/questions/58006033/how-to-run-setup-code-before-any-tests-run-in-rust static INIT: Once = Once::new(); @@ -20,16 +34,15 @@ pub fn init() { // initialization code here let tempdir = tempfile::tempdir().unwrap(); std::env::set_var(TRUSTCHAIN_DATA, Path::new(tempdir.as_ref().as_os_str())); - - // TODO: write the required key_manager path in TRUSTCHAIN_DATA with: root, root-plus-1, - // root-plus-2 signing keys in "signing_key.json" files - // DID suffixes: - // root: EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg - // root-plus-1: EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A - // root-plus-2: EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q - // - // Path for each DID's signing key file: - // env!("TRUSTCHAIN_DATA")/key_manager//signing_key.json + // Manually drop here so additional writes in the init call are not removed + drop(tempdir); + // Include test signing keys for two resolvable DIDs + let root_plus_1_did_suffix = "EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + let root_plus_2_did_suffix = "EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + 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 root_plus_2_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY","d":"bJnhIQgj0eQoRXIw5Xna6LErnili2ajMstoJLI21HiQ"}"#; + write_signing_key(root_plus_1_did_suffix, root_plus_1_signing_key).unwrap(); + write_signing_key(root_plus_2_did_suffix, root_plus_2_signing_key).unwrap(); }); } diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index b2c217e8..32cbf438 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -358,22 +358,12 @@ mod tests { #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] #[tokio::test] async fn test_attest_presentation() { - // Note: removed tmp directory overwrite for TRUSTCHAIN_DATA, to have access to - // the signing keys in .trustchain - // init(); + init(); let resolver = get_ion_resolver("http://localhost:3000/"); - // root+1 - let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; - // root+2 - let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; - + let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1 + let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; // root+2 let issuer = IONAttestor::new(issuer_did); let holder = IONAttestor::new(holder_did); - // let target = IONAttestor::try_from(AttestorData::new( - // did.to_string(), - // serde_json::from_str(TEST_SIGNING_KEYS).unwrap(), - // )) - // .unwrap(); let vc = serde_json::from_str(TEST_CREDENTIAL).unwrap(); let vc_with_proof = issuer.sign(&vc, None, None, &resolver).await.unwrap(); From 24bdd3bb97b62197c7edf901b1998e2d8343ad8a Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Wed, 6 Sep 2023 13:44:36 +0100 Subject: [PATCH 20/26] decode jwt, new error varient --- trustchain-api/src/api.rs | 67 +++++++++++++++++++++++++-------- trustchain-cli/src/bin/main.rs | 5 +++ trustchain-core/src/vc.rs | 3 ++ trustchain-core/src/verifier.rs | 4 +- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 0a57cb0c..6a256986 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -174,18 +174,27 @@ pub trait TrustchainVPAPI { verifier, ) .await - .map(|_| ()) - .map_err(|err| err.into()), + .map(|_| ()), CredentialOrJWT::JWT(jwt) => { - // TODO: add chain verification for JWT credentials. - let result = - Credential::verify_jwt(jwt, ldp_options, verifier.resolver()).await; - if !result.errors.is_empty() { - Err(PresentationError::CredentialError( - CredentialError::VerificationResultError(result), - )) - } else { - Ok(()) + // 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, + root_event_time, + verifier, + ) + .await + .map(|_| ()), + Err(e) => Err(e), } } } @@ -233,6 +242,7 @@ mod tests { }, "type": ["VerifiableCredential"], "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q", + "issuanceDate": "2023-09-06T12:15:08.630033Z", "image": "some_base64_representation", "credentialSubject": { "givenName": "Jane", @@ -294,6 +304,10 @@ mod tests { let vc_with_proof = signed_credential(issuer).await; let resolver = get_ion_resolver("http://localhost:3000/"); + + // 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"}"#; + // let jwk: JWK = serde_json::from_str(root_plus_1_signing_key).unwrap(); let mut presentation = Presentation { verifiable_credential: Some(OneOrMany::Many(vec![ CredentialOrJWT::Credential(vc_with_proof.clone()), @@ -305,7 +319,29 @@ mod tests { CredentialOrJWT::Credential(vc_with_proof.clone()), CredentialOrJWT::Credential(vc_with_proof.clone()), CredentialOrJWT::Credential(vc_with_proof.clone()), - CredentialOrJWT::Credential(vc_with_proof), + CredentialOrJWT::Credential(vc_with_proof.clone()), + // Currently cannot generate a valid jwt that passes verification + // Open issue to implement jwt generation for Issuer + // https://github.com/alan-turing-institute/trustchain/issues/118 + // CredentialOrJWT::JWT( + // vc.generate_jwt( + // Some(&jwk), + // &LinkedDataProofOptions { + // checks: None, + // created: None, + // ..Default::default() // created: None, + // // challenge: None, + // // domain: None, + // // type_: None, + // // eip712_domain: None, + // // proof_purpose: None, + // // verification_method: None, + // }, + // &resolver, + // ) + // .await + // .unwrap(), + // ), ])), // NB. Holder must be specified in order to retrieve verification method to verify // presentation. Otherwise must be specified in LinkedDataProofOptions. @@ -319,14 +355,15 @@ mod tests { .await .unwrap(); println!("{}", serde_json::to_string_pretty(&presentation).unwrap()); - assert!(TrustchainAPI::verify_presentation( + let res = TrustchainAPI::verify_presentation( &presentation, None, ROOT_EVENT_TIME_1, &IONVerifier::new(resolver), ) - .await - .is_ok()); + .await; + println!("{:?}", res); + assert!(res.is_ok()); } #[ignore = "requires a running Sidetree node listening on http://localhost:3000"] diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 56ee72e1..f8f8748c 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -214,6 +214,11 @@ async fn main() -> Result<(), Box> { println!("Issuer... ❌ (with verifier error)"); err?; } + err @ Err(CredentialError::FailedToDecodeJWT) => { + println!("Proof... ❌"); + println!("Issuer... ❌"); + err?; + } Ok(_) => { println!("Proof... ✅"); println!("Issuer... ✅"); diff --git a/trustchain-core/src/vc.rs b/trustchain-core/src/vc.rs index 2ce30f72..d625502b 100644 --- a/trustchain-core/src/vc.rs +++ b/trustchain-core/src/vc.rs @@ -9,6 +9,9 @@ pub enum CredentialError { /// No issuer present in credential. #[error("No issuer.")] NoIssuerPresent, + /// Failed to decode JWT error. + #[error("Failed to decode JWT.")] + FailedToDecodeJWT, /// Wrapped error for Verifier error. #[error("A wrapped Verifier error: {0}")] VerifierError(VerifierError), diff --git a/trustchain-core/src/verifier.rs b/trustchain-core/src/verifier.rs index 756d21e5..af804579 100644 --- a/trustchain-core/src/verifier.rs +++ b/trustchain-core/src/verifier.rs @@ -19,7 +19,7 @@ pub enum VerifierError { InvalidSignature(String), /// Invalid root DID after self-controller reached in path. #[error("Invalid root DID error: {0}")] - InvalidRoot(Box), + InvalidRoot(Box), /// Invalid root with error: #[error("Invalid root DID ({0}) with timestamp: {1}.")] InvalidRootTimestamp(String, Timestamp), @@ -122,7 +122,7 @@ pub enum VerifierError { TimestampVerificationError(String), /// Error fetching verification material. #[error("Error fetching verification material: {0}. Error: {1}")] - ErrorFetchingVerificationMaterial(String, Box), + ErrorFetchingVerificationMaterial(String, Box), /// Failed to fetch verification material. #[error("Failed to fetch verification material: {0}")] FailureToFetchVerificationMaterial(String), From 14ccc369d671199cfbf15dcacd7931fa0eb91d2e Mon Sep 17 00:00:00 2001 From: Ed Chapman <93717706+edchapman88@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:47:16 +0100 Subject: [PATCH 21/26] cleanup imports Co-authored-by: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> --- trustchain-api/src/api.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 6a256986..aa925991 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -21,7 +21,6 @@ use trustchain_ion::{ attest::attest_operation, attestor::IONAttestor, create::create_operation, get_ion_resolver, verifier::IONVerifier, }; - use crate::TrustchainAPI; /// API for Trustchain CLI DID functionality. From 470983e4dad8bf5e6be86ee1a42c8d46d6820985 Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:50:34 +0100 Subject: [PATCH 22/26] Move comment --- trustchain-api/src/api.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index aa925991..f12b3776 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -155,13 +155,12 @@ pub trait TrustchainVPAPI { .as_ref() .ok_or(PresentationError::NoCredentialsPresent)?; + // 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()) .collect(); - - // Verify signatures and issuers for each credential included in the presentation stream::iter(credentials.into_iter().zip(ldp_options_vec)) .map(Ok) .try_for_each_concurrent(limit, |(credential_or_jwt, ldp_options)| async move { From 9edd5077d3c25979e79679d3af136ef881816551 Mon Sep 17 00:00:00 2001 From: Ed Chapman - Turing Date: Wed, 6 Sep 2023 14:13:59 +0100 Subject: [PATCH 23/26] use keymanager to write test signing keys --- trustchain-api/src/api.rs | 2 +- trustchain-core/src/utils.rs | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index f12b3776..a419ce99 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -1,3 +1,4 @@ +use crate::TrustchainAPI; use async_trait::async_trait; use did_ion::sidetree::DocumentState; use futures::{stream, StreamExt, TryStreamExt}; @@ -21,7 +22,6 @@ use trustchain_ion::{ attest::attest_operation, attestor::IONAttestor, create::create_operation, get_ion_resolver, verifier::IONVerifier, }; -use crate::TrustchainAPI; /// API for Trustchain CLI DID functionality. #[async_trait] diff --git a/trustchain-core/src/utils.rs b/trustchain-core/src/utils.rs index 91b0153e..fa4ef9d9 100644 --- a/trustchain-core/src/utils.rs +++ b/trustchain-core/src/utils.rs @@ -1,9 +1,12 @@ //! Core utilities. +use crate::key_manager::KeyManager; +use crate::key_manager::KeyType; use crate::TRUSTCHAIN_DATA; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use ssi::did::{Document, ServiceEndpoint, VerificationMethod, VerificationMethodMap}; use ssi::jwk::JWK; +use ssi::one_or_many::OneOrMany; use std::path::{Path, PathBuf}; use std::sync::Once; @@ -12,25 +15,17 @@ pub fn type_of(_: &T) -> String { std::any::type_name::().to_string() } -/// Writes a given signing key for a given DID suffix to the key manager during test init only. -fn write_signing_key( - did_suffix: &str, - signing_key: &str, -) -> Result<(), Box> { - let path = Path::new(&std::env::var(TRUSTCHAIN_DATA)?) - .join("key_manager") - .join(did_suffix); - std::fs::create_dir_all(&path)?; - let path = path.join("signing_key.json"); - std::fs::write(path.clone(), signing_key)?; - Ok(()) -} +/// Utility key manager. +struct UtilsKeyManager; + +impl KeyManager for UtilsKeyManager {} /// Set-up tempdir and use as env var for `TRUSTCHAIN_DATA`. // https://stackoverflow.com/questions/58006033/how-to-run-setup-code-before-any-tests-run-in-rust static INIT: Once = Once::new(); pub fn init() { INIT.call_once(|| { + let utils_key_manager = UtilsKeyManager; // initialization code here let tempdir = tempfile::tempdir().unwrap(); std::env::set_var(TRUSTCHAIN_DATA, Path::new(tempdir.as_ref().as_os_str())); @@ -41,8 +36,10 @@ pub fn init() { let root_plus_2_did_suffix = "EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; 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 root_plus_2_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY","d":"bJnhIQgj0eQoRXIw5Xna6LErnili2ajMstoJLI21HiQ"}"#; - write_signing_key(root_plus_1_did_suffix, root_plus_1_signing_key).unwrap(); - write_signing_key(root_plus_2_did_suffix, root_plus_2_signing_key).unwrap(); + let root_plus_1_signing_jwk: JWK= serde_json::from_str(root_plus_1_signing_key).unwrap(); + let root_plus_2_signing_jwk: JWK= serde_json::from_str(root_plus_2_signing_key).unwrap(); + utils_key_manager.save_keys(root_plus_1_did_suffix, KeyType::SigningKey, &OneOrMany::One(root_plus_1_signing_jwk), false).unwrap(); + utils_key_manager.save_keys(root_plus_2_did_suffix, KeyType::SigningKey, &OneOrMany::One(root_plus_2_signing_jwk), false).unwrap(); }); } From 1c86f08782ed0123573d5f9c457da97a3d38f315 Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:16:09 +0100 Subject: [PATCH 24/26] Fix typo --- trustchain-core/src/vp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index 02452b17..5e8ea79b 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -19,7 +19,7 @@ pub enum PresentationError { #[error("A wrapped Credential error: {0}")] CredentialError(CredentialError), /// Wrapped variant for Verifier Error. - #[error("A wrapped Verfier error: {0}")] + #[error("A wrapped Verifier error: {0}")] VerifierError(VerifierError), /// Credentials verified, but holder failed to authenticate with invalid or missing presentation /// proof. From 79b4a290dc31e489d5b923310dbe4098c6bd8a4e Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:19:25 +0100 Subject: [PATCH 25/26] Remove unused variant --- trustchain-core/src/vp.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/trustchain-core/src/vp.rs b/trustchain-core/src/vp.rs index 5e8ea79b..8b3861bf 100644 --- a/trustchain-core/src/vp.rs +++ b/trustchain-core/src/vp.rs @@ -25,9 +25,6 @@ pub enum PresentationError { /// proof. #[error("Credentials verified for an unauthenticated holder: {0:?}")] VerifiedHolderUnauthenticated(VerificationResult), - /// Credentials verified, but holder DID failed verification (not part of valid Trustchain). - #[error("Credentials verified for an unverified holder: {0}")] - VerifiedHolderUnverfied(VerifierError), } impl From for PresentationError { From 4509eb99f00ba4bcbb2663e9bd6a0bf567c49839 Mon Sep 17 00:00:00 2001 From: Sam Greenbury <50113363+sgreenbury@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:31:59 +0100 Subject: [PATCH 26/26] Fix typo, remove unused comments --- trustchain-ion/src/attestor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 32cbf438..e6131195 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -176,7 +176,7 @@ impl Holder for IONAttestor { // - proof generation is handled by the ssi library // - ssi::ldp::ensure_or_pick_verification_relationship calls presentation.get_issuer() // which returns the holder (if Some, which is always the case) - // - ensure_or_pick_verification_relationship tries to resolve the holder DID and check it's + // - ensure_or_pick_verification_relationship tries to resolve the holder DID and check its // verification methods // - so the holder's DID must be resolvable async fn sign_presentation( @@ -207,8 +207,6 @@ impl Holder for IONAttestor { }; // Generate proof - // Example of verification_method derivation (optionally passed as a LinkedDataProofOption) - // let vm = format!("{}#{}", self.did(), signing_key.thumbprint().unwrap()); let proof = vp.generate_proof(&signing_key, &options, resolver).await?; // Add proof to credential vp.add_proof(proof); @@ -330,7 +328,6 @@ mod tests { let resolver = get_ion_resolver("http://localhost:3000/"); // 2. Load Attestor - // let attestor = IONAttestor::new(did); // Attestor let attestor = IONAttestor::try_from(AttestorData::new( did.to_string(),