treeship_core/attestation/
verify.rs1use std::collections::HashMap;
2use ed25519_dalek::{VerifyingKey, Verifier as DalekVerifier, Signature as DalekSignature};
3
4use crate::attestation::{
5 pae,
6 artifact_id_from_pae, digest_from_pae, ArtifactId,
7 Ed25519Signer, Signer,
8 Envelope,
9};
10
11#[derive(Debug)]
13pub struct VerifyResult {
14 pub artifact_id: ArtifactId,
18
19 pub digest: String,
21
22 pub verified_key_ids: Vec<String>,
24
25 pub payload_type: String,
27}
28
29#[derive(Debug)]
31pub enum VerifyError {
32 PayloadDecode(String),
34 UnknownKey(String),
36 InvalidSignature(String),
38 NoValidSignature,
40 MalformedSignature(String),
42}
43
44impl std::fmt::Display for VerifyError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::PayloadDecode(e) => write!(f, "payload decode: {}", e),
48 Self::UnknownKey(id) => write!(f, "unknown key: {}", id),
49 Self::InvalidSignature(id) => write!(f, "invalid signature for key: {}", id),
50 Self::NoValidSignature => write!(f, "no valid signature from any trusted key"),
51 Self::MalformedSignature(e) => write!(f, "malformed signature bytes: {}", e),
52 }
53 }
54}
55
56impl std::error::Error for VerifyError {}
57
58#[derive(Clone)]
63pub struct Verifier {
64 keys: HashMap<String, VerifyingKey>,
66}
67
68impl Verifier {
69 pub fn new(keys: HashMap<String, VerifyingKey>) -> Self {
71 Self { keys }
72 }
73
74 pub fn from_signer(signer: &Ed25519Signer) -> Self {
77 let mut keys = HashMap::new();
78 keys.insert(signer.key_id().to_string(), signer.verifying_key());
79 Self { keys }
80 }
81
82 pub fn add_key(&mut self, key_id: impl Into<String>, pub_key: VerifyingKey) {
84 self.keys.insert(key_id.into(), pub_key);
85 }
86
87 pub fn verify(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
96 if envelope.signatures.is_empty() {
101 return Err(VerifyError::NoValidSignature);
102 }
103
104 let pae_bytes = self.reconstruct_pae(envelope)?;
105 let mut verified = Vec::new();
106
107 for sig in &envelope.signatures {
108 let pub_key = self.keys.get(&sig.keyid)
109 .ok_or_else(|| VerifyError::UnknownKey(sig.keyid.clone()))?;
110
111 let raw_sig = self.decode_sig(sig)?;
112 self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)?;
113 verified.push(sig.keyid.clone());
114 }
115
116 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
117 }
118
119 pub fn verify_any(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
125 let pae_bytes = self.reconstruct_pae(envelope)?;
126 let mut verified = Vec::new();
127
128 for sig in &envelope.signatures {
129 let pub_key = match self.keys.get(&sig.keyid) {
130 Some(k) => k,
131 None => continue, };
133 let raw_sig = match self.decode_sig(sig) {
134 Ok(b) => b,
135 Err(_) => continue, };
137 if self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid).is_ok() {
138 verified.push(sig.keyid.clone());
139 }
140 }
141
142 if verified.is_empty() {
143 return Err(VerifyError::NoValidSignature);
144 }
145
146 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
147 }
148
149 fn reconstruct_pae(&self, envelope: &Envelope) -> Result<Vec<u8>, VerifyError> {
152 let payload_bytes = base64::Engine::decode(
153 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
154 &envelope.payload,
155 ).map_err(|e| VerifyError::PayloadDecode(e.to_string()))?;
156
157 Ok(pae(&envelope.payload_type, &payload_bytes))
158 }
159
160 fn decode_sig(&self, sig: &crate::attestation::Signature) -> Result<Vec<u8>, VerifyError> {
161 base64::Engine::decode(
162 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
163 &sig.sig,
164 ).map_err(|e| VerifyError::MalformedSignature(e.to_string()))
165 }
166
167 fn verify_sig(
168 &self,
169 pub_key: &VerifyingKey,
170 pae: &[u8],
171 raw_sig: &[u8],
172 key_id: &str,
173 ) -> Result<(), VerifyError> {
174 let sig_bytes: [u8; 64] = raw_sig.try_into()
175 .map_err(|_| VerifyError::MalformedSignature(
176 format!("signature for {} is {} bytes, expected 64", key_id, raw_sig.len())
177 ))?;
178
179 let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
180
181 pub_key.verify(pae, &dalek_sig)
182 .map_err(|_| VerifyError::InvalidSignature(key_id.to_string()))
183 }
184
185 fn build_result(
186 &self,
187 pae_bytes: Vec<u8>,
188 verified: Vec<String>,
189 payload_type: &str,
190 ) -> VerifyResult {
191 VerifyResult {
192 artifact_id: artifact_id_from_pae(&pae_bytes),
193 digest: digest_from_pae(&pae_bytes),
194 verified_key_ids: verified,
195 payload_type: payload_type.to_string(),
196 }
197 }
198}
199
200pub fn verify_with_key(
202 envelope: &Envelope,
203 key_id: &str,
204 pub_key: VerifyingKey,
205) -> Result<VerifyResult, VerifyError> {
206 let mut keys = HashMap::new();
207 keys.insert(key_id.to_string(), pub_key);
208 let v = Verifier::new(keys);
209 v.verify_any(envelope)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::attestation::{sign, Ed25519Signer};
216 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
217 use serde::{Deserialize, Serialize};
218
219 #[derive(Debug, Serialize, Deserialize)]
220 struct TestStmt { actor: String, action: String }
221
222 const PT: &str = "application/vnd.treeship.action.v1+json";
223
224 fn stmt() -> TestStmt {
225 TestStmt { actor: "agent://researcher".into(), action: "tool.call".into() }
226 }
227
228 fn make_signer() -> Ed25519Signer {
229 Ed25519Signer::generate("key_test_01").unwrap()
230 }
231
232 #[test]
235 fn verify_roundtrip() {
236 let signer = make_signer();
237 let verifier = Verifier::from_signer(&signer);
238 let signed = sign(PT, &stmt(), &signer).unwrap();
239 let result = verifier.verify(&signed.envelope).unwrap();
240
241 assert_eq!(result.artifact_id, signed.artifact_id);
242 assert_eq!(result.digest, signed.digest);
243 assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
244 assert_eq!(result.payload_type, PT);
245 }
246
247 #[test]
248 fn verify_any_roundtrip() {
249 let signer = make_signer();
250 let verifier = Verifier::from_signer(&signer);
251 let signed = sign(PT, &stmt(), &signer).unwrap();
252 verifier.verify_any(&signed.envelope).unwrap();
253 }
254
255 #[test]
258 fn tampered_payload_fails() {
259 let signer = make_signer();
260 let verifier = Verifier::from_signer(&signer);
261 let signed = sign(PT, &stmt(), &signer).unwrap();
262
263 let malicious = TestStmt { actor: "agent://attacker".into(), action: "steal".into() };
267 let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
268
269 let mut tampered = signed.envelope.clone();
270 tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
271
272 let err = verifier.verify(&tampered).unwrap_err();
273 assert!(
274 matches!(err, VerifyError::InvalidSignature(_)),
275 "Expected InvalidSignature, got: {}", err
276 );
277 }
278
279 #[test]
280 fn tampered_payload_type_fails() {
281 let signer = make_signer();
282 let verifier = Verifier::from_signer(&signer);
283 let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
284
285 let mut tampered = signed.envelope.clone();
288 tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
289
290 assert!(
291 verifier.verify(&tampered).is_err(),
292 "verify must fail when payloadType is tampered"
293 );
294 }
295
296 #[test]
299 fn wrong_key_fails() {
300 let signer = make_signer();
301 let wrong = Ed25519Signer::generate("key_test_01").unwrap();
304 let verifier = Verifier::from_signer(&wrong);
305
306 let signed = sign(PT, &stmt(), &signer).unwrap();
307 assert!(
308 verifier.verify(&signed.envelope).is_err(),
309 "verify with wrong public key must fail"
310 );
311 }
312
313 #[test]
314 fn unknown_key_fails() {
315 let signer = make_signer();
316 let verifier = Verifier::new(HashMap::new()); let signed = sign(PT, &stmt(), &signer).unwrap();
319 assert!(
320 verifier.verify(&signed.envelope).is_err(),
321 "verify with no trusted keys must fail"
322 );
323 }
324
325 #[test]
326 fn verify_any_skips_unknown_keys() {
327 let signer = make_signer();
328 let verifier = Verifier::from_signer(&signer);
330
331 let signed = sign(PT, &stmt(), &signer).unwrap();
333 let result = verifier.verify_any(&signed.envelope).unwrap();
334 assert_eq!(result.verified_key_ids.len(), 1);
335 }
336
337 #[test]
338 fn verify_rejects_empty_signature_envelope() {
339 let signer = make_signer();
344 let verifier = Verifier::from_signer(&signer);
345 let signed = sign(PT, &stmt(), &signer).unwrap();
346
347 let mut unsigned = signed.envelope.clone();
349 unsigned.signatures.clear();
350
351 let err = verifier.verify(&unsigned).unwrap_err();
352 assert!(
353 matches!(err, VerifyError::NoValidSignature),
354 "expected NoValidSignature for zero-signature envelope, got: {err}"
355 );
356
357 assert!(matches!(
360 verifier.verify_any(&unsigned).unwrap_err(),
361 VerifyError::NoValidSignature
362 ));
363 }
364
365 #[test]
366 fn verify_any_all_unknown_fails() {
367 let signer = make_signer();
368 let verifier = Verifier::new(HashMap::new());
369 let signed = sign(PT, &stmt(), &signer).unwrap();
370 assert!(matches!(
371 verifier.verify_any(&signed.envelope).unwrap_err(),
372 VerifyError::NoValidSignature
373 ));
374 }
375
376 #[test]
379 fn artifact_id_matches_sign() {
380 let signer = make_signer();
381 let verifier = Verifier::from_signer(&signer);
382 let signed = sign(PT, &stmt(), &signer).unwrap();
383 let verified = verifier.verify(&signed.envelope).unwrap();
384
385 assert_eq!(
388 signed.artifact_id, verified.artifact_id,
389 "ID from sign and verify must match"
390 );
391 }
392
393 #[test]
396 fn multi_key_verifier() {
397 let s1 = Ed25519Signer::generate("key_1").unwrap();
398 let s2 = Ed25519Signer::generate("key_2").unwrap();
399
400 let mut verifier = Verifier::from_signer(&s1);
401 verifier.add_key("key_2", s2.verifying_key());
402
403 let signed = sign(PT, &stmt(), &s1).unwrap();
405 let result = verifier.verify(&signed.envelope).unwrap();
406 assert_eq!(result.verified_key_ids, vec!["key_1"]);
407
408 let signed2 = sign(PT, &stmt(), &s2).unwrap();
410 let result2 = verifier.verify(&signed2.envelope).unwrap();
411 assert_eq!(result2.verified_key_ids, vec!["key_2"]);
412 }
413
414 #[test]
417 fn json_marshal_unmarshal() {
418 let signer = make_signer();
419 let verifier = Verifier::from_signer(&signer);
420 let signed = sign(PT, &stmt(), &signer).unwrap();
421
422 let json = signed.envelope.to_json().unwrap();
423 let restored = Envelope::from_json(&json).unwrap();
424
425 let result = verifier.verify(&restored).unwrap();
426 assert_eq!(result.artifact_id, signed.artifact_id);
427 }
428}