use crate::credential::VerifiableCredential;
use crate::did::{did_to_peer_id, extract_ed25519_bytes};
use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
#[derive(Debug)]
pub struct VerificationResult {
pub structure_valid: bool,
pub signature_valid: Option<bool>,
pub message: String,
}
impl VerificationResult {
pub fn is_valid(&self) -> bool {
self.structure_valid && self.signature_valid == Some(true)
}
}
pub fn verify(vc: &VerifiableCredential) -> VerificationResult {
if vc.credential_type.is_empty() {
return invalid_structure("Missing credential type");
}
if vc.issuer.is_empty() {
return invalid_structure("Missing issuer DID");
}
if vc.subject.id.is_empty() {
return invalid_structure("Missing subject DID");
}
if vc.is_expired() {
return invalid_structure("Credential has expired");
}
let Some(proof) = &vc.proof else {
return VerificationResult {
structure_valid: true,
signature_valid: None,
message: "Credential has no proof (unverified)".to_string(),
};
};
if proof.proof_type != "Ed25519Signature2020" {
return VerificationResult {
structure_valid: true,
signature_valid: None,
message: format!("Unsupported proof type: {}", proof.proof_type),
};
}
match verify_ed25519_proof(vc) {
Ok(true) => VerificationResult {
structure_valid: true,
signature_valid: Some(true),
message: "Signature verified ✓".to_string(),
},
Ok(false) => VerificationResult {
structure_valid: true,
signature_valid: Some(false),
message: "Signature verification failed".to_string(),
},
Err(e) => VerificationResult {
structure_valid: true,
signature_valid: Some(false),
message: format!("Verification error: {}", e),
},
}
}
fn invalid_structure(msg: &str) -> VerificationResult {
VerificationResult {
structure_valid: false,
signature_valid: None,
message: msg.to_string(),
}
}
fn verify_ed25519_proof(vc: &VerifiableCredential) -> Result<bool> {
let proof = vc.proof.as_ref().context("no proof")?;
let sig_bytes = URL_SAFE_NO_PAD
.decode(&proof.proof_value)
.context("decoding proofValue base64url")?;
if sig_bytes.len() != 64 {
bail!(
"Expected 64-byte Ed25519 signature, got {}",
sig_bytes.len()
);
}
let sig_array: [u8; 64] = sig_bytes.try_into().unwrap();
let signature = Signature::from_bytes(&sig_array);
let vm = &proof.verification_method;
let did = vm.split('#').next().context("invalid verificationMethod")?;
let peer_id =
did_to_peer_id(did).with_context(|| format!("could not parse DID as PeerId: {did}"))?;
let key_bytes = extract_ed25519_bytes(&peer_id)
.with_context(|| format!("could not extract Ed25519 key from PeerId: {peer_id}"))?;
let verifying_key =
VerifyingKey::from_bytes(&key_bytes).context("invalid Ed25519 public key")?;
let payload = vc.to_signing_bytes()?;
Ok(verifying_key.verify(&payload, &signature).is_ok())
}
pub fn sign_credential_bytes(
vc: &mut VerifiableCredential,
secret_key_bytes: &[u8; 32],
issuer_peer_id: &libp2p::PeerId,
) -> Result<()> {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(secret_key_bytes);
let payload = vc.to_signing_bytes()?;
let signature = signing_key.sign(&payload);
let proof_value = URL_SAFE_NO_PAD.encode(signature.to_bytes());
vc.proof = Some(crate::credential::CredentialProof {
proof_type: "Ed25519Signature2020".to_string(),
created: chrono::Utc::now(),
verification_method: crate::did::verification_method(issuer_peer_id),
proof_purpose: "assertionMethod".to_string(),
proof_value,
});
Ok(())
}