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 let pae_bytes = self.reconstruct_pae(envelope)?;
97 let mut verified = Vec::new();
98
99 for sig in &envelope.signatures {
100 let pub_key = self.keys.get(&sig.keyid)
101 .ok_or_else(|| VerifyError::UnknownKey(sig.keyid.clone()))?;
102
103 let raw_sig = self.decode_sig(sig)?;
104 self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)?;
105 verified.push(sig.keyid.clone());
106 }
107
108 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
109 }
110
111 pub fn verify_any(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
117 let pae_bytes = self.reconstruct_pae(envelope)?;
118 let mut verified = Vec::new();
119
120 for sig in &envelope.signatures {
121 let pub_key = match self.keys.get(&sig.keyid) {
122 Some(k) => k,
123 None => continue, };
125 let raw_sig = match self.decode_sig(sig) {
126 Ok(b) => b,
127 Err(_) => continue, };
129 if self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid).is_ok() {
130 verified.push(sig.keyid.clone());
131 }
132 }
133
134 if verified.is_empty() {
135 return Err(VerifyError::NoValidSignature);
136 }
137
138 Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
139 }
140
141 fn reconstruct_pae(&self, envelope: &Envelope) -> Result<Vec<u8>, VerifyError> {
144 let payload_bytes = base64::Engine::decode(
145 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
146 &envelope.payload,
147 ).map_err(|e| VerifyError::PayloadDecode(e.to_string()))?;
148
149 Ok(pae(&envelope.payload_type, &payload_bytes))
150 }
151
152 fn decode_sig(&self, sig: &crate::attestation::Signature) -> Result<Vec<u8>, VerifyError> {
153 base64::Engine::decode(
154 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
155 &sig.sig,
156 ).map_err(|e| VerifyError::MalformedSignature(e.to_string()))
157 }
158
159 fn verify_sig(
160 &self,
161 pub_key: &VerifyingKey,
162 pae: &[u8],
163 raw_sig: &[u8],
164 key_id: &str,
165 ) -> Result<(), VerifyError> {
166 let sig_bytes: [u8; 64] = raw_sig.try_into()
167 .map_err(|_| VerifyError::MalformedSignature(
168 format!("signature for {} is {} bytes, expected 64", key_id, raw_sig.len())
169 ))?;
170
171 let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
172
173 pub_key.verify(pae, &dalek_sig)
174 .map_err(|_| VerifyError::InvalidSignature(key_id.to_string()))
175 }
176
177 fn build_result(
178 &self,
179 pae_bytes: Vec<u8>,
180 verified: Vec<String>,
181 payload_type: &str,
182 ) -> VerifyResult {
183 VerifyResult {
184 artifact_id: artifact_id_from_pae(&pae_bytes),
185 digest: digest_from_pae(&pae_bytes),
186 verified_key_ids: verified,
187 payload_type: payload_type.to_string(),
188 }
189 }
190}
191
192pub fn verify_with_key(
194 envelope: &Envelope,
195 key_id: &str,
196 pub_key: VerifyingKey,
197) -> Result<VerifyResult, VerifyError> {
198 let v = Verifier::from_signer(&Ed25519Signer::from_bytes(
199 key_id,
200 pub_key.as_bytes(),
201 ).map_err(|e| VerifyError::InvalidSignature(e.to_string()))?);
202 v.verify_any(envelope)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::attestation::{sign, Ed25519Signer};
209 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
210 use serde::{Deserialize, Serialize};
211
212 #[derive(Debug, Serialize, Deserialize)]
213 struct TestStmt { actor: String, action: String }
214
215 const PT: &str = "application/vnd.treeship.action.v1+json";
216
217 fn stmt() -> TestStmt {
218 TestStmt { actor: "agent://researcher".into(), action: "tool.call".into() }
219 }
220
221 fn make_signer() -> Ed25519Signer {
222 Ed25519Signer::generate("key_test_01").unwrap()
223 }
224
225 #[test]
228 fn verify_roundtrip() {
229 let signer = make_signer();
230 let verifier = Verifier::from_signer(&signer);
231 let signed = sign(PT, &stmt(), &signer).unwrap();
232 let result = verifier.verify(&signed.envelope).unwrap();
233
234 assert_eq!(result.artifact_id, signed.artifact_id);
235 assert_eq!(result.digest, signed.digest);
236 assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
237 assert_eq!(result.payload_type, PT);
238 }
239
240 #[test]
241 fn verify_any_roundtrip() {
242 let signer = make_signer();
243 let verifier = Verifier::from_signer(&signer);
244 let signed = sign(PT, &stmt(), &signer).unwrap();
245 verifier.verify_any(&signed.envelope).unwrap();
246 }
247
248 #[test]
251 fn tampered_payload_fails() {
252 let signer = make_signer();
253 let verifier = Verifier::from_signer(&signer);
254 let signed = sign(PT, &stmt(), &signer).unwrap();
255
256 let malicious = TestStmt { actor: "agent://attacker".into(), action: "steal".into() };
260 let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
261
262 let mut tampered = signed.envelope.clone();
263 tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
264
265 let err = verifier.verify(&tampered).unwrap_err();
266 assert!(
267 matches!(err, VerifyError::InvalidSignature(_)),
268 "Expected InvalidSignature, got: {}", err
269 );
270 }
271
272 #[test]
273 fn tampered_payload_type_fails() {
274 let signer = make_signer();
275 let verifier = Verifier::from_signer(&signer);
276 let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
277
278 let mut tampered = signed.envelope.clone();
281 tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
282
283 assert!(
284 verifier.verify(&tampered).is_err(),
285 "verify must fail when payloadType is tampered"
286 );
287 }
288
289 #[test]
292 fn wrong_key_fails() {
293 let signer = make_signer();
294 let wrong = Ed25519Signer::generate("key_test_01").unwrap();
297 let verifier = Verifier::from_signer(&wrong);
298
299 let signed = sign(PT, &stmt(), &signer).unwrap();
300 assert!(
301 verifier.verify(&signed.envelope).is_err(),
302 "verify with wrong public key must fail"
303 );
304 }
305
306 #[test]
307 fn unknown_key_fails() {
308 let signer = make_signer();
309 let verifier = Verifier::new(HashMap::new()); let signed = sign(PT, &stmt(), &signer).unwrap();
312 assert!(
313 verifier.verify(&signed.envelope).is_err(),
314 "verify with no trusted keys must fail"
315 );
316 }
317
318 #[test]
319 fn verify_any_skips_unknown_keys() {
320 let signer = make_signer();
321 let verifier = Verifier::from_signer(&signer);
323
324 let signed = sign(PT, &stmt(), &signer).unwrap();
326 let result = verifier.verify_any(&signed.envelope).unwrap();
327 assert_eq!(result.verified_key_ids.len(), 1);
328 }
329
330 #[test]
331 fn verify_any_all_unknown_fails() {
332 let signer = make_signer();
333 let verifier = Verifier::new(HashMap::new());
334 let signed = sign(PT, &stmt(), &signer).unwrap();
335 assert!(matches!(
336 verifier.verify_any(&signed.envelope).unwrap_err(),
337 VerifyError::NoValidSignature
338 ));
339 }
340
341 #[test]
344 fn artifact_id_matches_sign() {
345 let signer = make_signer();
346 let verifier = Verifier::from_signer(&signer);
347 let signed = sign(PT, &stmt(), &signer).unwrap();
348 let verified = verifier.verify(&signed.envelope).unwrap();
349
350 assert_eq!(
353 signed.artifact_id, verified.artifact_id,
354 "ID from sign and verify must match"
355 );
356 }
357
358 #[test]
361 fn multi_key_verifier() {
362 let s1 = Ed25519Signer::generate("key_1").unwrap();
363 let s2 = Ed25519Signer::generate("key_2").unwrap();
364
365 let mut verifier = Verifier::from_signer(&s1);
366 verifier.add_key("key_2", s2.verifying_key());
367
368 let signed = sign(PT, &stmt(), &s1).unwrap();
370 let result = verifier.verify(&signed.envelope).unwrap();
371 assert_eq!(result.verified_key_ids, vec!["key_1"]);
372
373 let signed2 = sign(PT, &stmt(), &s2).unwrap();
375 let result2 = verifier.verify(&signed2.envelope).unwrap();
376 assert_eq!(result2.verified_key_ids, vec!["key_2"]);
377 }
378
379 #[test]
382 fn json_marshal_unmarshal() {
383 let signer = make_signer();
384 let verifier = Verifier::from_signer(&signer);
385 let signed = sign(PT, &stmt(), &signer).unwrap();
386
387 let json = signed.envelope.to_json().unwrap();
388 let restored = Envelope::from_json(&json).unwrap();
389
390 let result = verifier.verify(&restored).unwrap();
391 assert_eq!(result.artifact_id, signed.artifact_id);
392 }
393}