use std::collections::HashMap;
use ed25519_dalek::{VerifyingKey, Verifier as DalekVerifier, Signature as DalekSignature};
use crate::attestation::{
pae,
artifact_id_from_pae, digest_from_pae, ArtifactId,
Ed25519Signer, Signer,
Envelope,
};
#[derive(Debug)]
pub struct VerifyResult {
pub artifact_id: ArtifactId,
pub digest: String,
pub verified_key_ids: Vec<String>,
pub payload_type: String,
}
#[derive(Debug)]
pub enum VerifyError {
PayloadDecode(String),
UnknownKey(String),
InvalidSignature(String),
NoValidSignature,
MalformedSignature(String),
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PayloadDecode(e) => write!(f, "payload decode: {}", e),
Self::UnknownKey(id) => write!(f, "unknown key: {}", id),
Self::InvalidSignature(id) => write!(f, "invalid signature for key: {}", id),
Self::NoValidSignature => write!(f, "no valid signature from any trusted key"),
Self::MalformedSignature(e) => write!(f, "malformed signature bytes: {}", e),
}
}
}
impl std::error::Error for VerifyError {}
#[derive(Clone)]
pub struct Verifier {
keys: HashMap<String, VerifyingKey>,
}
impl Verifier {
pub fn new(keys: HashMap<String, VerifyingKey>) -> Self {
Self { keys }
}
pub fn from_signer(signer: &Ed25519Signer) -> Self {
let mut keys = HashMap::new();
keys.insert(signer.key_id().to_string(), signer.verifying_key());
Self { keys }
}
pub fn add_key(&mut self, key_id: impl Into<String>, pub_key: VerifyingKey) {
self.keys.insert(key_id.into(), pub_key);
}
pub fn verify(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
let pae_bytes = self.reconstruct_pae(envelope)?;
let mut verified = Vec::new();
for sig in &envelope.signatures {
let pub_key = self.keys.get(&sig.keyid)
.ok_or_else(|| VerifyError::UnknownKey(sig.keyid.clone()))?;
let raw_sig = self.decode_sig(sig)?;
self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)?;
verified.push(sig.keyid.clone());
}
Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
}
pub fn verify_any(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
let pae_bytes = self.reconstruct_pae(envelope)?;
let mut verified = Vec::new();
for sig in &envelope.signatures {
let pub_key = match self.keys.get(&sig.keyid) {
Some(k) => k,
None => continue, };
let raw_sig = match self.decode_sig(sig) {
Ok(b) => b,
Err(_) => continue, };
if self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid).is_ok() {
verified.push(sig.keyid.clone());
}
}
if verified.is_empty() {
return Err(VerifyError::NoValidSignature);
}
Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
}
fn reconstruct_pae(&self, envelope: &Envelope) -> Result<Vec<u8>, VerifyError> {
let payload_bytes = base64::Engine::decode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&envelope.payload,
).map_err(|e| VerifyError::PayloadDecode(e.to_string()))?;
Ok(pae(&envelope.payload_type, &payload_bytes))
}
fn decode_sig(&self, sig: &crate::attestation::Signature) -> Result<Vec<u8>, VerifyError> {
base64::Engine::decode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&sig.sig,
).map_err(|e| VerifyError::MalformedSignature(e.to_string()))
}
fn verify_sig(
&self,
pub_key: &VerifyingKey,
pae: &[u8],
raw_sig: &[u8],
key_id: &str,
) -> Result<(), VerifyError> {
let sig_bytes: [u8; 64] = raw_sig.try_into()
.map_err(|_| VerifyError::MalformedSignature(
format!("signature for {} is {} bytes, expected 64", key_id, raw_sig.len())
))?;
let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
pub_key.verify(pae, &dalek_sig)
.map_err(|_| VerifyError::InvalidSignature(key_id.to_string()))
}
fn build_result(
&self,
pae_bytes: Vec<u8>,
verified: Vec<String>,
payload_type: &str,
) -> VerifyResult {
VerifyResult {
artifact_id: artifact_id_from_pae(&pae_bytes),
digest: digest_from_pae(&pae_bytes),
verified_key_ids: verified,
payload_type: payload_type.to_string(),
}
}
}
pub fn verify_with_key(
envelope: &Envelope,
key_id: &str,
pub_key: VerifyingKey,
) -> Result<VerifyResult, VerifyError> {
let v = Verifier::from_signer(&Ed25519Signer::from_bytes(
key_id,
pub_key.as_bytes(),
).map_err(|e| VerifyError::InvalidSignature(e.to_string()))?);
v.verify_any(envelope)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attestation::{sign, Ed25519Signer};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct TestStmt { actor: String, action: String }
const PT: &str = "application/vnd.treeship.action.v1+json";
fn stmt() -> TestStmt {
TestStmt { actor: "agent://researcher".into(), action: "tool.call".into() }
}
fn make_signer() -> Ed25519Signer {
Ed25519Signer::generate("key_test_01").unwrap()
}
#[test]
fn verify_roundtrip() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
let result = verifier.verify(&signed.envelope).unwrap();
assert_eq!(result.artifact_id, signed.artifact_id);
assert_eq!(result.digest, signed.digest);
assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
assert_eq!(result.payload_type, PT);
}
#[test]
fn verify_any_roundtrip() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
verifier.verify_any(&signed.envelope).unwrap();
}
#[test]
fn tampered_payload_fails() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
let malicious = TestStmt { actor: "agent://attacker".into(), action: "steal".into() };
let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
let mut tampered = signed.envelope.clone();
tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
let err = verifier.verify(&tampered).unwrap_err();
assert!(
matches!(err, VerifyError::InvalidSignature(_)),
"Expected InvalidSignature, got: {}", err
);
}
#[test]
fn tampered_payload_type_fails() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
let mut tampered = signed.envelope.clone();
tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
assert!(
verifier.verify(&tampered).is_err(),
"verify must fail when payloadType is tampered"
);
}
#[test]
fn wrong_key_fails() {
let signer = make_signer();
let wrong = Ed25519Signer::generate("key_test_01").unwrap();
let verifier = Verifier::from_signer(&wrong);
let signed = sign(PT, &stmt(), &signer).unwrap();
assert!(
verifier.verify(&signed.envelope).is_err(),
"verify with wrong public key must fail"
);
}
#[test]
fn unknown_key_fails() {
let signer = make_signer();
let verifier = Verifier::new(HashMap::new());
let signed = sign(PT, &stmt(), &signer).unwrap();
assert!(
verifier.verify(&signed.envelope).is_err(),
"verify with no trusted keys must fail"
);
}
#[test]
fn verify_any_skips_unknown_keys() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
let result = verifier.verify_any(&signed.envelope).unwrap();
assert_eq!(result.verified_key_ids.len(), 1);
}
#[test]
fn verify_any_all_unknown_fails() {
let signer = make_signer();
let verifier = Verifier::new(HashMap::new());
let signed = sign(PT, &stmt(), &signer).unwrap();
assert!(matches!(
verifier.verify_any(&signed.envelope).unwrap_err(),
VerifyError::NoValidSignature
));
}
#[test]
fn artifact_id_matches_sign() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
let verified = verifier.verify(&signed.envelope).unwrap();
assert_eq!(
signed.artifact_id, verified.artifact_id,
"ID from sign and verify must match"
);
}
#[test]
fn multi_key_verifier() {
let s1 = Ed25519Signer::generate("key_1").unwrap();
let s2 = Ed25519Signer::generate("key_2").unwrap();
let mut verifier = Verifier::from_signer(&s1);
verifier.add_key("key_2", s2.verifying_key());
let signed = sign(PT, &stmt(), &s1).unwrap();
let result = verifier.verify(&signed.envelope).unwrap();
assert_eq!(result.verified_key_ids, vec!["key_1"]);
let signed2 = sign(PT, &stmt(), &s2).unwrap();
let result2 = verifier.verify(&signed2.envelope).unwrap();
assert_eq!(result2.verified_key_ids, vec!["key_2"]);
}
#[test]
fn json_marshal_unmarshal() {
let signer = make_signer();
let verifier = Verifier::from_signer(&signer);
let signed = sign(PT, &stmt(), &signer).unwrap();
let json = signed.envelope.to_json().unwrap();
let restored = Envelope::from_json(&json).unwrap();
let result = verifier.verify(&restored).unwrap();
assert_eq!(result.artifact_id, signed.artifact_id);
}
}