Skip to main content

treeship_core/attestation/
verify.rs

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/// The result of a successful verification.
9#[derive(Debug)]
10pub struct VerifyResult {
11    /// Content-addressed ID **re-derived** from the envelope during verification.
12    /// If the envelope payload or payloadType was tampered with since signing,
13    /// this will differ from any stored artifact ID — a reliable tamper signal.
14    pub artifact_id: ArtifactId,
15
16    /// Full SHA-256 digest of the PAE bytes: "sha256:<hex>".
17    pub digest: String,
18
19    /// Key IDs whose signatures were successfully verified.
20    pub verified_key_ids: Vec<String>,
21
22    /// The payloadType from the envelope.
23    pub payload_type: String,
24}
25
26/// Error from verification.
27#[derive(Debug)]
28pub enum VerifyError {
29    /// The payload could not be base64-decoded.
30    PayloadDecode(String),
31    /// A key ID in the envelope has no corresponding trusted public key.
32    UnknownKey(String),
33    /// A signature was cryptographically invalid.
34    InvalidSignature(String),
35    /// No valid signature was found from any trusted key (VerifyAny only).
36    NoValidSignature,
37    /// The signature bytes were malformed (wrong length etc.).
38    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/// Holds trusted public keys and verifies DSSE envelopes against them.
56///
57/// Separate from `Signer` — signing requires a private key, verification
58/// requires only public keys. Verifiers are cheap to clone and pass around.
59#[derive(Clone)]
60pub struct Verifier {
61    /// Map of key_id → VerifyingKey (Ed25519 public key).
62    keys: HashMap<String, VerifyingKey>,
63}
64
65impl Verifier {
66    /// Creates a Verifier with the given trusted key map.
67    pub fn new(keys: HashMap<String, VerifyingKey>) -> Self {
68        Self { keys }
69    }
70
71    /// Convenience: creates a single-key Verifier from an `Ed25519Signer`.
72    /// Most useful in tests and local-only workflows.
73    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    /// Adds a trusted public key.
80    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    /// Verifies all signatures in the envelope.
85    ///
86    /// Returns `Ok(VerifyResult)` only if **every** signature in the envelope
87    /// is valid and its key is trusted. Any unknown key or invalid signature
88    /// returns `Err`.
89    ///
90    /// Use this for strict verification where all listed signers must be valid
91    /// (e.g., hybrid Ed25519 + ML-DSA in v2 where both are required).
92    pub fn verify(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
93        // An envelope with zero signatures has nothing to verify. The for-loop
94        // below would be a no-op and `verified` would stay empty, returning
95        // `Ok` to any caller that only checks `Result::is_ok()`. Reject up
96        // front so an unsigned envelope cannot masquerade as verified.
97        if envelope.signatures.is_empty() {
98            return Err(VerifyError::NoValidSignature);
99        }
100
101        let pae_bytes = self.reconstruct_pae(envelope)?;
102        let mut verified = Vec::new();
103
104        for sig in &envelope.signatures {
105            let pub_key = self
106                .keys
107                .get(&sig.keyid)
108                .ok_or_else(|| VerifyError::UnknownKey(sig.keyid.clone()))?;
109
110            let raw_sig = self.decode_sig(sig)?;
111            self.verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)?;
112            verified.push(sig.keyid.clone());
113        }
114
115        Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
116    }
117
118    /// Verifies that at least one signature in the envelope is valid from a
119    /// trusted key. Signatures from unknown keys are skipped.
120    ///
121    /// Use this during key rotation when old and new keys may coexist, or
122    /// when accepting envelopes from multiple possible signers.
123    pub fn verify_any(&self, envelope: &Envelope) -> Result<VerifyResult, VerifyError> {
124        let pae_bytes = self.reconstruct_pae(envelope)?;
125        let mut verified = Vec::new();
126
127        for sig in &envelope.signatures {
128            let pub_key = match self.keys.get(&sig.keyid) {
129                Some(k) => k,
130                None => continue, // skip unknown keys
131            };
132            let raw_sig = match self.decode_sig(sig) {
133                Ok(b) => b,
134                Err(_) => continue, // skip malformed sigs
135            };
136            if self
137                .verify_sig(pub_key, &pae_bytes, &raw_sig, &sig.keyid)
138                .is_ok()
139            {
140                verified.push(sig.keyid.clone());
141            }
142        }
143
144        if verified.is_empty() {
145            return Err(VerifyError::NoValidSignature);
146        }
147
148        Ok(self.build_result(pae_bytes, verified, &envelope.payload_type))
149    }
150
151    // --- private helpers ---
152
153    fn reconstruct_pae(&self, envelope: &Envelope) -> Result<Vec<u8>, VerifyError> {
154        let payload_bytes = base64::Engine::decode(
155            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
156            &envelope.payload,
157        )
158        .map_err(|e| VerifyError::PayloadDecode(e.to_string()))?;
159
160        Ok(pae(&envelope.payload_type, &payload_bytes))
161    }
162
163    fn decode_sig(&self, sig: &crate::attestation::Signature) -> Result<Vec<u8>, VerifyError> {
164        base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &sig.sig)
165            .map_err(|e| VerifyError::MalformedSignature(e.to_string()))
166    }
167
168    fn verify_sig(
169        &self,
170        pub_key: &VerifyingKey,
171        pae: &[u8],
172        raw_sig: &[u8],
173        key_id: &str,
174    ) -> Result<(), VerifyError> {
175        let sig_bytes: [u8; 64] = raw_sig.try_into().map_err(|_| {
176            VerifyError::MalformedSignature(format!(
177                "signature for {} is {} bytes, expected 64",
178                key_id,
179                raw_sig.len()
180            ))
181        })?;
182
183        let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
184
185        pub_key
186            .verify_strict(pae, &dalek_sig)
187            .map_err(|_| VerifyError::InvalidSignature(key_id.to_string()))
188    }
189
190    fn build_result(
191        &self,
192        pae_bytes: Vec<u8>,
193        verified: Vec<String>,
194        payload_type: &str,
195    ) -> VerifyResult {
196        VerifyResult {
197            artifact_id: artifact_id_from_pae(&pae_bytes),
198            digest: digest_from_pae(&pae_bytes),
199            verified_key_ids: verified,
200            payload_type: payload_type.to_string(),
201        }
202    }
203}
204
205/// Convenience: verify an envelope with a single known public key.
206pub fn verify_with_key(
207    envelope: &Envelope,
208    key_id: &str,
209    pub_key: VerifyingKey,
210) -> Result<VerifyResult, VerifyError> {
211    let mut keys = HashMap::new();
212    keys.insert(key_id.to_string(), pub_key);
213    let v = Verifier::new(keys);
214    v.verify_any(envelope)
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::attestation::{sign, Ed25519Signer};
221    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
222    use serde::{Deserialize, Serialize};
223
224    #[derive(Debug, Serialize, Deserialize)]
225    struct TestStmt {
226        actor: String,
227        action: String,
228    }
229
230    const PT: &str = "application/vnd.treeship.action.v1+json";
231
232    fn stmt() -> TestStmt {
233        TestStmt {
234            actor: "agent://researcher".into(),
235            action: "tool.call".into(),
236        }
237    }
238
239    fn make_signer() -> Ed25519Signer {
240        Ed25519Signer::generate("key_test_01").unwrap()
241    }
242
243    // --- round-trip ---
244
245    #[test]
246    fn verify_roundtrip() {
247        let signer = make_signer();
248        let verifier = Verifier::from_signer(&signer);
249        let signed = sign(PT, &stmt(), &signer).unwrap();
250        let result = verifier.verify(&signed.envelope).unwrap();
251
252        assert_eq!(result.artifact_id, signed.artifact_id);
253        assert_eq!(result.digest, signed.digest);
254        assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
255        assert_eq!(result.payload_type, PT);
256    }
257
258    #[test]
259    fn verify_any_roundtrip() {
260        let signer = make_signer();
261        let verifier = Verifier::from_signer(&signer);
262        let signed = sign(PT, &stmt(), &signer).unwrap();
263        verifier.verify_any(&signed.envelope).unwrap();
264    }
265
266    // --- tamper detection ---
267
268    #[test]
269    fn tampered_payload_fails() {
270        let signer = make_signer();
271        let verifier = Verifier::from_signer(&signer);
272        let signed = sign(PT, &stmt(), &signer).unwrap();
273
274        // Replace the payload with different content. The signature was
275        // computed over PAE(original_payload) — after tampering the PAE
276        // is different and the signature fails.
277        let malicious = TestStmt {
278            actor: "agent://attacker".into(),
279            action: "steal".into(),
280        };
281        let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
282
283        let mut tampered = signed.envelope.clone();
284        tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
285
286        let err = verifier.verify(&tampered).unwrap_err();
287        assert!(
288            matches!(err, VerifyError::InvalidSignature(_)),
289            "Expected InvalidSignature, got: {}",
290            err
291        );
292    }
293
294    #[test]
295    fn tampered_payload_type_fails() {
296        let signer = make_signer();
297        let verifier = Verifier::from_signer(&signer);
298        let signed = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
299
300        // Change the payloadType without re-signing.
301        // PAE includes payloadType, so the reconstructed PAE ≠ signed PAE.
302        let mut tampered = signed.envelope.clone();
303        tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
304
305        assert!(
306            verifier.verify(&tampered).is_err(),
307            "verify must fail when payloadType is tampered"
308        );
309    }
310
311    // --- key rejection ---
312
313    #[test]
314    fn wrong_key_fails() {
315        let signer = make_signer();
316        // Build a verifier with a different keypair but the same key_id.
317        // Simulates an attacker substituting their public key.
318        let wrong = Ed25519Signer::generate("key_test_01").unwrap();
319        let verifier = Verifier::from_signer(&wrong);
320
321        let signed = sign(PT, &stmt(), &signer).unwrap();
322        assert!(
323            verifier.verify(&signed.envelope).is_err(),
324            "verify with wrong public key must fail"
325        );
326    }
327
328    #[test]
329    fn unknown_key_fails() {
330        let signer = make_signer();
331        let verifier = Verifier::new(HashMap::new()); // no keys
332
333        let signed = sign(PT, &stmt(), &signer).unwrap();
334        assert!(
335            verifier.verify(&signed.envelope).is_err(),
336            "verify with no trusted keys must fail"
337        );
338    }
339
340    #[test]
341    fn verify_any_skips_unknown_keys() {
342        let signer = make_signer();
343        // Verifier only knows about key_test_01
344        let verifier = Verifier::from_signer(&signer);
345
346        // Envelope only has key_test_01 — verifier should accept it
347        let signed = sign(PT, &stmt(), &signer).unwrap();
348        let result = verifier.verify_any(&signed.envelope).unwrap();
349        assert_eq!(result.verified_key_ids.len(), 1);
350    }
351
352    #[test]
353    fn verify_rejects_empty_signature_envelope() {
354        // P0 #4: an envelope with zero signatures must not verify. Without
355        // the explicit check, the for-loop is a no-op and `verify` returns
356        // `Ok(...)` with an empty `verified_key_ids` list — callers that
357        // only check `Result::is_ok()` would accept unsigned envelopes.
358        let signer = make_signer();
359        let verifier = Verifier::from_signer(&signer);
360        let signed = sign(PT, &stmt(), &signer).unwrap();
361
362        // Strip the signatures off an otherwise-valid envelope.
363        let mut unsigned = signed.envelope.clone();
364        unsigned.signatures.clear();
365
366        let err = verifier.verify(&unsigned).unwrap_err();
367        assert!(
368            matches!(err, VerifyError::NoValidSignature),
369            "expected NoValidSignature for zero-signature envelope, got: {err}"
370        );
371
372        // verify_any already rejects this via its `verified.is_empty()` guard,
373        // but assert it explicitly to keep both paths covered.
374        assert!(matches!(
375            verifier.verify_any(&unsigned).unwrap_err(),
376            VerifyError::NoValidSignature
377        ));
378    }
379
380    #[test]
381    fn verify_any_all_unknown_fails() {
382        let signer = make_signer();
383        let verifier = Verifier::new(HashMap::new());
384        let signed = sign(PT, &stmt(), &signer).unwrap();
385        assert!(matches!(
386            verifier.verify_any(&signed.envelope).unwrap_err(),
387            VerifyError::NoValidSignature
388        ));
389    }
390
391    // --- ID consistency ---
392
393    #[test]
394    fn artifact_id_matches_sign() {
395        let signer = make_signer();
396        let verifier = Verifier::from_signer(&signer);
397        let signed = sign(PT, &stmt(), &signer).unwrap();
398        let verified = verifier.verify(&signed.envelope).unwrap();
399
400        // The ID is derived from the same PAE bytes during both sign and verify.
401        // A mismatch here means the envelope was tampered with between sign and verify.
402        assert_eq!(
403            signed.artifact_id, verified.artifact_id,
404            "ID from sign and verify must match"
405        );
406    }
407
408    // --- multi-key verifier ---
409
410    #[test]
411    fn multi_key_verifier() {
412        let s1 = Ed25519Signer::generate("key_1").unwrap();
413        let s2 = Ed25519Signer::generate("key_2").unwrap();
414
415        let mut verifier = Verifier::from_signer(&s1);
416        verifier.add_key("key_2", s2.verifying_key());
417
418        // Sign with s1 — verifier knows both keys, should accept
419        let signed = sign(PT, &stmt(), &s1).unwrap();
420        let result = verifier.verify(&signed.envelope).unwrap();
421        assert_eq!(result.verified_key_ids, vec!["key_1"]);
422
423        // Sign with s2 — should also work
424        let signed2 = sign(PT, &stmt(), &s2).unwrap();
425        let result2 = verifier.verify(&signed2.envelope).unwrap();
426        assert_eq!(result2.verified_key_ids, vec!["key_2"]);
427    }
428
429    // --- serialization ---
430
431    #[test]
432    fn json_marshal_unmarshal() {
433        let signer = make_signer();
434        let verifier = Verifier::from_signer(&signer);
435        let signed = sign(PT, &stmt(), &signer).unwrap();
436
437        let json = signed.envelope.to_json().unwrap();
438        let restored = Envelope::from_json(&json).unwrap();
439
440        let result = verifier.verify(&restored).unwrap();
441        assert_eq!(result.artifact_id, signed.artifact_id);
442    }
443
444    #[test]
445    fn verifier_uses_strict_ed25519_rejecting_small_order_keys() {
446        // verify_strict rejects small-order public keys (and non-canonical R),
447        // which plain verify accepts. This pins that the core verifier is
448        // strict — the same discipline present.rs already used — so a
449        // malleable/degenerate signature cannot verify on one surface while
450        // failing on another (cross-SDK split-view), and cannot be admitted
451        // via a small-order key.
452        use ed25519_dalek::VerifyingKey;
453        // The canonical Ed25519 small-order point (order 8) — a classic
454        // degenerate public key that verify() accepts and verify_strict()
455        // rejects.
456        let small_order = [
457            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,
458            0, 0, 0, 0, 0,
459        ];
460        // If the bytes even decode to a VerifyingKey, a zero-signature against
461        // it must NOT verify strictly. (Some small-order encodings fail to
462        // decode outright, which is also a rejection — either way, not Ok.)
463        if let Ok(vk) = VerifyingKey::from_bytes(&small_order) {
464            let zero_sig = ed25519_dalek::Signature::from_bytes(&[0u8; 64]);
465            assert!(
466                vk.verify_strict(b"anything", &zero_sig).is_err(),
467                "strict verification must reject a small-order key"
468            );
469        }
470        // And a genuine signature by a real key still verifies through the
471        // envelope verifier (no false negatives from the strict switch).
472        let signer = make_signer();
473        let env = sign(PT, &stmt(), &signer).unwrap().envelope;
474        let mut v = Verifier::new(std::collections::HashMap::new());
475        v.add_key(signer.key_id().to_string(), signer.verifying_key());
476        assert!(
477            v.verify_any(&env).is_ok(),
478            "a real signature must still verify strictly"
479        );
480    }
481}