use crate::error::AcdpError;
use crate::types::{
body::{Body, Signature},
primitives::ContentHash,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::{Verifier as _, VerifyingKey};
#[cfg(feature = "client")]
use {
super::hash::verify_content_hash, crate::did::web::WebResolver,
crate::types::primitives::AgentDid, crate::types::publish::PublishRequest,
};
#[cfg(feature = "client")]
pub struct Verifier<'a> {
resolver: &'a WebResolver,
}
#[cfg(feature = "client")]
impl<'a> Verifier<'a> {
pub fn new(resolver: &'a WebResolver) -> Self {
Self { resolver }
}
pub async fn verify_body(&self, body: &Body) -> Result<(), AcdpError> {
crate::validation::validate_body(body)?;
self.verify_body_signed(body).await
}
pub async fn verify_body_signed(&self, body: &Body) -> Result<(), AcdpError> {
self.verify_body_hash(body)?;
self.verify_body_signature(body).await
}
pub fn verify_body_hash(&self, body: &Body) -> Result<(), AcdpError> {
let body_val = serde_json::to_value(body)?;
verify_content_hash(&body_val, &body.content_hash)
}
pub async fn verify_body_signature(&self, body: &Body) -> Result<(), AcdpError> {
verify_signature_envelope(
&body.agent_id,
&body.signature,
&body.content_hash,
self.resolver,
)
.await
}
}
#[cfg(feature = "client")]
pub async fn verify_publish_request_signature(
req: &PublishRequest,
resolver: &WebResolver,
) -> Result<(), AcdpError> {
verify_signature_envelope(&req.agent_id, &req.signature, &req.content_hash, resolver).await
}
#[cfg(feature = "client")]
async fn verify_signature_envelope(
agent_id: &AgentDid,
signature: &Signature,
content_hash: &ContentHash,
resolver: &WebResolver,
) -> Result<(), AcdpError> {
let key_id = &signature.key_id;
let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
})?;
if fragment.is_empty() {
return Err(AcdpError::KeyResolution(format!(
"signature.key_id '{key_id}' has an empty '#fragment'"
)));
}
if did_part != agent_id.as_str() {
return Err(AcdpError::KeyNotAuthorized(format!(
"key_id DID '{did_part}' ≠ agent_id '{agent_id}'"
)));
}
if did_part.starts_with("did:key:") {
return verify_did_key_envelope(signature, content_hash);
}
if !did_part.starts_with("did:web:") {
return Err(AcdpError::KeyNotAuthorized(format!(
"signatures require a did:web or did:key key_id; got '{did_part}'"
)));
}
let doc = resolver.resolve(did_part).await?;
let method = doc.find_by_fragment(fragment).ok_or_else(|| {
AcdpError::KeyResolution(format!(
"no verification method with fragment '#{fragment}'"
))
})?;
if !doc.is_assertion_method(&method.id) {
return Err(AcdpError::KeyNotAuthorized(format!(
"'{}' is not in assertionMethod",
method.id
)));
}
if let Some(declared) = method.declared_algorithm() {
if declared != signature.algorithm {
return Err(AcdpError::InvalidSignature(format!(
"signature.algorithm '{}' does not match verification method type \
(resolved key declares '{declared}')",
signature.algorithm
)));
}
}
match signature.algorithm.as_str() {
"ed25519" => {
let pub_bytes = method.ed25519_public_key_bytes()?;
verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
}
"ecdsa-p256" => {
let pub_sec1 = method.ecdsa_p256_public_key_sec1()?;
verify_ecdsa_p256(&pub_sec1, &signature.value, content_hash.as_str())
}
other => Err(AcdpError::UnsupportedAlgorithm(format!(
"verifier does not support signature algorithm '{other}'"
))),
}
}
pub fn verify_did_key_envelope(
signature: &Signature,
content_hash: &ContentHash,
) -> Result<(), AcdpError> {
let material = crate::did::key::resolve_did_key_url(&signature.key_id)?;
if material.algorithm() != signature.algorithm {
return Err(AcdpError::InvalidSignature(format!(
"signature.algorithm '{}' does not match the did:key multicodec \
(key implies '{}')",
signature.algorithm,
material.algorithm()
)));
}
match material {
crate::did::key::DidKeyMaterial::Ed25519(pub_bytes) => {
verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
}
crate::did::key::DidKeyMaterial::EcdsaP256(sec1_compressed) => {
verify_ecdsa_p256(&sec1_compressed, &signature.value, content_hash.as_str())
}
}
}
pub fn verify_body_offline(body: &Body) -> Result<(), AcdpError> {
crate::validation::validate_body(body)?;
if !body.agent_id.as_str().starts_with("did:key:") {
return Err(AcdpError::KeyResolution(format!(
"verify_body_offline supports did:key producers only; '{}' requires \
the resolver-backed Verifier (client feature)",
body.agent_id
)));
}
let body_val = serde_json::to_value(body)?;
super::hash::verify_content_hash(&body_val, &body.content_hash)?;
let did_part = body
.signature
.key_id
.split_once('#')
.map(|(d, _)| d)
.unwrap_or(body.signature.key_id.as_str());
if did_part != body.agent_id.as_str() {
return Err(AcdpError::KeyNotAuthorized(format!(
"key_id DID '{did_part}' ≠ agent_id '{}'",
body.agent_id
)));
}
verify_did_key_envelope(&body.signature, &body.content_hash)
}
pub fn verify_publish_request_signature_offline(
req: &crate::types::publish::PublishRequest,
) -> Result<(), AcdpError> {
let key_id = req.signature.key_id.as_str();
let did_part = key_id.split_once('#').map(|(d, _)| d).unwrap_or(key_id);
if did_part != req.agent_id.as_str() {
return Err(AcdpError::KeyNotAuthorized(format!(
"key_id DID '{did_part}' ≠ agent_id '{}'",
req.agent_id
)));
}
if !did_part.starts_with("did:key:") {
return Err(AcdpError::KeyResolution(format!(
"offline verification supports did:key only; got '{did_part}'"
)));
}
verify_did_key_envelope(&req.signature, &req.content_hash)
}
#[cfg(feature = "client")]
pub async fn verify_body_signature_historical(
body: &Body,
resolver: &WebResolver,
) -> Result<(), AcdpError> {
let key_id = &body.signature.key_id;
let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
})?;
if did_part != body.agent_id.as_str() {
return Err(AcdpError::KeyNotAuthorized(format!(
"key_id DID '{did_part}' ≠ agent_id '{}'",
body.agent_id
)));
}
if !did_part.starts_with("did:web:") {
return Err(AcdpError::KeyResolution(format!(
"historical-key verification applies to did:web only; got '{did_part}'"
)));
}
let doc = resolver.resolve(did_part).await?;
let method = doc.find_by_fragment(fragment).ok_or_else(|| {
AcdpError::KeyResolution(format!(
"no verification method with fragment '#{fragment}' — the key was \
removed from the DID document, not just rotated out of assertionMethod"
))
})?;
if let Some(declared) = method.declared_algorithm() {
if declared != body.signature.algorithm {
return Err(AcdpError::InvalidSignature(format!(
"signature.algorithm '{}' does not match verification method type \
(resolved key declares '{declared}')",
body.signature.algorithm
)));
}
}
match body.signature.algorithm.as_str() {
"ed25519" => verify_ed25519(
&method.ed25519_public_key_bytes()?,
&body.signature.value,
body.content_hash.as_str(),
),
"ecdsa-p256" => verify_ecdsa_p256(
&method.ecdsa_p256_public_key_sec1()?,
&body.signature.value,
body.content_hash.as_str(),
),
other => Err(AcdpError::UnsupportedAlgorithm(format!(
"verifier does not support signature algorithm '{other}'"
))),
}
}
pub fn verify_ed25519(
pub_key_bytes: &[u8; 32],
sig_b64: &str,
message: &str,
) -> Result<(), AcdpError> {
let key = VerifyingKey::from_bytes(pub_key_bytes)
.map_err(|e| AcdpError::InvalidSignature(e.to_string()))?;
let sig_bytes = STANDARD
.decode(sig_b64)
.map_err(|e| AcdpError::InvalidSignature(format!("base64: {e}")))?;
let sig = ed25519_dalek::Signature::from_slice(&sig_bytes)
.map_err(|e| AcdpError::InvalidSignature(format!("sig parse: {e}")))?;
key.verify(message.as_bytes(), &sig)
.map_err(|_| AcdpError::InvalidSignature("signature verification failed".into()))
}
pub fn verify_ecdsa_p256(
pub_key_sec1: &[u8],
sig_b64: &str,
message: &str,
) -> Result<(), AcdpError> {
use p256::ecdsa::{signature::Verifier as _, Signature, VerifyingKey as P256VerifyingKey};
let key = P256VerifyingKey::from_sec1_bytes(pub_key_sec1)
.map_err(|e| AcdpError::InvalidSignature(format!("ecdsa-p256 key parse: {e}")))?;
let sig_bytes = STANDARD
.decode(sig_b64)
.map_err(|e| AcdpError::InvalidSignature(format!("base64: {e}")))?;
if sig_bytes.len() != 64 {
return Err(AcdpError::InvalidSignature(format!(
"ecdsa-p256 signature MUST be 64 bytes (IEEE 1363 r‖s), got {}",
sig_bytes.len()
)));
}
let sig = Signature::from_slice(&sig_bytes)
.map_err(|e| AcdpError::InvalidSignature(format!("ecdsa-p256 sig parse: {e}")))?;
key.verify(message.as_bytes(), &sig)
.map_err(|_| AcdpError::InvalidSignature("ecdsa-p256 signature verification failed".into()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::sign::SigningKey;
use crate::types::primitives::ContentHash;
const TEST_SEED: [u8; 32] = [0u8; 32];
const TEST_PUB_HEX: &str = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
#[test]
fn sign_and_verify_golden() {
let key = SigningKey::from_bytes(&TEST_SEED);
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
let sig_b64 = key.sign_content_hash(&hash);
assert_eq!(
sig_b64,
"ErkbV+FUdn49TgF3zJ3RBe3AmyGxLVAQdMjlhabUfM96qendmWwdVodX/SV3O3aKLypbUu6gmb5Npt3O/w7nDQ=="
);
let pub_bytes: [u8; 32] = hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap();
verify_ed25519(&pub_bytes, &sig_b64, hash.as_str()).unwrap();
}
#[test]
fn wrong_message_fails() {
let key = SigningKey::from_bytes(&TEST_SEED);
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
let sig_b64 = key.sign_content_hash(&hash);
let pub_bytes: [u8; 32] = hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap();
let result = verify_ed25519(&pub_bytes, &sig_b64, "sha256:wronghash");
assert!(result.is_err());
}
#[test]
fn declared_algorithm_mismatch_rejected() {
use crate::did::document::VerificationMethod;
let raw: [u8; 32] = hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap();
let mut prefixed = vec![0xed, 0x01];
prefixed.extend_from_slice(&raw);
let mb = format!("z{}", bs58::encode(&prefixed).into_string());
let vm = VerificationMethod {
id: "did:web:example.com#key-1".into(),
method_type: "Ed25519VerificationKey2020".into(),
controller: "did:web:example.com".into(),
public_key_jwk: None,
public_key_multibase: Some(mb),
};
assert_eq!(vm.declared_algorithm(), Some("ed25519"));
}
#[test]
fn verify_ed25519_rejects_malformed_base64() {
let pub_bytes: [u8; 32] = hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap();
let err = verify_ed25519(&pub_bytes, "not valid base64!!", "sha256:x").unwrap_err();
assert!(
matches!(err, AcdpError::InvalidSignature(ref m) if m.contains("base64")),
"got {err:?}"
);
}
#[test]
fn verify_ed25519_rejects_wrong_length_signature() {
let pub_bytes: [u8; 32] = hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap();
let short = STANDARD.encode([1u8, 2, 3]);
let err = verify_ed25519(&pub_bytes, &short, "sha256:x").unwrap_err();
assert!(matches!(err, AcdpError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn verify_ecdsa_p256_rejects_bad_public_key() {
let err = verify_ecdsa_p256(&[1, 2, 3, 4, 5], "AAAA", "sha256:x").unwrap_err();
assert!(
matches!(err, AcdpError::InvalidSignature(ref m) if m.contains("key parse")),
"got {err:?}"
);
}
#[test]
fn verify_ecdsa_p256_rejects_malformed_base64() {
let key = crate::crypto::sign::P256SigningKey::generate();
let err =
verify_ecdsa_p256(&key.verifying_key_sec1(), "not base64!!", "sha256:x").unwrap_err();
assert!(
matches!(err, AcdpError::InvalidSignature(ref m) if m.contains("base64")),
"got {err:?}"
);
}
#[test]
fn verify_ecdsa_p256_rejects_wrong_length_signature() {
let key = crate::crypto::sign::P256SigningKey::generate();
let short = STANDARD.encode([0u8; 10]);
let err = verify_ecdsa_p256(&key.verifying_key_sec1(), &short, "sha256:x").unwrap_err();
assert!(
matches!(err, AcdpError::InvalidSignature(ref m) if m.contains("64 bytes")),
"got {err:?}"
);
}
}