use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
use affinidi_secrets_resolver::secrets::Secret;
use chrono::Utc;
use serde_json::Value;
use trust_tasks_rs::{Proof, TrustTask};
use crate::did_key::decode_private_key_multibase;
#[derive(Debug)]
pub enum TrustTaskSignError {
NotDidKey(String),
BadPrivateKey(String),
TypeUri(String),
Sign(String),
}
impl std::fmt::Display for TrustTaskSignError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotDidKey(did) => write!(
f,
"signing a Trust Task requires a did:key holder; got {did}"
),
Self::BadPrivateKey(e) => write!(f, "decode holder private key: {e}"),
Self::TypeUri(e) => write!(f, "Trust Task type URI parse: {e}"),
Self::Sign(e) => write!(f, "sign Trust Task: {e}"),
}
}
}
impl std::error::Error for TrustTaskSignError {}
pub fn did_key_to_vm(did: &str) -> Option<String> {
let mb = did.strip_prefix("did:key:")?;
Some(format!("{did}#{mb}"))
}
pub fn build_unsigned(
type_uri: &str,
payload: Value,
issuer: &str,
recipient: &str,
) -> Result<TrustTask<Value>, TrustTaskSignError> {
let mut doc: TrustTask<Value> = TrustTask::new(
format!("urn:uuid:{}", uuid::Uuid::new_v4()),
type_uri
.parse()
.map_err(|e| TrustTaskSignError::TypeUri(format!("{e}")))?,
payload,
);
doc.issuer = Some(issuer.to_string());
doc.recipient = Some(recipient.to_string());
doc.issued_at = Some(Utc::now());
Ok(doc)
}
pub async fn sign_in_place(
doc: &mut TrustTask<Value>,
holder_did: &str,
private_key_multibase: &str,
) -> Result<(), TrustTaskSignError> {
let vm_id = did_key_to_vm(holder_did)
.ok_or_else(|| TrustTaskSignError::NotDidKey(holder_did.to_string()))?;
let seed = decode_private_key_multibase(private_key_multibase)
.map_err(|e| TrustTaskSignError::BadPrivateKey(e.to_string()))?;
let mut signer = Secret::generate_ed25519(Some(&vm_id), Some(&seed));
signer.id = vm_id;
let mut signing_doc =
serde_json::to_value(&*doc).map_err(|e| TrustTaskSignError::Sign(e.to_string()))?;
if let Some(obj) = signing_doc.as_object_mut() {
obj.remove("proof");
}
let di_proof = DataIntegrityProof::sign(
&signing_doc,
&signer,
SignOptions::new()
.with_proof_purpose("assertionMethod")
.with_created(Utc::now()),
)
.await
.map_err(|e| TrustTaskSignError::Sign(e.to_string()))?;
let proof_json =
serde_json::to_value(&di_proof).map_err(|e| TrustTaskSignError::Sign(e.to_string()))?;
doc.proof = Some(
serde_json::from_value::<Proof>(proof_json)
.map_err(|e| TrustTaskSignError::Sign(e.to_string()))?,
);
Ok(())
}
pub async fn build_signed(
type_uri: &str,
payload: Value,
holder_did: &str,
private_key_multibase: &str,
recipient: &str,
) -> Result<String, TrustTaskSignError> {
let mut doc = build_unsigned(type_uri, payload, holder_did, recipient)?;
sign_in_place(&mut doc, holder_did, private_key_multibase).await?;
serde_json::to_string(&doc).map_err(|e| TrustTaskSignError::Sign(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn did_key_from_seed(seed_byte: u8) -> (String, String) {
let seed = [seed_byte; 32];
let sk = ed25519_dalek::SigningKey::from_bytes(&seed);
let did = format!(
"did:key:{}",
crate::did_key::ed25519_multibase_pubkey(&sk.verifying_key().to_bytes())
);
let mut buf = vec![0x80, 0x26];
buf.extend_from_slice(&seed);
(did, multibase::encode(multibase::Base::Base58Btc, &buf))
}
const TYPE: &str = "https://trusttasks.org/spec/vtc/join-requests/submit/0.1";
#[tokio::test]
async fn signed_document_verifies_server_side() {
use affinidi_data_integrity::{DidKeyResolver, VerifyOptions};
let (did, pk) = did_key_from_seed(0xa1);
let body = build_signed(TYPE, json!({"vp": {}}), &did, &pk, "did:key:z6MkVtc")
.await
.expect("sign");
let doc: TrustTask<Value> = serde_json::from_str(&body).unwrap();
assert_eq!(doc.issuer.as_deref(), Some(did.as_str()));
assert_eq!(doc.recipient.as_deref(), Some("did:key:z6MkVtc"));
let proof: DataIntegrityProof =
serde_json::from_value(serde_json::to_value(doc.proof.as_ref().unwrap()).unwrap())
.unwrap();
let mut unsigned = doc.clone();
unsigned.proof = None;
proof
.verify(&unsigned, &DidKeyResolver, VerifyOptions::new())
.await
.expect("must verify under the server's resolver");
}
#[tokio::test]
async fn tampered_payload_fails() {
use affinidi_data_integrity::{DidKeyResolver, VerifyOptions};
let (did, pk) = did_key_from_seed(0xa2);
let body = build_signed(TYPE, json!({"vp": {}}), &did, &pk, "did:key:z6MkVtc")
.await
.unwrap();
let mut doc: TrustTask<Value> = serde_json::from_str(&body).unwrap();
doc.payload = json!({"vp": {"forged": true}});
let proof: DataIntegrityProof =
serde_json::from_value(serde_json::to_value(doc.proof.as_ref().unwrap()).unwrap())
.unwrap();
let mut unsigned = doc.clone();
unsigned.proof = None;
assert!(
proof
.verify(&unsigned, &DidKeyResolver, VerifyOptions::new())
.await
.is_err()
);
}
#[tokio::test]
async fn non_did_key_holder_refused() {
let (_, pk) = did_key_from_seed(0xa3);
let err = build_signed(
TYPE,
json!({}),
"did:web:example.com",
&pk,
"did:key:z6MkVtc",
)
.await
.expect_err("did:web must be refused");
assert!(matches!(err, TrustTaskSignError::NotDidKey(_)), "{err:?}");
}
#[test]
fn unsigned_sets_issuer_and_recipient() {
let doc = build_unsigned(TYPE, json!({}), "did:key:z6MkA", "did:key:z6MkB").unwrap();
assert_eq!(doc.issuer.as_deref(), Some("did:key:z6MkA"));
assert_eq!(doc.recipient.as_deref(), Some("did:key:z6MkB"));
assert!(doc.proof.is_none());
}
}