Skip to content

Commit

Permalink
Merge pull request #116 from alan-turing-institute/59-vp-api
Browse files Browse the repository at this point in the history
Verifiable presentation API for ION attestor #59
  • Loading branch information
edchapman88 authored Sep 6, 2023
2 parents 9c3f73b + 4509eb9 commit ca915a6
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 108 deletions.
4 changes: 4 additions & 0 deletions trustchain-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ async-trait = "0.1"
serde_json = "1.0"
ssi = "0.4"
did-ion = "0.1.0"
futures = "0.3.28"

[dev-dependencies]
tokio = {version = "1.20.1", features = ["full"]}
288 changes: 284 additions & 4 deletions trustchain-api/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
use crate::TrustchainAPI;
use async_trait::async_trait;
use did_ion::sidetree::DocumentState;
use futures::{stream, StreamExt, TryStreamExt};
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::{Resolver, ResolverResult},
vc::CredentialError,
verifier::{Timestamp, Verifier, VerifierError},
vp::PresentationError,
};
use trustchain_ion::{
attest::attest_operation, attestor::IONAttestor, create::create_operation, get_ion_resolver,
verifier::IONVerifier,
};
use trustchain_ion::{attest::attest_operation, attestor::IONAttestor, create::create_operation};

/// API for Trustchain CLI DID functionality.
#[async_trait]
Expand Down Expand Up @@ -119,5 +126,278 @@ pub trait TrustchainVCAPI {
}
}

#[async_trait]
pub trait TrustchainVPAPI {
/// Signs a presentation constructing a verifiable presentation.
async fn sign_presentation(
presentation: Presentation,
did: &str,
key_id: Option<&str>,
endpoint: &str,
linked_data_proof_options: Option<LinkedDataProofOptions>,
) -> Result<Presentation, PresentationError> {
let resolver = get_ion_resolver(endpoint);
let attestor = IONAttestor::new(did);
Ok(attestor
.sign_presentation(&presentation, linked_data_proof_options, key_id, &resolver)
.await?)
}
/// Verifies a verifiable presentation.
async fn verify_presentation<T: DIDResolver + Send + Sync>(
presentation: &Presentation,
ldp_options: Option<LinkedDataProofOptions>,
root_event_time: Timestamp,
verifier: &IONVerifier<T>,
) -> Result<(), PresentationError> {
// Check credentials are present in presentation
let credentials = presentation
.verifiable_credential
.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<Option<LinkedDataProofOptions>> = (0..credentials.len())
.map(|_| ldp_options.clone())
.collect();
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(|_| ()),
CredentialOrJWT::JWT(jwt) => {
// decode and verify for credential jwts
match Credential::decode_verify_jwt(
jwt,
ldp_options.clone(),
verifier.resolver(),
)
.await
.0
.ok_or(CredentialError::FailedToDecodeJWT)
{
Ok(credential) => TrustchainAPI::verify_credential(
&credential,
ldp_options,
root_event_time,
verifier,
)
.await
.map(|_| ()),
Err(e) => Err(e),
}
}
}
})
.await?;

// 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));
}
Ok(())
}
}

#[cfg(test)]
mod tests {}
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, VCDateTime};
use trustchain_core::utils::init;
use trustchain_core::vc::CredentialError;
use trustchain_core::vp::PresentationError;
use trustchain_core::{holder::Holder, 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",
"issuanceDate": "2023-09-06T12:15:08.630033Z",
"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_verify_credential() {
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/");
let res = TrustchainAPI::verify_credential(
&vc_with_proof,
None,
ROOT_EVENT_TIME_1,
&IONVerifier::new(resolver),
)
.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() {
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);

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()),
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()),
// 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.
// If the holder field is left unpopulated here, it is automatically populated during
// signing (with the did of the presentation signer) in `holder.sign_presentation()`
..Default::default()
};

presentation = holder
.sign_presentation(&presentation, None, None, &resolver)
.await
.unwrap();
println!("{}", serde_json::to_string_pretty(&presentation).unwrap());
let res = TrustchainAPI::verify_presentation(
&presentation,
None,
ROOT_EVENT_TIME_1,
&IONVerifier::new(resolver),
)
.await;
println!("{:?}", res);
assert!(res.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() {
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;
let resolver = get_ion_resolver("http://localhost:3000/");
let presentation = Presentation {
verifiable_credential: Some(OneOrMany::Many(vec![CredentialOrJWT::Credential(
vc_with_proof,
)])),
..Default::default()
};

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

// Helper function to create a signed credential given an attesor.
async fn signed_credential(attestor: IONAttestor) -> Credential {
let resolver = get_ion_resolver("http://localhost:3000/");
let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap();
attestor.sign(&vc, None, None, &resolver).await.unwrap()
}
}
3 changes: 2 additions & 1 deletion trustchain-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 {}
5 changes: 5 additions & 0 deletions trustchain-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Issuer... ❌ (with verifier error)");
err?;
}
err @ Err(CredentialError::FailedToDecodeJWT) => {
println!("Proof... ❌");
println!("Issuer... ❌");
err?;
}
Ok(_) => {
println!("Proof... ✅");
println!("Issuer... ✅");
Expand Down
48 changes: 48 additions & 0 deletions trustchain-core/src/holder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! 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::{LinkedDataProofOptions, Presentation};
use thiserror::Error;

/// An error relating to a Trustchain holder.
#[derive(Error, Debug)]
pub enum HolderError {
/// Wrapped error for SSI error.
#[error("A wrapped variant for an SSI error: {0}")]
SSI(ssi::error::Error),
/// Wrapped error for 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<ssi::error::Error> for HolderError {
fn from(err: ssi::error::Error) -> Self {
HolderError::SSI(err)
}
}

impl From<KeyManagerError> for HolderError {
fn from(err: KeyManagerError) -> Self {
HolderError::KeyManager(err)
}
}

/// 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
/// proof. The `@context` of the presentation has linked-data fields strictly checked as part of
/// proof generation.
async fn sign_presentation<T: DIDResolver>(
&self,
presentation: &Presentation,
linked_data_proof_options: Option<LinkedDataProofOptions>,
key_id: Option<&str>,
resolver: &T,
) -> Result<Presentation, HolderError>;
}
Loading

0 comments on commit ca915a6

Please sign in to comment.