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),
NotAVerificationMethod(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,
"HolderKey::from_did_key needs a did:key, which carries its own verification \
method; got {did} — use HolderKey::new and name the key"
),
Self::NotAVerificationMethod(vm) => write!(
f,
"`{vm}` does not identify a key: a verification method is a DID with a \
`#fragment`, e.g. did:webvh:<scid>:example.com:glenn#key-0"
),
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)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HolderKey {
verification_method: String,
private_key_multibase: String,
}
impl HolderKey {
pub fn new(
verification_method: impl Into<String>,
private_key_multibase: impl Into<String>,
) -> Result<Self, TrustTaskSignError> {
let verification_method = verification_method.into();
let (did, fragment) = verification_method.split_once('#').ok_or_else(|| {
TrustTaskSignError::NotAVerificationMethod(verification_method.clone())
})?;
if !did.starts_with("did:") || fragment.is_empty() {
return Err(TrustTaskSignError::NotAVerificationMethod(
verification_method.clone(),
));
}
Ok(Self {
verification_method,
private_key_multibase: private_key_multibase.into(),
})
}
pub fn from_did_key(
did: &str,
private_key_multibase: impl Into<String>,
) -> Result<Self, TrustTaskSignError> {
let vm =
did_key_to_vm(did).ok_or_else(|| TrustTaskSignError::NotDidKey(did.to_string()))?;
Ok(Self {
verification_method: vm,
private_key_multibase: private_key_multibase.into(),
})
}
#[must_use]
pub fn verification_method(&self) -> &str {
&self.verification_method
}
#[must_use]
pub fn holder_did(&self) -> &str {
self.verification_method
.split('#')
.next()
.unwrap_or(&self.verification_method)
}
}
pub async fn sign_in_place(
doc: &mut TrustTask<Value>,
holder_did: &str,
private_key_multibase: &str,
) -> Result<(), TrustTaskSignError> {
let key = HolderKey::from_did_key(holder_did, private_key_multibase)?;
sign_in_place_with(doc, &key).await
}
pub async fn sign_in_place_with(
doc: &mut TrustTask<Value>,
key: &HolderKey,
) -> Result<(), TrustTaskSignError> {
let vm_id = key.verification_method.clone();
let seed = decode_private_key_multibase(&key.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 key = HolderKey::from_did_key(holder_did, private_key_multibase)?;
build_signed_with(type_uri, payload, &key, recipient).await
}
pub async fn build_signed_with(
type_uri: &str,
payload: Value,
key: &HolderKey,
recipient: &str,
) -> Result<String, TrustTaskSignError> {
let mut doc = build_unsigned(type_uri, payload, key.holder_did(), recipient)?;
sign_in_place_with(&mut doc, key).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.2";
#[tokio::test]
async fn a_did_webvh_holder_signs_and_names_its_key() {
const VM: &str = "did:webvh:QmScidExample:example.com:glenn#key-0";
let (_did, pk) = did_key_from_seed(0xb2);
let key = HolderKey::new(VM, &pk).expect("a DID with a fragment is a verification method");
assert_eq!(
key.holder_did(),
"did:webvh:QmScidExample:example.com:glenn"
);
let body = build_signed_with(TYPE, json!({"vp": {}}), &key, "did:key:z6MkVtc")
.await
.expect("a did:webvh holder can sign");
let doc: TrustTask<Value> = serde_json::from_str(&body).unwrap();
assert_eq!(doc.issuer.as_deref(), Some(key.holder_did()));
let proof = doc.proof.as_ref().expect("signed");
let di: DataIntegrityProof =
serde_json::from_value(serde_json::to_value(proof).unwrap()).unwrap();
assert_eq!(
di.verification_method, VM,
"the proof must name the key the DID document publishes, not a derived one"
);
}
#[test]
fn a_verification_method_must_name_a_key() {
for bad in [
"did:webvh:QmScid:example.com:glenn",
"did:webvh:QmScid:example.com:glenn#",
"https://example.com/keys#key-0",
"no-scheme#key-0",
] {
assert!(
matches!(
HolderKey::new(bad, "z1234"),
Err(TrustTaskSignError::NotAVerificationMethod(_))
),
"`{bad}` must be refused"
);
}
}
#[test]
fn a_did_key_holder_still_derives_its_own_verification_method() {
let (did, pk) = did_key_from_seed(0xb3);
let key = HolderKey::from_did_key(&did, &pk).expect("did:key derives");
assert_eq!(key.verification_method(), format!("{did}#{}", &did[8..]));
assert_eq!(key.holder_did(), did);
assert!(matches!(
HolderKey::from_did_key("did:webvh:QmScid:example.com:glenn", &pk),
Err(TrustTaskSignError::NotDidKey(_))
));
}
#[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 the_did_key_helper_refuses_a_did_it_cannot_derive_a_key_from() {
let (_, pk) = did_key_from_seed(0xa3);
let err = build_signed(
TYPE,
json!({}),
"did:web:example.com",
&pk,
"did:key:z6MkVtc",
)
.await
.expect_err("the derive-it form cannot serve did:web");
assert!(matches!(err, TrustTaskSignError::NotDidKey(_)), "{err:?}");
let key = HolderKey::new("did:web:example.com#key-1", &pk).expect("names a key");
build_signed_with(TYPE, json!({}), &key, "did:key:z6MkVtc")
.await
.expect("naming the key is all it took");
}
#[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());
}
}