use affinidi_data_integrity::{DataIntegrityProof, SignOptions, VerifyOptions};
use affinidi_secrets_resolver::secrets::Secret;
use affinidi_vc::VerifiableCredential;
use vti_common::error::AppError;
pub const ASSERTION_KEY_FRAGMENT: &str = "key-0";
#[derive(Debug, Clone)]
pub struct LocalSigner {
issuer_did: String,
secret: Secret,
}
impl LocalSigner {
pub fn new(issuer_did: String, secret: Secret) -> Self {
Self { issuer_did, secret }
}
pub fn from_ed25519_seed(issuer_did: String, seed: &[u8; 32]) -> Self {
let assertion_id = assertion_method_id(&issuer_did);
let secret = Secret::generate_ed25519(Some(&assertion_id), Some(seed));
Self { issuer_did, secret }
}
pub fn issuer_did(&self) -> &str {
&self.issuer_did
}
pub fn assertion_method_id(&self) -> &str {
&self.secret.id
}
pub fn public_bytes(&self) -> &[u8] {
self.secret.get_public_bytes()
}
pub async fn sign(&self, vc: &mut VerifiableCredential) -> Result<(), AppError> {
let proof = DataIntegrityProof::sign(vc, &self.secret, SignOptions::new())
.await
.map_err(|e| AppError::Internal(format!("sign VC: {e}")))?;
vc.proof = Some(
serde_json::to_value(&proof)
.map_err(|e| AppError::Internal(format!("serialize VC proof: {e}")))?,
);
Ok(())
}
pub fn verify(&self, vc: &VerifiableCredential) -> Result<(), AppError> {
let proof_value = vc
.proof
.as_ref()
.ok_or_else(|| AppError::Validation("VC has no proof to verify".into()))?;
let proof: DataIntegrityProof = serde_json::from_value(proof_value.clone())
.map_err(|e| AppError::Validation(format!("parse VC proof: {e}")))?;
let mut vc_without_proof = vc.clone();
vc_without_proof.proof = None;
proof
.verify_with_public_key(&vc_without_proof, self.public_bytes(), VerifyOptions::new())
.map_err(|e| AppError::Forbidden(format!("verify VC: {e}")))?;
Ok(())
}
}
pub fn assertion_method_id(issuer_did: &str) -> String {
format!("{issuer_did}#{ASSERTION_KEY_FRAGMENT}")
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_DID: &str = "did:webvh:vtc.example.com:abc";
#[test]
fn from_seed_constructs_with_canonical_kid() {
let seed = [0xAB; 32];
let signer = LocalSigner::from_ed25519_seed(TEST_DID.into(), &seed);
assert_eq!(signer.issuer_did(), TEST_DID);
assert_eq!(
signer.assertion_method_id(),
format!("{TEST_DID}#{ASSERTION_KEY_FRAGMENT}")
);
let other = LocalSigner::from_ed25519_seed(TEST_DID.into(), &seed);
assert_eq!(signer.public_bytes(), other.public_bytes());
}
#[test]
fn different_seeds_produce_different_public_keys() {
let a = LocalSigner::from_ed25519_seed(TEST_DID.into(), &[0xAB; 32]);
let b = LocalSigner::from_ed25519_seed(TEST_DID.into(), &[0xCD; 32]);
assert_ne!(a.public_bytes(), b.public_bytes());
}
#[test]
fn assertion_method_id_is_did_hash_fragment() {
assert_eq!(
assertion_method_id("did:key:zX"),
"did:key:zX#key-0".to_string()
);
}
}