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 mut keys = HashMap::new();
199 keys.insert(key_id.to_string(), pub_key);
200 let v = Verifier::new(keys);
201 v.verify_any(envelope)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::attestation::{sign, Ed25519Signer};
208 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
209 use serde::{Deserialize, Serialize};
210
211 #[derive(Debug, Serialize, Deserialize)]
212 struct TestStmt { actor: String, action: String }
213
214 const PT: &str = "application/vnd.treeship.action.v1+json";
215
216 fn stmt() -> TestStmt {
217 TestStmt { actor: "agent://researcher".into(), action: "tool.call".into() }
218 }
219
220 fn make_signer() -> Ed25519Signer {
221 Ed25519Signer::generate("key_test_01").unwrap()
222 }
223
224 #[test]
227 fn verify_roundtrip() {
228 let signer = make_signer();
229 let verifier = Verifier::from_signer(&signer);
230 let signed = sign(PT, &stmt(), &signer).unwrap();
231 let result = verifier.verify(&signed.envelope).unwrap();
232
233 assert_eq!(result.artifact_id, signed.artifact_id);
234 assert_eq!(result.digest, signed.digest);
235 assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
236 assert_eq!(result.payload_type, PT);
237 }
238
239 #[test]
240 fn verify_any_roundtrip() {
241 let signer = make_signer();
242 let verifier = Verifier::from_signer(&signer);
243 let signed = sign(PT, &stmt(), &signer).unwrap();
244 verifier.verify_any(&signed.envelope).unwrap();
245 }
246
247 #[test]
250 fn tampered_payload_fails() {
251 let signer = make_signer();
252 let verifier = Verifier::from_signer(&signer);
253 let signed = sign(PT, &stmt(), &signer).unwrap();
254
255 let malicious = TestStmt { actor: "agent://attacker".into(), action: "steal".into() };
259 let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
260
261 let mut tampered = signed.envelope.clone();
262 tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
263
264 let err = verifier.verify(&tampered).unwrap_err();
265 assert!(
266 matches!(err, VerifyError::InvalidSignature(_)),
267 "Expected InvalidSignature, got: {}", err
268 );
269 }
270
271 #[test]
272 fn tampered_payload_type_fails() {
273 let signer = make_signer();
274 let verifier = Verifier::from_signer(&signer);
275 let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
276
277 let mut tampered = signed.envelope.clone();
280 tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
281
282 assert!(
283 verifier.verify(&tampered).is_err(),
284 "verify must fail when payloadType is tampered"
285 );
286 }
287
288 #[test]
291 fn wrong_key_fails() {
292 let signer = make_signer();
293 let wrong = Ed25519Signer::generate("key_test_01").unwrap();
296 let verifier = Verifier::from_signer(&wrong);
297
298 let signed = sign(PT, &stmt(), &signer).unwrap();
299 assert!(
300 verifier.verify(&signed.envelope).is_err(),
301 "verify with wrong public key must fail"
302 );
303 }
304
305 #[test]
306 fn unknown_key_fails() {
307 let signer = make_signer();
308 let verifier = Verifier::new(HashMap::new()); let signed = sign(PT, &stmt(), &signer).unwrap();
311 assert!(
312 verifier.verify(&signed.envelope).is_err(),
313 "verify with no trusted keys must fail"
314 );
315 }
316
317 #[test]
318 fn verify_any_skips_unknown_keys() {
319 let signer = make_signer();
320 let verifier = Verifier::from_signer(&signer);
322
323 let signed = sign(PT, &stmt(), &signer).unwrap();
325 let result = verifier.verify_any(&signed.envelope).unwrap();
326 assert_eq!(result.verified_key_ids.len(), 1);
327 }
328
329 #[test]
330 fn verify_any_all_unknown_fails() {
331 let signer = make_signer();
332 let verifier = Verifier::new(HashMap::new());
333 let signed = sign(PT, &stmt(), &signer).unwrap();
334 assert!(matches!(
335 verifier.verify_any(&signed.envelope).unwrap_err(),
336 VerifyError::NoValidSignature
337 ));
338 }
339
340 #[test]
343 fn artifact_id_matches_sign() {
344 let signer = make_signer();
345 let verifier = Verifier::from_signer(&signer);
346 let signed = sign(PT, &stmt(), &signer).unwrap();
347 let verified = verifier.verify(&signed.envelope).unwrap();
348
349 assert_eq!(
352 signed.artifact_id, verified.artifact_id,
353 "ID from sign and verify must match"
354 );
355 }
356
357 #[test]
360 fn multi_key_verifier() {
361 let s1 = Ed25519Signer::generate("key_1").unwrap();
362 let s2 = Ed25519Signer::generate("key_2").unwrap();
363
364 let mut verifier = Verifier::from_signer(&s1);
365 verifier.add_key("key_2", s2.verifying_key());
366
367 let signed = sign(PT, &stmt(), &s1).unwrap();
369 let result = verifier.verify(&signed.envelope).unwrap();
370 assert_eq!(result.verified_key_ids, vec!["key_1"]);
371
372 let signed2 = sign(PT, &stmt(), &s2).unwrap();
374 let result2 = verifier.verify(&signed2.envelope).unwrap();
375 assert_eq!(result2.verified_key_ids, vec!["key_2"]);
376 }
377
378 #[test]
381 fn json_marshal_unmarshal() {
382 let signer = make_signer();
383 let verifier = Verifier::from_signer(&signer);
384 let signed = sign(PT, &stmt(), &signer).unwrap();
385
386 let json = signed.envelope.to_json().unwrap();
387 let restored = Envelope::from_json(&json).unwrap();
388
389 let result = verifier.verify(&restored).unwrap();
390 assert_eq!(result.artifact_id, signed.artifact_id);
391 }
392}