use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::Utc;
use serde::Serialize;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use crate::errors::OpenVTCError;
pub fn build<P: Serialize>(
type_uri: &str,
issuer_did: &str,
recipient_did: &str,
document_id: impl Into<String>,
payload: P,
) -> Result<TrustTask<Value>, OpenVTCError> {
let type_uri = type_uri
.parse()
.map_err(|e| OpenVTCError::Config(format!("trust task type URI parse: {e}")))?;
let payload = serde_json::to_value(payload)
.map_err(|e| OpenVTCError::Config(format!("trust task payload serialize: {e}")))?;
let mut doc = TrustTask::new(document_id.into(), type_uri, payload);
doc.issuer = Some(issuer_did.to_string());
doc.recipient = Some(recipient_did.to_string());
doc.issued_at = Some(Utc::now());
Ok(doc)
}
pub async fn build_signed_value<P: Serialize>(
type_uri: &str,
issuer_did: &str,
recipient_did: &str,
document_id: impl Into<String>,
payload: P,
signer: &Secret,
) -> Result<Value, OpenVTCError> {
let mut doc = build(type_uri, issuer_did, recipient_did, document_id, payload)?;
crate::capabilities::sign_document(&mut doc, signer).await?;
serde_json::to_value(&doc)
.map_err(|e| OpenVTCError::Config(format!("trust task document serialize: {e}")))
}
pub fn build_value<P: Serialize>(
type_uri: &str,
issuer_did: &str,
recipient_did: &str,
document_id: impl Into<String>,
payload: P,
) -> Result<Value, OpenVTCError> {
let doc = build(type_uri, issuer_did, recipient_did, document_id, payload)?;
serde_json::to_value(&doc)
.map_err(|e| OpenVTCError::Config(format!("trust task document serialize: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const TYPE_URI: &str = "https://trusttasks.org/spec/vtc/members/self-remove/0.1";
#[tokio::test]
async fn a_signed_document_carries_a_proof_by_its_issuer() {
use affinidi_tdk::dids::{DID, KeyType};
let (issuer_did, signer) =
DID::generate_did_key(KeyType::Ed25519).expect("did:key generates");
let doc = build_signed_value(
TYPE_URI,
&issuer_did,
"did:webvh:community",
"urn:uuid:1",
json!({}),
&signer,
)
.await
.expect("builds and signs");
let proof = doc.get("proof").expect("a proof is attached");
assert_eq!(
proof.get("cryptosuite").and_then(Value::as_str),
Some("eddsa-jcs-2022"),
);
let vm = proof
.get("verificationMethod")
.and_then(Value::as_str)
.expect("the proof names a verification method");
assert!(
vm.starts_with(&issuer_did),
"the proof must be by the issuer's own key: {vm} is not under {issuer_did}"
);
assert_eq!(
doc.get("issuer").and_then(Value::as_str),
Some(issuer_did.as_str())
);
assert_eq!(
doc.get("recipient").and_then(Value::as_str),
Some("did:webvh:community")
);
assert!(doc.get("issuedAt").is_some());
}
#[tokio::test]
async fn a_request_verifies_as_the_senders_authentication_proof() {
use crate::proof_check::{Purpose, verify_proofs};
use affinidi_tdk::dids::{DID, KeyType};
let (issuer_did, signer) =
DID::generate_did_key(KeyType::Ed25519).expect("did:key generates");
let resolver = affinidi_did_resolver_cache_sdk::DIDCacheClient::new(
affinidi_did_resolver_cache_sdk::config::DIDCacheConfigBuilder::default().build(),
)
.await
.unwrap();
let issuer_doc = resolver.resolve(&issuer_did).await.unwrap().doc;
let a = build_signed_value(
TYPE_URI,
&issuer_did,
"did:webvh:community",
"urn:uuid:a",
json!({}),
&signer,
)
.await
.unwrap();
assert_eq!(a["proof"]["proofPurpose"], json!("authentication"));
assert_eq!(
verify_proofs(&a, &issuer_did, &issuer_doc, &[Purpose::Authentication]),
Ok(())
);
assert!(verify_proofs(&a, &issuer_did, &issuer_doc, &[Purpose::AssertionMethod]).is_err());
let mut readdressed = a.clone();
readdressed["recipient"] = json!("did:webvh:elsewhere");
assert!(
verify_proofs(
&readdressed,
&issuer_did,
&issuer_doc,
&[Purpose::Authentication]
)
.is_err()
);
}
#[test]
fn a_built_document_is_addressed_and_dated() {
let doc = build(
TYPE_URI,
"did:key:zMember",
"did:webvh:community",
"urn:uuid:1",
json!({}),
)
.expect("builds");
assert_eq!(doc.id, "urn:uuid:1");
assert_eq!(doc.type_uri.to_string(), TYPE_URI);
assert_eq!(
doc.issuer.as_deref(),
Some("did:key:zMember"),
"without `issuer` a peer cannot bind the document to its sender"
);
assert_eq!(
doc.recipient.as_deref(),
Some("did:webvh:community"),
"without `recipient` the document is refused under §7.2 item 5b"
);
assert!(
doc.issued_at.is_some(),
"without `issuedAt` the document is refused under §7.3 item 17"
);
}
#[test]
fn the_two_builders_agree() {
let doc = build(
TYPE_URI,
"did:key:zM",
"did:webvh:c",
"urn:uuid:2",
json!({"a": 1}),
)
.expect("builds");
let value = build_value(
TYPE_URI,
"did:key:zM",
"did:webvh:c",
"urn:uuid:2",
json!({"a": 1}),
)
.expect("builds");
let mut from_doc = serde_json::to_value(&doc).expect("serialises");
let mut from_value = value;
for v in [&mut from_doc, &mut from_value] {
v.as_object_mut().expect("an object").remove("issuedAt");
}
assert_eq!(from_doc, from_value);
}
#[test]
fn a_malformed_type_uri_is_refused_at_build_time() {
let err = build(
"not a uri",
"did:key:zM",
"did:webvh:c",
"urn:uuid:3",
json!({}),
)
.expect_err("a malformed type URI must not produce a document");
assert!(
err.to_string().contains("type URI"),
"the error should name what was wrong: {err}"
);
}
}