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_strict(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
429    #[test]
430    fn verifier_uses_strict_ed25519_rejecting_small_order_keys() {
431        // verify_strict rejects small-order public keys (and non-canonical R),
432        // which plain verify accepts. This pins that the core verifier is
433        // strict — the same discipline present.rs already used — so a
434        // malleable/degenerate signature cannot verify on one surface while
435        // failing on another (cross-SDK split-view), and cannot be admitted
436        // via a small-order key.
437        use ed25519_dalek::VerifyingKey;
438        // The canonical Ed25519 small-order point (order 8) — a classic
439        // degenerate public key that verify() accepts and verify_strict()
440        // rejects.
441        let small_order = [
442            0x00u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
443            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
444        ];
445        // If the bytes even decode to a VerifyingKey, a zero-signature against
446        // it must NOT verify strictly. (Some small-order encodings fail to
447        // decode outright, which is also a rejection — either way, not Ok.)
448        if let Ok(vk) = VerifyingKey::from_bytes(&small_order) {
449            let zero_sig = ed25519_dalek::Signature::from_bytes(&[0u8; 64]);
450            assert!(
451                vk.verify_strict(b"anything", &zero_sig).is_err(),
452                "strict verification must reject a small-order key"
453            );
454        }
455        // And a genuine signature by a real key still verifies through the
456        // envelope verifier (no false negatives from the strict switch).
457        let signer = make_signer();
458        let env = sign(PT, &stmt(), &signer).unwrap().envelope;
459        let mut v = Verifier::new(std::collections::HashMap::new());
460        v.add_key(signer.key_id().to_string(), signer.verifying_key());
461        assert!(v.verify_any(&env).is_ok(), "a real signature must still verify strictly");
462    }
463
464}