use std::str::FromStr;
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use trust_tasks_rs::{ErrorPayload, TrustTask, TrustTaskCode};
use uuid::Uuid;
use vta_sdk::protocols::credential_exchange::ISSUE as CREDENTIAL_ISSUE_TYPE;
use vta_sdk::trust_task_proof::{TrustTaskVmResolver, verify_trust_task_proof_with};
use crate::errors::OpenVTCError;
use crate::messaging::{MESSAGE_EXPIRY_SECS, build_didcomm_message, unix_now};
#[derive(Debug, thiserror::Error)]
pub enum WireError {
#[error("malformed vetting document: {0}")]
Malformed(String),
#[error("document type does not match the message type")]
TypeMismatch,
#[error("document issuer is not the authenticated sender")]
IssuerNotSender,
#[error("document proof did not verify")]
Proof(String),
#[error("document is not signed by its issuer")]
WrongSigner,
}
pub type Document = TrustTask<Value>;
#[must_use]
pub fn new_id() -> String {
format!("urn:uuid:{}", Uuid::new_v4())
}
fn config_error(what: &str, e: impl std::fmt::Display) -> OpenVTCError {
OpenVTCError::Config(format!("{what}: {e}"))
}
pub fn document<P: Serialize>(
type_uri: &str,
issuer: &str,
recipient: &str,
id: String,
payload: &P,
) -> Result<TrustTask<Value>, OpenVTCError> {
crate::trust_task_doc::build(type_uri, issuer, recipient, id, payload)
}
pub fn response<P: Serialize>(
request: &TrustTask<Value>,
payload: &P,
) -> Result<TrustTask<Value>, OpenVTCError> {
let payload = serde_json::to_value(payload).map_err(|e| config_error("vetting payload", e))?;
Ok(request.respond_with(new_id(), payload))
}
pub fn refusal(
request: &TrustTask<Value>,
code: &str,
message: Option<&str>,
) -> Result<TrustTask<Value>, OpenVTCError> {
let code = TrustTaskCode::from_str(code).map_err(|e| config_error("error code", e))?;
let mut payload = ErrorPayload::new(code);
if let Some(message) = message {
payload = payload.with_message(message);
}
let error = request.reject_with(new_id(), payload);
serde_json::to_value(&error)
.and_then(serde_json::from_value)
.map_err(|e| config_error("error document", e))
}
pub async fn sign(doc: &mut TrustTask<Value>, signer: &Secret) -> Result<(), OpenVTCError> {
crate::capabilities::sign_document(doc, signer).await
}
#[must_use]
pub fn is_community_bound(type_uri: &str) -> bool {
use vta_sdk::protocols::vetting::{
VETTING_REVOKE_STATEMENT_TYPE, VETTING_VETTER_GRANT_TYPE, VETTING_VETTER_LIST_TYPE,
VETTING_VETTER_PROFILE_TYPE, VETTING_VETTER_RESEND_TYPE,
};
if super::protocol::JoinProtocol::from_manifest_request(type_uri).is_some() {
return true;
}
matches!(
type_uri,
VETTING_REVOKE_STATEMENT_TYPE
| VETTING_VETTER_GRANT_TYPE
| VETTING_VETTER_LIST_TYPE
| VETTING_VETTER_PROFILE_TYPE
| VETTING_VETTER_RESEND_TYPE
| pcs::ROOT_TYPE
| pcs::TOKENS_TYPE
| pcs::EVENT_MODE_TYPE
| pcs::CHALLENGE_TYPE
)
}
fn carriage_type(doc: &TrustTask<Value>) -> String {
let type_uri = doc.type_uri.to_string();
if is_community_bound(&type_uri) {
crate::capabilities::TRUST_TASK_ENVELOPE_TYPE.to_string()
} else {
type_uri
}
}
pub fn to_message(doc: &TrustTask<Value>) -> Result<Message, OpenVTCError> {
let from = doc
.issuer
.clone()
.ok_or_else(|| OpenVTCError::Config("vetting document has no issuer".into()))?;
let to = doc
.recipient
.clone()
.ok_or_else(|| OpenVTCError::Config("vetting document has no recipient".into()))?;
let body = serde_json::to_value(doc).map_err(|e| config_error("vetting document", e))?;
let now = unix_now();
let mut builder = Message::build(doc.id.clone(), carriage_type(doc), body)
.from(from)
.to(to)
.created_time(now)
.expires_time(now + MESSAGE_EXPIRY_SECS);
if let Some(thread) = &doc.thread_id {
builder = builder.thid(thread.clone());
}
Ok(builder.finalize())
}
pub async fn credential_delivery(
vetter_did: &str,
applicant_did: &str,
statement: &Value,
session_id: &str,
signer: &Secret,
) -> Result<Message, OpenVTCError> {
let mut doc = document(
CREDENTIAL_ISSUE_TYPE,
vetter_did,
applicant_did,
new_id(),
&json!({ "credential_response": { "credential": statement } }),
)?;
doc.thread_id = Some(session_id.to_string());
sign(&mut doc, signer).await?;
to_message(&doc)
}
pub fn hidden_attestation(
vetter_did: &str,
applicant_did: &str,
attestation: &Value,
session_id: &str,
) -> Result<Message, OpenVTCError> {
build_didcomm_message(
HIDDEN_ATTESTATION_TYPE,
attestation.clone(),
vetter_did,
applicant_did,
Some(session_id),
)
.map_err(|e| config_error("attestation delivery", e))
}
pub const HIDDEN_ATTESTATION_TYPE: &str = "https://trusttasks.org/spec/vetting/attestation/0.1";
#[derive(Debug, Clone)]
pub struct Opened<P> {
pub document: TrustTask<Value>,
pub payload: P,
}
pub async fn open<P: DeserializeOwned>(
message: &Message,
sender: &str,
resolver: &TrustTaskVmResolver,
) -> Result<Opened<P>, WireError> {
let document: TrustTask<Value> = serde_json::from_value(message.body.clone())
.map_err(|e| WireError::Malformed(e.to_string()))?;
if document.type_uri.to_string() != message.typ {
return Err(WireError::TypeMismatch);
}
if document.issuer.as_deref() != Some(sender) {
return Err(WireError::IssuerNotSender);
}
let signer = verify_trust_task_proof_with(&document, resolver)
.await
.map_err(|e| WireError::Proof(e.cause().unwrap_or("no detail").to_string()))?;
if signer != sender {
return Err(WireError::WrongSigner);
}
let payload = serde_json::from_value(document.payload.clone())
.map_err(|e| WireError::Malformed(e.to_string()))?;
Ok(Opened { document, payload })
}
pub fn manifest_request(
applicant_did: &str,
community_did: &str,
protocol: super::protocol::JoinProtocol,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
protocol.manifest_type(),
applicant_did,
community_did,
new_id(),
&json!({}),
)
}
pub fn vetter_list_request(
issuer: &str,
community_did: &str,
body: &vta_sdk::protocols::vetting::vetters::list::v0_1::Payload,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
vta_sdk::protocols::vetting::VETTING_VETTER_LIST_TYPE,
issuer,
community_did,
new_id(),
body,
)
}
pub fn vetter_profile_request(
vetter_did: &str,
community_did: &str,
body: &vta_sdk::protocols::vetting::vetters::profile::v0_1::Payload,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
vta_sdk::protocols::vetting::VETTING_VETTER_PROFILE_TYPE,
vetter_did,
community_did,
new_id(),
body,
)
}
pub mod pcs {
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const ROOT_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/vetters/pcs-root/0.1";
pub const TOKENS_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/vetters/pcs-tokens/0.1";
pub const EVENT_MODE_TYPE: &str =
"https://trusttasks.org/spec/vtc/vetting/vetters/event-mode/0.1";
pub const CHALLENGE_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/pcs-challenge/0.1";
#[must_use]
pub fn response_of(type_uri: &str) -> String {
format!("{type_uri}#response")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RootRequest {
pub label: String,
pub id: String,
pub request: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RootResponse {
pub label: String,
pub pre_credential: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ext: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TokensRequest {
pub label: String,
pub tick: u32,
pub requests: Vec<TokenRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TokenRequest {
pub commitment: String,
pub opening_proof: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TokensResponse {
pub label: String,
pub tick: u32,
pub pre_credentials: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ext: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventModeRequest {
pub event_id: String,
pub tier: String,
pub window: EventWindow,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventWindow {
pub start_date: chrono::NaiveDate,
pub end_date: chrono::NaiveDate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventModeResponse {
pub event_id: String,
pub state: String,
pub tier: String,
pub window: EventWindow,
pub group_size: usize,
pub group_floor: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drip_per_tick: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub closes_after: Option<chrono::NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ext: Option<Value>,
}
pub const EVENT_APPROVED: &str = "approved";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChallengeRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub criterion_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChallengeResponse {
pub challenge: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ext: Option<Value>,
}
}
pub fn pcs_root_request(
vetter_did: &str,
community_did: &str,
body: &pcs::RootRequest,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(pcs::ROOT_TYPE, vetter_did, community_did, new_id(), body)
}
pub fn pcs_tokens_request(
vetter_did: &str,
community_did: &str,
body: &pcs::TokensRequest,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(pcs::TOKENS_TYPE, vetter_did, community_did, new_id(), body)
}
pub fn pcs_event_mode_request(
vetter_did: &str,
community_did: &str,
body: &pcs::EventModeRequest,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
pcs::EVENT_MODE_TYPE,
vetter_did,
community_did,
new_id(),
body,
)
}
pub fn pcs_challenge_request(
applicant_did: &str,
community_did: &str,
criterion_id: Option<&str>,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
pcs::CHALLENGE_TYPE,
applicant_did,
community_did,
new_id(),
&pcs::ChallengeRequest {
criterion_id: criterion_id.map(ToString::to_string),
},
)
}
pub fn vetter_resend_request(
vetter_did: &str,
community_did: &str,
) -> Result<TrustTask<Value>, OpenVTCError> {
document(
vta_sdk::protocols::vetting::VETTING_VETTER_RESEND_TYPE,
vetter_did,
community_did,
new_id(),
&vta_sdk::protocols::vetting::vetters::resend::v0_1::Payload::default(),
)
}
pub async fn sign_and_send(
config: &crate::config::Config,
tdk: &affinidi_tdk::TDK,
service: &crate::didcomm::Messaging,
persona: crate::config::account::PersonaId,
document: TrustTask<Value>,
) -> Result<String, OpenVTCError> {
send_reply(
config,
tdk,
service,
super::inbound::Reply {
persona,
document,
eligibility: None,
},
)
.await
}
pub async fn attach_eligibility(
document: &mut TrustTask<Value>,
holder: &Secret,
eligibility: super::inbound::EligibilityPresentation,
) -> Result<(), OpenVTCError> {
let vp = vta_sdk::vetting::eligibility::build_eligibility_vp(
holder,
eligibility.credentials,
&eligibility.nonce,
&eligibility.domain,
)
.await
.map_err(|e| config_error("eligibility presentation", e))?;
document
.payload
.as_object_mut()
.ok_or_else(|| OpenVTCError::Config("vetting reply payload is not an object".into()))?
.insert("eligibilityVp".into(), vp);
Ok(())
}
pub async fn send_reply(
config: &crate::config::Config,
tdk: &affinidi_tdk::TDK,
service: &crate::didcomm::Messaging,
reply: super::inbound::Reply,
) -> Result<String, OpenVTCError> {
let keys = config.get_persona_keys_for(reply.persona, tdk).await?;
let mut document = reply.document;
if let Some(eligibility) = reply.eligibility {
attach_eligibility(&mut document, &keys.authentication.secret, eligibility).await?;
}
sign(&mut document, &keys.authentication.secret).await?;
let message = to_message(&document)?;
let (from, to) = (
document.issuer.as_deref().unwrap_or_default(),
document.recipient.as_deref().unwrap_or_default(),
);
crate::didcomm::send_message(service, config, &message, from, to)
.await
.map_err(|e| config_error("send vetting document", e))?;
Ok(document.id)
}
pub async fn send_statement(
config: &crate::config::Config,
service: &crate::didcomm::Messaging,
vetter_did: &str,
applicant_did: &str,
statement: &Value,
session_id: &str,
signer: &Secret,
) -> Result<(), OpenVTCError> {
let message =
credential_delivery(vetter_did, applicant_did, statement, session_id, signer).await?;
crate::didcomm::send_message(service, config, &message, vetter_did, applicant_did)
.await
.map_err(|e| config_error("send vetting statement", e))
}
#[must_use]
pub fn delivered_statement(body: &Value) -> Option<&Value> {
body.get("payload")
.and_then(statement_in)
.or_else(|| statement_in(body))
}
#[must_use]
pub fn statement_in(payload: &Value) -> Option<&Value> {
let credential = payload.pointer("/credential_response/credential")?;
(credential
.pointer("/credentialSubject/predicate")
.and_then(Value::as_str)
== Some(vta_sdk::protocols::vetting::VETTED_PREDICATE)
&& !is_community_issued(credential))
.then_some(credential)
}
#[must_use]
pub fn is_community_issued(credential: &Value) -> bool {
let issuer = match credential.get("issuer") {
Some(Value::String(id)) => Some(id.as_str()),
Some(Value::Object(o)) => o.get("id").and_then(Value::as_str),
_ => None,
};
let Some(value) = credential.pointer("/credentialSubject/object/value") else {
return false;
};
let community = value.get("community").and_then(Value::as_str);
issuer.is_some()
&& issuer == community
&& [
"identityCommitment",
"cardDigestMultibase",
"declaredRelationship",
]
.iter()
.all(|member| value.get(member).is_none())
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use vta_sdk::protocols::vetting::{
VETTING_DECLINE_TYPE, VETTING_REQUEST_ERR_CAPACITY, VETTING_REQUEST_TYPE, decline, session,
};
pub(crate) fn secret(seed_byte: u8) -> Secret {
let mut secret = Secret::generate_ed25519(None, Some(&[seed_byte; 32]));
let public = secret.get_public_keymultibase().unwrap();
secret.id = format!("did:key:{public}#{public}");
secret
}
pub(crate) fn did(secret: &Secret) -> String {
secret.id.split('#').next().unwrap().to_string()
}
fn webvh_persona_secret() -> Secret {
let mut secret = Secret::generate_ed25519(None, Some(&[7u8; 32]));
secret.id = "did:webvh:QmScid:example.com:alice#key-0".to_string();
secret
}
#[tokio::test]
async fn a_persona_signs_as_its_webvh_verification_method() {
let signer = webvh_persona_secret();
let persona = "did:webvh:QmScid:example.com:alice";
let mut doc = document(
VETTING_DECLINE_TYPE,
persona,
"did:webvh:QmScid:example.com:bob",
new_id(),
&decline(),
)
.unwrap();
sign(&mut doc, &signer).await.unwrap();
let signed = serde_json::to_value(&doc).unwrap();
assert_eq!(
signed.pointer("/proof/verificationMethod"),
Some(&json!("did:webvh:QmScid:example.com:alice#key-0"))
);
let draft = vta_sdk::vetting::card::CardDraft {
id: new_id(),
publisher: persona.to_string(),
audience: "did:webvh:QmScid:example.com:bob".to_string(),
community: "did:webvh:QmScid:example.com:vtc".to_string(),
challenge: "c".repeat(43),
domain: "did:webvh:QmScid:example.com:vtc".to_string(),
issued_at: chrono::Utc::now(),
validity: chrono::Duration::minutes(10),
claims: vec![
session::v0_1::VettingCardClaim::try_from(
session::v0_1::VettingCardClaim::builder()
.type_("name.legal")
.value(json!("Alice Example"))
.provenance("selfAsserted"),
)
.unwrap(),
],
identity_types: vec!["name.legal".into()],
salt: vta_sdk::vetting::card::new_commitment_salt().unwrap(),
};
let card = vta_sdk::vetting::card::sign_card(draft, &signer)
.await
.unwrap();
assert_eq!(
card.pointer("/proof/verificationMethod"),
Some(&json!("did:webvh:QmScid:example.com:alice#key-0"))
);
}
fn decline() -> decline::v0_1::Payload {
decline::v0_1::Payload::try_from(decline::v0_1::Payload::builder().request_id("r1"))
.unwrap()
}
#[tokio::test]
async fn a_signed_document_opens_for_its_sender_only() {
let (vetter, applicant, other) = (secret(1), secret(2), secret(3));
let mut doc = document(
VETTING_DECLINE_TYPE,
&did(&vetter),
&did(&applicant),
new_id(),
&decline(),
)
.unwrap();
sign(&mut doc, &vetter).await.unwrap();
let message = to_message(&doc).unwrap();
assert_eq!(message.id, doc.id);
let resolver = TrustTaskVmResolver::did_key_only();
let opened: Opened<decline::v0_1::Payload> =
open(&message, &did(&vetter), &resolver).await.unwrap();
assert_eq!(opened.payload.request_id.as_str(), "r1");
assert!(matches!(
open::<decline::v0_1::Payload>(&message, &did(&other), &resolver).await,
Err(WireError::IssuerNotSender)
));
}
#[tokio::test]
async fn an_unsigned_document_does_not_open() {
let (vetter, applicant) = (secret(1), secret(2));
let doc = document(
VETTING_DECLINE_TYPE,
&did(&vetter),
&did(&applicant),
new_id(),
&decline(),
)
.unwrap();
let message = to_message(&doc).unwrap();
assert!(matches!(
open::<decline::v0_1::Payload>(
&message,
&did(&vetter),
&TrustTaskVmResolver::did_key_only()
)
.await,
Err(WireError::Proof(_))
));
}
#[tokio::test]
async fn a_resend_request_is_signed_under_authentication_proof_purpose() {
let vetter = secret(3);
let mut request = vetter_resend_request(&did(&vetter), "did:key:zVtc").unwrap();
sign(&mut request, &vetter).await.unwrap();
assert_eq!(
request.proof.as_ref().unwrap().proof_purpose,
"authentication"
);
}
#[test]
fn a_community_gets_the_envelope_and_a_peer_the_task_type() {
use vta_sdk::protocols::vetting::{VETTING_REVOKE_STATEMENT_TYPE, vetters};
let community_bound = [
manifest_request(
"did:key:zApplicant",
"did:key:zVtc",
crate::vetting::protocol::JoinProtocol::V0_2,
)
.unwrap(),
vetter_resend_request("did:key:zVetter", "did:key:zVtc").unwrap(),
vetter_list_request(
"did:key:zApplicant",
"did:key:zVtc",
&vetters::list::v0_1::Payload::try_from(vetters::list::v0_1::Payload::builder())
.unwrap(),
)
.unwrap(),
document(
VETTING_REVOKE_STATEMENT_TYPE,
"did:key:zVetter",
"did:key:zVtc",
new_id(),
&json!({}),
)
.unwrap(),
];
for doc in &community_bound {
let message = to_message(doc).unwrap();
assert_eq!(
message.typ,
crate::capabilities::TRUST_TASK_ENVELOPE_TYPE,
"{} must ride the envelope",
doc.type_uri
);
assert_eq!(message.id, doc.id);
assert_eq!(message.body, serde_json::to_value(doc).unwrap());
assert!(is_community_bound(&doc.type_uri.to_string()));
}
for type_uri in [
pcs::ROOT_TYPE,
pcs::TOKENS_TYPE,
pcs::EVENT_MODE_TYPE,
pcs::CHALLENGE_TYPE,
] {
let doc = document(
type_uri,
"did:key:zVetter",
"did:key:zVtc",
new_id(),
&json!({}),
)
.unwrap();
assert_eq!(
to_message(&doc).unwrap().typ,
crate::capabilities::TRUST_TASK_ENVELOPE_TYPE,
"{type_uri} must ride the envelope"
);
}
assert!(!is_community_bound(HIDDEN_ATTESTATION_TYPE));
for type_uri in [VETTING_REQUEST_TYPE, VETTING_DECLINE_TYPE] {
let doc = document(
type_uri,
"did:key:zApplicant",
"did:key:zVetter",
new_id(),
&json!({}),
)
.unwrap();
let message = to_message(&doc).unwrap();
assert_eq!(message.typ, type_uri, "a peer leg stays typed as its task");
}
let request = manifest_request(
"did:key:zApplicant",
"did:key:zVtc",
crate::vetting::protocol::JoinProtocol::V0_2,
)
.unwrap();
let reply = response(&request, &json!({})).unwrap();
assert!(!is_community_bound(&reply.type_uri.to_string()));
}
#[test]
fn responses_and_refusals_thread_on_the_request() {
let request = document(
VETTING_REQUEST_TYPE,
"did:key:zApplicant",
"did:key:zVetter",
new_id(),
&json!({}),
)
.unwrap();
let reply = response(&request, &json!({ "requestId": "r1" })).unwrap();
assert_eq!(reply.thread_id.as_deref(), Some(request.id.as_str()));
assert_eq!(reply.issuer.as_deref(), Some("did:key:zVetter"));
assert!(reply.type_uri.to_string().ends_with("#response"));
let refused = refusal(&request, VETTING_REQUEST_ERR_CAPACITY, None).unwrap();
assert_eq!(refused.thread_id.as_deref(), Some(request.id.as_str()));
assert_eq!(refused.payload["code"], VETTING_REQUEST_ERR_CAPACITY);
assert!(
crate::messaging::is_trust_task_error_type(&refused.type_uri.to_string()),
"the inbound router must recognise it"
);
}
fn a_statement() -> Value {
json!({
"credentialSubject": {
"predicate": vta_sdk::protocols::vetting::VETTED_PREDICATE
}
})
}
#[tokio::test]
async fn a_statement_delivery_is_signed_for_authentication_and_refused_unsigned() {
let (vetter, applicant, other) = (secret(1), secret(2), secret(3));
let statement = a_statement();
let message = credential_delivery(
&did(&vetter),
&did(&applicant),
&statement,
"urn:uuid:session",
&vetter,
)
.await
.unwrap();
assert_eq!(message.typ, CREDENTIAL_ISSUE_TYPE);
assert_eq!(message.thid.as_deref(), Some("urn:uuid:session"));
let body = &message.body;
assert_eq!(body["type"], CREDENTIAL_ISSUE_TYPE);
assert_eq!(body["issuer"], did(&vetter));
assert_eq!(body["recipient"], did(&applicant));
assert!(body["id"].as_str().unwrap().starts_with("urn:uuid:"));
assert!(body.get("issuedAt").is_some());
assert_eq!(
body.pointer("/proof/proofPurpose"),
Some(&json!("authentication"))
);
assert_eq!(delivered_statement(body), Some(&statement));
let resolver = TrustTaskVmResolver::did_key_only();
let opened: Opened<Value> = open(&message, &did(&vetter), &resolver).await.unwrap();
assert_eq!(statement_in(&opened.payload), Some(&statement));
assert!(matches!(
open::<Value>(&message, &did(&other), &resolver).await,
Err(WireError::IssuerNotSender)
));
let mut unsigned = message.clone();
unsigned.body.as_object_mut().unwrap().remove("proof");
assert!(matches!(
open::<Value>(&unsigned, &did(&vetter), &resolver).await,
Err(WireError::Proof(_))
));
let bare = crate::messaging::build_didcomm_message(
CREDENTIAL_ISSUE_TYPE,
json!({ "credential_response": { "credential": statement } }),
&did(&vetter),
&did(&applicant),
Some("urn:uuid:session"),
)
.unwrap();
assert!(delivered_statement(&bare.body).is_some());
assert!(
open::<Value>(&bare, &did(&vetter), &resolver)
.await
.is_err()
);
}
#[test]
fn only_identity_vetting_statements_are_picked_out_of_an_issue() {
let statement = a_statement();
let body = json!({ "credential_response": { "credential": statement } });
assert!(delivered_statement(&body).is_some());
assert!(delivered_statement(&json!({ "payload": body })).is_some());
let vmc = json!({ "credential_response": { "credential": {
"type": ["VerifiableCredential", "MembershipCredential"]
} } });
assert!(delivered_statement(&vmc).is_none());
}
#[test]
fn a_community_issued_vetted_statement_is_not_a_vetter_statement() {
let statement = |issuer: &str, vetter_members: bool| {
let mut value = json!({
"community": "did:example:c",
"method": "video",
"claimsVerified": ["name.legal"],
"livenessConfirmed": true,
});
if vetter_members {
value["identityCommitment"] = json!("zC");
value["cardDigestMultibase"] = json!("zD");
value["declaredRelationship"] = json!("none");
}
json!({ "credential_response": { "credential": {
"issuer": issuer,
"credentialSubject": {
"predicate": vta_sdk::protocols::vetting::VETTED_PREDICATE,
"object": { "value": value }
}
} } })
};
let own = statement("did:example:c", false);
assert!(is_community_issued(
&own["credential_response"]["credential"]
));
assert!(statement_in(&own).is_none());
assert!(statement_in(&statement("did:example:v", true)).is_some());
assert!(statement_in(&statement("did:example:v", false)).is_some());
assert!(statement_in(&statement("did:example:c", true)).is_some());
}
}