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        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    /// Verifies that at least one signature in the envelope is valid from a
112    /// trusted key. Signatures from unknown keys are skipped.
113    ///
114    /// Use this during key rotation when old and new keys may coexist, or
115    /// when accepting envelopes from multiple possible signers.
116    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, // skip unknown keys
124            };
125            let raw_sig = match self.decode_sig(sig) {
126                Ok(b)  => b,
127                Err(_) => continue, // skip malformed sigs
128            };
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    // --- private helpers ---
142
143    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
192/// Convenience: verify an envelope with a single known public key.
193pub fn verify_with_key(
194    envelope: &Envelope,
195    key_id:   &str,
196    pub_key:  VerifyingKey,
197) -> Result<VerifyResult, VerifyError> {
198    let v = Verifier::from_signer(&Ed25519Signer::from_bytes(
199        key_id,
200        pub_key.as_bytes(),
201    ).map_err(|e| VerifyError::InvalidSignature(e.to_string()))?);
202    v.verify_any(envelope)
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::attestation::{sign, Ed25519Signer};
209    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
210    use serde::{Deserialize, Serialize};
211
212    #[derive(Debug, Serialize, Deserialize)]
213    struct TestStmt { actor: String, action: String }
214
215    const PT: &str = "application/vnd.treeship.action.v1+json";
216
217    fn stmt() -> TestStmt {
218        TestStmt { actor: "agent://researcher".into(), action: "tool.call".into() }
219    }
220
221    fn make_signer() -> Ed25519Signer {
222        Ed25519Signer::generate("key_test_01").unwrap()
223    }
224
225    // --- round-trip ---
226
227    #[test]
228    fn verify_roundtrip() {
229        let signer   = make_signer();
230        let verifier = Verifier::from_signer(&signer);
231        let signed   = sign(PT, &stmt(), &signer).unwrap();
232        let result   = verifier.verify(&signed.envelope).unwrap();
233
234        assert_eq!(result.artifact_id, signed.artifact_id);
235        assert_eq!(result.digest, signed.digest);
236        assert_eq!(result.verified_key_ids, vec!["key_test_01"]);
237        assert_eq!(result.payload_type, PT);
238    }
239
240    #[test]
241    fn verify_any_roundtrip() {
242        let signer   = make_signer();
243        let verifier = Verifier::from_signer(&signer);
244        let signed   = sign(PT, &stmt(), &signer).unwrap();
245        verifier.verify_any(&signed.envelope).unwrap();
246    }
247
248    // --- tamper detection ---
249
250    #[test]
251    fn tampered_payload_fails() {
252        let signer   = make_signer();
253        let verifier = Verifier::from_signer(&signer);
254        let signed   = sign(PT, &stmt(), &signer).unwrap();
255
256        // Replace the payload with different content. The signature was
257        // computed over PAE(original_payload) — after tampering the PAE
258        // is different and the signature fails.
259        let malicious = TestStmt { actor: "agent://attacker".into(), action: "steal".into() };
260        let malicious_bytes = serde_json::to_vec(&malicious).unwrap();
261
262        let mut tampered = signed.envelope.clone();
263        tampered.payload = URL_SAFE_NO_PAD.encode(malicious_bytes);
264
265        let err = verifier.verify(&tampered).unwrap_err();
266        assert!(
267            matches!(err, VerifyError::InvalidSignature(_)),
268            "Expected InvalidSignature, got: {}", err
269        );
270    }
271
272    #[test]
273    fn tampered_payload_type_fails() {
274        let signer   = make_signer();
275        let verifier = Verifier::from_signer(&signer);
276        let signed   = sign("application/vnd.treeship.action.v1+json", &stmt(), &signer).unwrap();
277
278        // Change the payloadType without re-signing.
279        // PAE includes payloadType, so the reconstructed PAE ≠ signed PAE.
280        let mut tampered = signed.envelope.clone();
281        tampered.payload_type = "application/vnd.treeship.approval.v1+json".into();
282
283        assert!(
284            verifier.verify(&tampered).is_err(),
285            "verify must fail when payloadType is tampered"
286        );
287    }
288
289    // --- key rejection ---
290
291    #[test]
292    fn wrong_key_fails() {
293        let signer      = make_signer();
294        // Build a verifier with a different keypair but the same key_id.
295        // Simulates an attacker substituting their public key.
296        let wrong       = Ed25519Signer::generate("key_test_01").unwrap();
297        let verifier    = Verifier::from_signer(&wrong);
298
299        let signed = sign(PT, &stmt(), &signer).unwrap();
300        assert!(
301            verifier.verify(&signed.envelope).is_err(),
302            "verify with wrong public key must fail"
303        );
304    }
305
306    #[test]
307    fn unknown_key_fails() {
308        let signer   = make_signer();
309        let verifier = Verifier::new(HashMap::new()); // no keys
310
311        let signed = sign(PT, &stmt(), &signer).unwrap();
312        assert!(
313            verifier.verify(&signed.envelope).is_err(),
314            "verify with no trusted keys must fail"
315        );
316    }
317
318    #[test]
319    fn verify_any_skips_unknown_keys() {
320        let signer   = make_signer();
321        // Verifier only knows about key_test_01
322        let verifier = Verifier::from_signer(&signer);
323
324        // Envelope only has key_test_01 — verifier should accept it
325        let signed = sign(PT, &stmt(), &signer).unwrap();
326        let result = verifier.verify_any(&signed.envelope).unwrap();
327        assert_eq!(result.verified_key_ids.len(), 1);
328    }
329
330    #[test]
331    fn verify_any_all_unknown_fails() {
332        let signer   = make_signer();
333        let verifier = Verifier::new(HashMap::new());
334        let signed   = sign(PT, &stmt(), &signer).unwrap();
335        assert!(matches!(
336            verifier.verify_any(&signed.envelope).unwrap_err(),
337            VerifyError::NoValidSignature
338        ));
339    }
340
341    // --- ID consistency ---
342
343    #[test]
344    fn artifact_id_matches_sign() {
345        let signer   = make_signer();
346        let verifier = Verifier::from_signer(&signer);
347        let signed   = sign(PT, &stmt(), &signer).unwrap();
348        let verified = verifier.verify(&signed.envelope).unwrap();
349
350        // The ID is derived from the same PAE bytes during both sign and verify.
351        // A mismatch here means the envelope was tampered with between sign and verify.
352        assert_eq!(
353            signed.artifact_id, verified.artifact_id,
354            "ID from sign and verify must match"
355        );
356    }
357
358    // --- multi-key verifier ---
359
360    #[test]
361    fn multi_key_verifier() {
362        let s1 = Ed25519Signer::generate("key_1").unwrap();
363        let s2 = Ed25519Signer::generate("key_2").unwrap();
364
365        let mut verifier = Verifier::from_signer(&s1);
366        verifier.add_key("key_2", s2.verifying_key());
367
368        // Sign with s1 — verifier knows both keys, should accept
369        let signed = sign(PT, &stmt(), &s1).unwrap();
370        let result = verifier.verify(&signed.envelope).unwrap();
371        assert_eq!(result.verified_key_ids, vec!["key_1"]);
372
373        // Sign with s2 — should also work
374        let signed2 = sign(PT, &stmt(), &s2).unwrap();
375        let result2 = verifier.verify(&signed2.envelope).unwrap();
376        assert_eq!(result2.verified_key_ids, vec!["key_2"]);
377    }
378
379    // --- serialization ---
380
381    #[test]
382    fn json_marshal_unmarshal() {
383        let signer   = make_signer();
384        let verifier = Verifier::from_signer(&signer);
385        let signed   = sign(PT, &stmt(), &signer).unwrap();
386
387        let json     = signed.envelope.to_json().unwrap();
388        let restored = Envelope::from_json(&json).unwrap();
389
390        let result = verifier.verify(&restored).unwrap();
391        assert_eq!(result.artifact_id, signed.artifact_id);
392    }
393}