use vta_sdk::jws::JwsKey;
pub fn verify_domain_signed(
did: &str,
domain_tag: &[u8],
payload: &[u8],
signature_hex: &str,
) -> Result<(), String> {
let key =
JwsKey::from_did_key(did).map_err(|e| format!("{did} is not a usable did:key: {e}"))?;
verify_hex_signature(&key, domain_tag, payload, signature_hex)
}
pub fn verify_hex_signature(
key: &JwsKey,
domain_tag: &[u8],
payload: &[u8],
signature_hex: &str,
) -> Result<(), String> {
let raw = hex::decode(signature_hex).map_err(|e| format!("signature is not hex: {e}"))?;
let mut signing_bytes = Vec::with_capacity(domain_tag.len() + payload.len());
signing_bytes.extend_from_slice(domain_tag);
signing_bytes.extend_from_slice(payload);
key.verify_signature(&signing_bytes, &raw)
.map_err(|e| format!("holder-binding signature failed ({}): {e}", key.alg()))
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
const TAG: &[u8] = b"vtc-test/v1\0";
fn signer() -> (SigningKey, String) {
let sk = SigningKey::from_bytes(&[0x11; 32]);
let did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&sk.verifying_key().to_bytes());
(sk, did)
}
fn sign_over(sk: &SigningKey, tag: &[u8], payload: &[u8]) -> String {
let mut buf = tag.to_vec();
buf.extend_from_slice(payload);
hex::encode(sk.sign(&buf).to_bytes())
}
#[test]
fn round_trip_verifies() {
let (sk, did) = signer();
let payload = b"{\"a\":1}";
let sig = sign_over(&sk, TAG, payload);
assert!(verify_domain_signed(&did, TAG, payload, &sig).is_ok());
}
#[test]
fn empty_tag_matches_pre_prefixed_payload() {
let (sk, did) = signer();
let mut combined = TAG.to_vec();
combined.extend_from_slice(b"rotation-body");
let sig = sign_over(&sk, &[], &combined);
assert!(verify_domain_signed(&did, &[], &combined, &sig).is_ok());
}
#[test]
fn wrong_domain_tag_rejected() {
let (sk, did) = signer();
let payload = b"body";
let sig = sign_over(&sk, b"vtc-other/v1\0", payload);
assert!(verify_domain_signed(&did, TAG, payload, &sig).is_err());
}
#[test]
fn wrong_signer_rejected() {
let (_sk, did) = signer();
let other = SigningKey::from_bytes(&[0x22; 32]);
let payload = b"body";
let sig = sign_over(&other, TAG, payload);
assert!(verify_domain_signed(&did, TAG, payload, &sig).is_err());
}
#[test]
fn non_did_key_and_garbage_sig_rejected() {
let (_sk, did) = signer();
assert!(verify_domain_signed("did:web:example.com", TAG, b"x", "00").is_err());
assert!(verify_domain_signed(&did, TAG, b"x", "not-hex").is_err());
assert!(verify_domain_signed(&did, TAG, b"x", "0011").is_err());
}
#[test]
fn a_p256_did_key_holder_signs() {
let kp = affinidi_crypto::p256::generate(Some(&[0x31; 32])).unwrap();
let did = JwsKey::from_resolved(&affinidi_data_integrity::ResolvedKey::new(
affinidi_secrets_resolver::secrets::KeyType::P256,
kp.public_bytes.clone(),
))
.unwrap()
.did_key();
assert!(did.starts_with("did:key:zDn"), "{did}");
let mut buf = TAG.to_vec();
buf.extend_from_slice(b"body");
let sig = hex::encode(affinidi_crypto::p256::sign(&kp.private_bytes, &buf).unwrap());
verify_domain_signed(&did, TAG, b"body", &sig).expect("ES256 verifies");
assert!(verify_domain_signed(&did, b"vtc-other/v1\0", b"body", &sig).is_err());
}
#[test]
fn a_secp256k1_did_key_holder_signs() {
use k256::ecdsa::signature::Signer as _;
let seed = [0x41; 32];
let kp = affinidi_crypto::secp256k1::generate(Some(&seed)).unwrap();
let did = JwsKey::from_resolved(&affinidi_data_integrity::ResolvedKey::new(
affinidi_secrets_resolver::secrets::KeyType::Secp256k1,
kp.public_bytes.clone(),
))
.unwrap()
.did_key();
assert!(did.starts_with("did:key:zQ3s"), "{did}");
let sk = k256::ecdsa::SigningKey::from_slice(&seed).unwrap();
let mut buf = TAG.to_vec();
buf.extend_from_slice(b"body");
let sig: k256::ecdsa::Signature = sk.sign(&buf);
let sig = hex::encode(sig.to_bytes());
verify_domain_signed(&did, TAG, b"body", &sig).expect("ES256K verifies");
assert!(verify_domain_signed(&did, TAG, b"other", &sig).is_err());
}
}