Skip to main content

treeship_core/attestation/
verify.rs

1use 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/// The result of a successful verification.
12#[derive(Debug)]
13pub struct VerifyResult {
14    /// Content-addressed ID **re-derived** from the envelope during verification.
15    /// If the envelope payload or payloadType was tampered with since signing,
16    /// this will differ from any stored artifact ID — a reliable tamper signal.
17    pub artifact_id: ArtifactId,
18
19    /// Full SHA-256 digest of the PAE bytes: "sha256:<hex>".
20    pub digest: String,
21
22    /// Key IDs whose signatures were successfully verified.
23    pub verified_key_ids: Vec<String>,
24
25    /// The payloadType from the envelope.
26    pub payload_type: String,
27}
28
29/// Error from verification.
30#[derive(Debug)]
31pub enum VerifyError {
32    /// The payload could not be base64-decoded.
33    PayloadDecode(String),
34    /// A key ID in the envelope has no corresponding trusted public key.
35    UnknownKey(String),
36    /// A signature was cryptographically invalid.
37    InvalidSignature(String),
38    /// No valid signature was found from any trusted key (VerifyAny only).
39    NoValidSignature,
40    /// The signature bytes were malformed (wrong length etc.).
41    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/// Holds trusted public keys and verifies DSSE envelopes against them.
59///
60/// Separate from `Signer` — signing requires a private key, verification
61/// requires only public keys. Verifiers are cheap to clone and pass around.
62#[derive(Clone)]
63pub struct Verifier {
64    /// Map of key_id → VerifyingKey (Ed25519 public key).
65    keys: HashMap<String, VerifyingKey>,
66}
67
68impl Verifier {
69    /// Creates a Verifier with the given trusted key map.
70    pub fn new(keys: HashMap<String, VerifyingKey>) -> Self {
71        Self { keys }
72    }
73
74    /// Convenience: creates a single-key Verifier from an `Ed25519Signer`.
75    /// Most useful in tests and local-only workflows.
76    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    /// Adds a trusted public key.
83    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    /// Verifies all signatures in the envelope.
88    ///
89    /// Returns `Ok(VerifyResult)` only if **every** signature in the envelope
90    /// is valid and its key is trusted. Any unknown key or invalid signature
91    /// returns `Err`.
92    ///
93    /// Use this for strict verification where all listed signers must be valid
94    /// (e.g., hybrid Ed25519 + ML-DSA in v2 where both are required).
95    pub fn verify(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
96        // An envelope with zero signatures has nothing to verify. The for-loop
97        // below would be a no-op and `verified` would stay empty, returning
98        // `Ok` to any caller that only checks `Result::is_ok()`. Reject up
99        // front so an unsigned envelope cannot masquerade as verified.
100        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    /// Verifies that at least one signature in the envelope is valid from a
120    /// trusted key. Signatures from unknown keys are skipped.
121    ///
122    /// Use this during key rotation when old and new keys may coexist, or
123    /// when accepting envelopes from multiple possible signers.
124    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, // skip unknown keys
132            };
133            let raw_sig = match self.decode_sig(sig) {
134                Ok(b)  => b,
135                Err(_) => continue, // skip malformed sigs
136            };
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    // --- private helpers ---
150
151    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
200/// Convenience: verify an envelope with a single known public key.
201pub 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    // --- round-trip ---
233
234    #[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    // --- tamper detection ---
256
257    #[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        // Replace the payload with different content. The signature was
264        // computed over PAE(original_payload) — after tampering the PAE
265        // is different and the signature fails.
266        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        // Change the payloadType without re-signing.
286        // PAE includes payloadType, so the reconstructed PAE ≠ signed PAE.
287        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    // --- key rejection ---
297
298    #[test]
299    fn wrong_key_fails() {
300        let signer      = make_signer();
301        // Build a verifier with a different keypair but the same key_id.
302        // Simulates an attacker substituting their public key.
303        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()); // no keys
317
318        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        // Verifier only knows about key_test_01
329        let verifier = Verifier::from_signer(&signer);
330
331        // Envelope only has key_test_01 — verifier should accept it
332        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        // P0 #4: an envelope with zero signatures must not verify. Without
340        // the explicit check, the for-loop is a no-op and `verify` returns
341        // `Ok(...)` with an empty `verified_key_ids` list — callers that
342        // only check `Result::is_ok()` would accept unsigned envelopes.
343        let signer   = make_signer();
344        let verifier = Verifier::from_signer(&signer);
345        let signed   = sign(PT, &stmt(), &signer).unwrap();
346
347        // Strip the signatures off an otherwise-valid envelope.
348        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        // verify_any already rejects this via its `verified.is_empty()` guard,
358        // but assert it explicitly to keep both paths covered.
359        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    // --- ID consistency ---
377
378    #[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        // The ID is derived from the same PAE bytes during both sign and verify.
386        // A mismatch here means the envelope was tampered with between sign and verify.
387        assert_eq!(
388            signed.artifact_id, verified.artifact_id,
389            "ID from sign and verify must match"
390        );
391    }
392
393    // --- multi-key verifier ---
394
395    #[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        // Sign with s1 — verifier knows both keys, should accept
404        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        // Sign with s2 — should also work
409        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    // --- serialization ---
415
416    #[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}