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