use crate::{TrustchainAPI, DATA_ATTRIBUTE, DATA_CREDENTIAL_TEMPLATE};
use async_trait::async_trait;
use did_ion::sidetree::DocumentState;
use futures::{stream, StreamExt, TryStreamExt};
use sha2::{Digest, Sha256};
use ssi::{
did_resolve::DIDResolver,
jsonld::ContextLoader,
ldp::LinkedDataDocument,
vc::{Credential, CredentialOrJWT, LinkedDataProofOptions, Presentation, URI},
};
use std::error::Error;
use trustchain_core::{
chain::DIDChain,
holder::Holder,
issuer::{Issuer, IssuerError},
resolver::{ResolverResult, TrustchainResolver},
vc::{CredentialError, DataCredentialError},
verifier::{Timestamp, Verifier, VerifierError},
vp::PresentationError,
};
use trustchain_ion::{
attest::attest_operation, attestor::IONAttestor, create::create_operation, trustchain_resolver,
};
#[async_trait]
pub trait TrustchainDIDAPI {
fn create(
document_state: Option<DocumentState>,
verbose: bool,
) -> Result<String, Box<dyn Error>> {
create_operation(document_state, verbose)
}
async fn attest(did: &str, controlled_did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
attest_operation(did, controlled_did, verbose).await
}
async fn resolve(did: &str, resolver: &dyn TrustchainResolver) -> ResolverResult {
resolver.resolve_as_result(did).await
}
async fn verify<T, U>(
did: &str,
root_event_time: Timestamp,
verifier: &U,
) -> Result<DIDChain, VerifierError>
where
T: DIDResolver + Send,
U: Verifier<T> + Send + Sync,
{
verifier.verify(did, root_event_time).await
}
}
#[async_trait]
pub trait TrustchainVCAPI {
async fn sign(
mut credential: Credential,
did: &str,
linked_data_proof_options: Option<LinkedDataProofOptions>,
key_id: Option<&str>,
resolver: &dyn TrustchainResolver,
context_loader: &mut ContextLoader,
) -> Result<Credential, IssuerError> {
credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string())));
let attestor = IONAttestor::new(did);
attestor
.sign(
&credential,
linked_data_proof_options,
key_id,
resolver,
context_loader,
)
.await
}
async fn verify_credential<T, U>(
credential: &Credential,
linked_data_proof_options: Option<LinkedDataProofOptions>,
root_event_time: Timestamp,
verifier: &U,
context_loader: &mut ContextLoader,
) -> Result<DIDChain, CredentialError>
where
T: DIDResolver + Send,
U: Verifier<T> + Send + Sync,
{
let result = credential
.verify(
linked_data_proof_options,
verifier.resolver().as_did_resolver(),
context_loader,
)
.await;
if !result.errors.is_empty() {
return Err(CredentialError::VerificationResultError(result));
}
let issuer = credential
.get_issuer()
.ok_or(CredentialError::NoIssuerPresent)?;
Ok(verifier.verify(issuer, root_event_time).await?)
}
}
#[async_trait]
pub trait TrustchainVPAPI {
async fn sign_presentation(
presentation: Presentation,
did: &str,
key_id: Option<&str>,
endpoint: &str,
linked_data_proof_options: Option<LinkedDataProofOptions>,
context_loader: &mut ContextLoader,
) -> Result<Presentation, PresentationError> {
let resolver = trustchain_resolver(endpoint);
let attestor = IONAttestor::new(did);
Ok(attestor
.sign_presentation(
&presentation,
linked_data_proof_options,
key_id,
&resolver,
context_loader,
)
.await?)
}
async fn verify_presentation<T, U>(
presentation: &Presentation,
ldp_options: Option<LinkedDataProofOptions>,
root_event_time: Timestamp,
verifier: &U,
context_loader: &mut ContextLoader,
) -> Result<(), PresentationError>
where
T: DIDResolver + Send,
U: Verifier<T> + Send + Sync,
{
let credentials = presentation
.verifiable_credential
.as_ref()
.ok_or(PresentationError::NoCredentialsPresent)?;
let limit = Some(5);
let ldp_opts_and_context_loader: Vec<(Option<LinkedDataProofOptions>, ContextLoader)> = (0
..credentials.len())
.map(|_| (ldp_options.clone(), context_loader.clone()))
.collect();
stream::iter(credentials.into_iter().zip(ldp_opts_and_context_loader))
.map(Ok)
.try_for_each_concurrent(
limit,
|(credential_or_jwt, (ldp_opts, mut context_loader))| async move {
match credential_or_jwt {
CredentialOrJWT::Credential(credential) => {
TrustchainAPI::verify_credential(
credential,
ldp_opts,
root_event_time,
verifier,
&mut context_loader,
)
.await
.map(|_| ())
}
CredentialOrJWT::JWT(jwt) => {
match Credential::decode_verify_jwt(
jwt,
ldp_opts.clone(),
verifier.resolver().as_did_resolver(),
&mut context_loader,
)
.await
.0
.ok_or(CredentialError::FailedToDecodeJWT)
{
Ok(credential) => TrustchainAPI::verify_credential(
&credential,
ldp_opts,
root_event_time,
verifier,
&mut context_loader,
)
.await
.map(|_| ()),
Err(e) => Err(e),
}
}
}
},
)
.await?;
let result = presentation
.verify(
ldp_options.clone(),
verifier.resolver().as_did_resolver(),
context_loader,
)
.await;
if !result.errors.is_empty() {
return Err(PresentationError::VerifiedHolderUnauthenticated(result));
}
Ok(())
}
}
#[async_trait]
pub trait TrustchainDataAPI {
async fn sign_data(
bytes: &[u8],
did: &str,
linked_data_proof_options: Option<LinkedDataProofOptions>,
key_id: Option<&str>,
resolver: &dyn TrustchainResolver,
context_loader: &mut ContextLoader,
) -> Result<Credential, IssuerError> {
let mut credential = Credential::from_json_unsigned(DATA_CREDENTIAL_TEMPLATE).unwrap();
credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string())));
credential.issuance_date = Some(chrono::offset::Local::now().into());
let data_hash = Sha256::digest(bytes);
let data_element = credential
.credential_subject
.to_single_mut()
.expect("Template credential has a single credentialSubject.")
.property_set
.as_mut()
.expect("Template credential has a property set.")
.get_mut(DATA_ATTRIBUTE)
.expect("Template credential has a dataset property.");
*data_element = hex::encode(data_hash).to_string().into();
let attestor = IONAttestor::new(did);
Ok(attestor
.sign(
&credential,
linked_data_proof_options,
key_id,
resolver,
context_loader,
)
.await?)
}
async fn verify_data<T, U>(
bytes: &[u8],
credential: &Credential,
linked_data_proof_options: Option<LinkedDataProofOptions>,
root_event_time: Timestamp,
verifier: &U,
context_loader: &mut ContextLoader,
) -> Result<DIDChain, DataCredentialError>
where
T: DIDResolver + Send,
U: Verifier<T> + Send + Sync,
{
let actual_hash = hex::encode(Sha256::digest(bytes));
let expected_hash = credential
.credential_subject
.to_single()
.ok_or(DataCredentialError::ManyCredentialSubject(
credential.credential_subject.clone(),
))?
.property_set
.as_ref()
.ok_or(DataCredentialError::MissingAttribute(
"property_set".to_string(),
))?
.get(DATA_ATTRIBUTE)
.ok_or(DataCredentialError::MissingAttribute(
DATA_ATTRIBUTE.to_string(),
))?
.as_str()
.expect("dataset attribute is a str");
if actual_hash != expected_hash {
return Err(DataCredentialError::MismatchedHashDigests(
expected_hash.to_string(),
actual_hash,
));
};
TrustchainAPI::verify_credential(
credential,
linked_data_proof_options,
root_event_time,
verifier,
context_loader,
)
.await
.map_err(DataCredentialError::CredentialError)
}
}
#[cfg(test)]
mod tests {
use crate::api::{
TrustchainDataAPI, TrustchainVCAPI, TrustchainVPAPI, DATA_CREDENTIAL_TEMPLATE,
};
use crate::TrustchainAPI;
use sha2::{Digest, Sha256};
use ssi::jsonld::ContextLoader;
use ssi::ldp::now_ns;
use ssi::one_or_many::OneOrMany;
use ssi::vc::{Credential, CredentialOrJWT, Presentation, VCDateTime, URI};
use trustchain_core::utils::init;
use trustchain_core::vc::{CredentialError, DataCredentialError};
use trustchain_core::vp::PresentationError;
use trustchain_core::{holder::Holder, issuer::Issuer};
use trustchain_ion::attestor::IONAttestor;
use trustchain_ion::trustchain_resolver;
use trustchain_ion::verifier::TrustchainVerifier;
const ROOT_EVENT_TIME_1: u64 = 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"
],
"type": ["VerifiableCredential"],
"issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q",
"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"; let issuer = IONAttestor::new(issuer_did);
let mut vc_with_proof = signed_credential(issuer).await;
let resolver = trustchain_resolver("http://localhost:3000/");
let mut context_loader = ContextLoader::default();
let res = TrustchainAPI::verify_credential(
&vc_with_proof,
None,
ROOT_EVENT_TIME_1,
&TrustchainVerifier::new(resolver),
&mut context_loader,
)
.await;
assert!(res.is_ok());
vc_with_proof.expiration_date = Some(VCDateTime::from(now_ns()));
let resolver = trustchain_resolver("http://localhost:3000/");
let res = TrustchainAPI::verify_credential(
&vc_with_proof,
None,
ROOT_EVENT_TIME_1,
&TrustchainVerifier::new(resolver),
&mut context_loader,
)
.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"; 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 = trustchain_resolver("http://localhost:3000/");
let mut context_loader = ContextLoader::default();
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()),
])),
..Default::default()
};
presentation = holder
.sign_presentation(&presentation, None, None, &resolver, &mut context_loader)
.await
.unwrap();
println!("{}", serde_json::to_string_pretty(&presentation).unwrap());
let res = TrustchainAPI::verify_presentation(
&presentation,
None,
ROOT_EVENT_TIME_1,
&TrustchainVerifier::new(resolver),
&mut context_loader,
)
.await;
println!("{:?}", res);
assert!(res.is_ok());
}
#[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
#[tokio::test]
async fn test_verify_presentation_unauthenticated() {
init();
let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; let issuer = IONAttestor::new(issuer_did);
let vc_with_proof = signed_credential(issuer).await;
let resolver = trustchain_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,
&TrustchainVerifier::new(resolver),
&mut ContextLoader::default()
)
.await,
Err(PresentationError::VerifiedHolderUnauthenticated(..))
));
}
async fn signed_credential(attestor: IONAttestor) -> Credential {
let resolver = trustchain_resolver("http://localhost:3000/");
let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap();
attestor
.sign(&vc, None, None, &resolver, &mut ContextLoader::default())
.await
.unwrap()
}
async fn signed_data_credential(issuer_did: &str, bytes: &[u8]) -> Credential {
let attestor = IONAttestor::new(issuer_did);
let resolver = trustchain_resolver("http://localhost:3000/");
let mut vc: Credential = serde_json::from_str(DATA_CREDENTIAL_TEMPLATE).unwrap();
vc.issuer = Some(ssi::vc::Issuer::URI(URI::String(issuer_did.to_string())));
let data_element = vc
.credential_subject
.to_single_mut()
.expect("Template credential has a single credentialSubject.")
.property_set
.as_mut()
.expect("Template credential has a property set.")
.get_mut(crate::DATA_ATTRIBUTE)
.expect("Template credential has a dataset property.");
*data_element = hex::encode(Sha256::digest(bytes)).to_string().into();
attestor
.sign(&vc, None, None, &resolver, &mut ContextLoader::default())
.await
.unwrap()
}
#[test]
fn test_data_credential_template() {
let credential = Credential::from_json_unsigned(DATA_CREDENTIAL_TEMPLATE).unwrap();
assert_eq!(credential.issuer.unwrap().get_id(), "did:ion:test:XYZ");
}
#[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
#[tokio::test]
async fn test_verify_data() {
init();
let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A";
let bytes = "test-data-content".as_bytes();
let expected_hash = hex::encode(Sha256::digest(bytes));
let vc_with_proof = signed_data_credential(issuer_did, bytes).await;
let resolver = trustchain_resolver("http://localhost:3000/");
let mut context_loader = ContextLoader::default();
let res = TrustchainAPI::verify_data(
bytes,
&vc_with_proof,
None,
ROOT_EVENT_TIME_1,
&TrustchainVerifier::new(resolver),
&mut context_loader,
)
.await;
assert!(res.is_ok());
let bytes = "different-data-content".as_bytes();
let resolver = trustchain_resolver("http://localhost:3000/");
let res = TrustchainAPI::verify_data(
bytes,
&vc_with_proof,
None,
ROOT_EVENT_TIME_1,
&TrustchainVerifier::new(resolver),
&mut context_loader,
)
.await;
assert!(res.is_err());
if let DataCredentialError::MismatchedHashDigests(expected, actual) = res.err().unwrap() {
assert_eq!(expected, expected_hash);
assert_ne!(actual, expected_hash);
} else {
panic!("Unexpected CredentialError variant.")
}
}
}