1use ed25519_dalek::{Signature as DalekSignature, Verifier as DalekVerifier, VerifyingKey};
2use std::collections::HashMap;
3
4use crate::attestation::{
5 artifact_id_from_pae, digest_from_pae, pae, ArtifactId, Ed25519Signer, Envelope, Signer,
6};
7
8#[derive(Debug)]
10pub struct VerifyResult {
11 pub artifact_id: ArtifactId,
15
16 pub digest: String,
18
19 pub verified_key_ids: Vec<String>,
21
22 pub payload_type: String,
24}
25
26#[derive(Debug)]
28pub enum VerifyError {
29 PayloadDecode(String),
31 UnknownKey(String),
33 InvalidSignature(String),
35 NoValidSignature,
37 MalformedSignature(String),
39}
40
41impl std::fmt::Display for VerifyError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::PayloadDecode(e) => write!(f, "payload decode: {}", e),
45 Self::UnknownKey(id) => write!(f, "unknown key: {}", id),
46 Self::InvalidSignature(id) => write!(f, "invalid signature for key: {}", id),
47 Self::NoValidSignature => write!(f, "no valid signature from any trusted key"),
48 Self::MalformedSignature(e) => write!(f, "malformed signature bytes: {}", e),
49 }
50 }
51}
52
53impl std::error::Error for VerifyError {}
54
55#[derive(Clone)]
60pub struct Verifier {
61 keys: HashMap<String, VerifyingKey>,
63}
64
65impl Verifier {
66 pub fn new(keys: HashMap<String, VerifyingKey>) -> Self {
68 Self { keys }
69 }
70
71 pub fn from_signer(signer: &Ed25519Signer) -> Self {
74 let mut keys = HashMap::new();
75 keys.insert(signer.key_id().to_string(), signer.verifying_key());
76 Self { keys }
77 }
78
79 pub fn add_key(&mut self, key_id: impl Into<String>, pub_key: VerifyingKey) {
81 self.keys.insert(key_id.into(), pub_key);
82 }
83
84 pub fn public_key(&self, key_id: &str) -> Option<&VerifyingKey> {
91 self.keys.get(key_id)
92 }
93
94 pub fn verify(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
103 if envelope.signatures.is_empty() {
108 return Err(VerifyError::NoValidSignature);
109 }
110
111 let pae_bytes = self.reconstruct_pae(envelope)?;
112 let mut verified = Vec::new();
113
114 for sig in &envelope.signatures {
115 let pub_key = self
116 .keys
117 .get(&sig.keyid)
118 .ok_or_else(|| VerifyError::UnknownKey(sig.keyid.clone()))?;
119
120 let raw_sig = self.decode_sig(sig)?;
121 self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)?;
122 verified.push(sig.keyid.clone());
123 }
124
125 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
126 }
127
128 pub fn verify_any(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
134 let pae_bytes = self.reconstruct_pae(envelope)?;
135 let mut verified = Vec::new();
136
137 for sig in &envelope.signatures {
138 let pub_key = match self.keys.get(&sig.keyid) {
139 Some(k) => k,
140 None => continue, };
142 let raw_sig = match self.decode_sig(sig) {
143 Ok(b) => b,
144 Err(_) => continue, };
146 if self
147 .verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)
148 .is_ok()
149 {
150 verified.push(sig.keyid.clone());
151 }
152 }
153
154 if verified.is_empty() {
155 return Err(VerifyError::NoValidSignature);
156 }
157
158 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
159 }
160
161 fn reconstruct_pae(&self, envelope: &Envelope) -> Result<Vec<u8>, VerifyError> {
164 let payload_bytes = base64::Engine::decode(
165 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
166 &envelope.payload,
167 )
168 .map_err(|e| VerifyError::PayloadDecode(e.to_string()))?;
169
170 Ok(pae(&envelope.payload_type, &payload_bytes))
171 }
172
173 fn decode_sig(&self, sig: &crate::attestation::Signature) -> Result<Vec<u8>, VerifyError> {
174 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &sig.sig)
175 .map_err(|e| VerifyError::MalformedSignature(e.to_string()))
176 }
177
178 fn verify_sig(
179 &self,
180 pub_key: &VerifyingKey,
181 pae: &[u8],
182 raw_sig: &[u8],
183 key_id: &str,
184 ) -> Result<(), VerifyError> {
185 let sig_bytes: [u8; 64] = raw_sig.try_into().map_err(|_| {
186 VerifyError::MalformedSignature(format!(
187 "signature for {} is {} bytes, expected 64",
188 key_id,
189 raw_sig.len()
190 ))
191 })?;
192
193 let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
194
195 pub_key
196 .verify_strict(pae, &dalek_sig)
197 .map_err(|_| VerifyError::InvalidSignature(key_id.to_string()))
198 }
199
200 fn build_result(
201 &self,
202 pae_bytes: Vec<u8>,
203 verified: Vec<String>,
204 payload_type: &str,
205 ) -> VerifyResult {
206 VerifyResult {
207 artifact_id: artifact_id_from_pae(&pae_bytes),
208 digest: digest_from_pae(&pae_bytes),
209 verified_key_ids: verified,
210 payload_type: payload_type.to_string(),
211 }
212 }
213}
214
215pub fn verify_with_key(
217 envelope: &Envelope,
218 key_id: &str,
219 pub_key: VerifyingKey,
220) -> Result<VerifyResult, VerifyError> {
221 let mut keys = HashMap::new();
222 keys.insert(key_id.to_string(), pub_key);
223 let v = Verifier::new(keys);
224 v.verify_any(envelope)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::attestation::{sign, Ed25519Signer};
231 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
232 use serde::{Deserialize, Serialize};
233
234 #[derive(Debug, Serialize, Deserialize)]
235 struct TestStmt {
236 actor: String,
237 action: String,
238 }
239
240 const PT: &str = "application/vnd.treeship.action.v1+json";
241
242 fn stmt() -> TestStmt {
243 TestStmt {
244 actor: "agent://researcher".into(),
245 action: "tool.call".into(),
246 }
247 }
248
249 fn make_signer() -> Ed25519Signer {
250 Ed25519Signer::generate("key_test_01").unwrap()
251 }
252
253 #[test]
256 fn verify_roundtrip() {
257 let signer = make_signer();
258 let verifier = Verifier::from_signer(&signer);
259 let signed = sign(PT, &stmt(), &signer).unwrap();
260 let result = verifier.verify(&signed.envelope).unwrap();
261
262 assert_eq!(result.artifact_id, signed.artifact_id);
263 assert_eq!(result.digest, signed.digest);
264 assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
265 assert_eq!(result.payload_type, PT);
266 }
267
268 #[test]
269 fn verify_any_roundtrip() {
270 let signer = make_signer();
271 let verifier = Verifier::from_signer(&signer);
272 let signed = sign(PT, &stmt(), &signer).unwrap();
273 verifier.verify_any(&signed.envelope).unwrap();
274 }
275
276 #[test]
279 fn tampered_payload_fails() {
280 let signer = make_signer();
281 let verifier = Verifier::from_signer(&signer);
282 let signed = sign(PT, &stmt(), &signer).unwrap();
283
284 let malicious = TestStmt {
288 actor: "agent://attacker".into(),
289 action: "steal".into(),
290 };
291 let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
292
293 let mut tampered = signed.envelope.clone();
294 tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
295
296 let err = verifier.verify(&tampered).unwrap_err();
297 assert!(
298 matches!(err, VerifyError::InvalidSignature(_)),
299 "Expected InvalidSignature, got: {}",
300 err
301 );
302 }
303
304 #[test]
305 fn tampered_payload_type_fails() {
306 let signer = make_signer();
307 let verifier = Verifier::from_signer(&signer);
308 let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
309
310 let mut tampered = signed.envelope.clone();
313 tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
314
315 assert!(
316 verifier.verify(&tampered).is_err(),
317 "verify must fail when payloadType is tampered"
318 );
319 }
320
321 #[test]
324 fn wrong_key_fails() {
325 let signer = make_signer();
326 let wrong = Ed25519Signer::generate("key_test_01").unwrap();
329 let verifier = Verifier::from_signer(&wrong);
330
331 let signed = sign(PT, &stmt(), &signer).unwrap();
332 assert!(
333 verifier.verify(&signed.envelope).is_err(),
334 "verify with wrong public key must fail"
335 );
336 }
337
338 #[test]
339 fn unknown_key_fails() {
340 let signer = make_signer();
341 let verifier = Verifier::new(HashMap::new()); let signed = sign(PT, &stmt(), &signer).unwrap();
344 assert!(
345 verifier.verify(&signed.envelope).is_err(),
346 "verify with no trusted keys must fail"
347 );
348 }
349
350 #[test]
351 fn verify_any_skips_unknown_keys() {
352 let signer = make_signer();
353 let verifier = Verifier::from_signer(&signer);
355
356 let signed = sign(PT, &stmt(), &signer).unwrap();
358 let result = verifier.verify_any(&signed.envelope).unwrap();
359 assert_eq!(result.verified_key_ids.len(), 1);
360 }
361
362 #[test]
363 fn verify_rejects_empty_signature_envelope() {
364 let signer = make_signer();
369 let verifier = Verifier::from_signer(&signer);
370 let signed = sign(PT, &stmt(), &signer).unwrap();
371
372 let mut unsigned = signed.envelope.clone();
374 unsigned.signatures.clear();
375
376 let err = verifier.verify(&unsigned).unwrap_err();
377 assert!(
378 matches!(err, VerifyError::NoValidSignature),
379 "expected NoValidSignature for zero-signature envelope, got: {err}"
380 );
381
382 assert!(matches!(
385 verifier.verify_any(&unsigned).unwrap_err(),
386 VerifyError::NoValidSignature
387 ));
388 }
389
390 #[test]
391 fn verify_any_all_unknown_fails() {
392 let signer = make_signer();
393 let verifier = Verifier::new(HashMap::new());
394 let signed = sign(PT, &stmt(), &signer).unwrap();
395 assert!(matches!(
396 verifier.verify_any(&signed.envelope).unwrap_err(),
397 VerifyError::NoValidSignature
398 ));
399 }
400
401 #[test]
404 fn artifact_id_matches_sign() {
405 let signer = make_signer();
406 let verifier = Verifier::from_signer(&signer);
407 let signed = sign(PT, &stmt(), &signer).unwrap();
408 let verified = verifier.verify(&signed.envelope).unwrap();
409
410 assert_eq!(
413 signed.artifact_id, verified.artifact_id,
414 "ID from sign and verify must match"
415 );
416 }
417
418 #[test]
421 fn multi_key_verifier() {
422 let s1 = Ed25519Signer::generate("key_1").unwrap();
423 let s2 = Ed25519Signer::generate("key_2").unwrap();
424
425 let mut verifier = Verifier::from_signer(&s1);
426 verifier.add_key("key_2", s2.verifying_key());
427
428 let signed = sign(PT, &stmt(), &s1).unwrap();
430 let result = verifier.verify(&signed.envelope).unwrap();
431 assert_eq!(result.verified_key_ids, vec!["key_1"]);
432
433 let signed2 = sign(PT, &stmt(), &s2).unwrap();
435 let result2 = verifier.verify(&signed2.envelope).unwrap();
436 assert_eq!(result2.verified_key_ids, vec!["key_2"]);
437 }
438
439 #[test]
442 fn json_marshal_unmarshal() {
443 let signer = make_signer();
444 let verifier = Verifier::from_signer(&signer);
445 let signed = sign(PT, &stmt(), &signer).unwrap();
446
447 let json = signed.envelope.to_json().unwrap();
448 let restored = Envelope::from_json(&json).unwrap();
449
450 let result = verifier.verify(&restored).unwrap();
451 assert_eq!(result.artifact_id, signed.artifact_id);
452 }
453
454 #[test]
455 fn verifier_uses_strict_ed25519_rejecting_small_order_keys() {
456 use ed25519_dalek::VerifyingKey;
463 let small_order = [
467 0x00u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
468 0, 0, 0, 0, 0,
469 ];
470 if let Ok(vk) = VerifyingKey::from_bytes(&small_order) {
474 let zero_sig = ed25519_dalek::Signature::from_bytes(&[0u8; 64]);
475 assert!(
476 vk.verify_strict(b"anything", &zero_sig).is_err(),
477 "strict verification must reject a small-order key"
478 );
479 }
480 let signer = make_signer();
483 let env = sign(PT, &stmt(), &signer).unwrap().envelope;
484 let mut v = Verifier::new(std::collections::HashMap::new());
485 v.add_key(signer.key_id().to_string(), signer.verifying_key());
486 assert!(
487 v.verify_any(&env).is_ok(),
488 "a real signature must still verify strictly"
489 );
490 }
491}