Skip to main content

agent_toolprint/
verify.rs

1//! DSSE envelope verification — implements SPEC §4.
2
3use base64::{engine::general_purpose::STANDARD, Engine};
4use chrono::{DateTime, Utc};
5use ed25519_dalek::{Signature, Verifier, VerifyingKey};
6use serde_json::Value;
7
8use crate::canonical::{canonical, sha256_hash};
9use crate::did_key::Resolver;
10use crate::envelope::{envelope_pae_bytes, envelope_payload_bytes};
11use crate::types::{validate_envelope, validate_receipt};
12
13/// Optional plaintext for the in-line args / response re-hash check.
14#[derive(Default, Debug, Clone)]
15pub struct Plaintext {
16    pub args: Option<Value>,
17    pub response: Option<Value>,
18}
19
20pub struct VerifyOptions<'a> {
21    pub resolver: &'a dyn Resolver,
22    pub now: Option<DateTime<Utc>>,
23    pub max_clock_skew_ms: i64,
24    pub skip_timestamp_check: bool,
25    pub plaintext: Option<Plaintext>,
26}
27
28impl<'a> VerifyOptions<'a> {
29    pub fn new(resolver: &'a dyn Resolver) -> Self {
30        Self {
31            resolver,
32            now: None,
33            max_clock_skew_ms: 24 * 3600 * 1000,
34            skip_timestamp_check: false,
35            plaintext: None,
36        }
37    }
38}
39
40#[derive(Debug)]
41pub struct VerifyResult {
42    pub ok: bool,
43    pub receipt: Option<Value>,
44    pub error: Option<String>,
45}
46
47fn fail(error: impl Into<String>) -> VerifyResult {
48    VerifyResult {
49        ok: false,
50        receipt: None,
51        error: Some(error.into()),
52    }
53}
54
55pub async fn verify(envelope: &Value, opts: &VerifyOptions<'_>) -> VerifyResult {
56    if let Err(e) = validate_envelope(envelope) {
57        return fail(format!("envelope schema: {e}"));
58    }
59    let sigs = envelope["signatures"].as_array().unwrap();
60    if sigs.len() != 2 {
61        return fail(format!(
62            "verify requires exactly 2 signatures, got {}",
63            sigs.len()
64        ));
65    }
66    let kid0 = sigs[0]["keyid"].as_str().unwrap_or("");
67    let kid1 = sigs[1]["keyid"].as_str().unwrap_or("");
68    if kid0 == kid1 {
69        return fail("verify rejects duplicate keyid across signatures");
70    }
71
72    let payload_bytes = match envelope_payload_bytes(envelope) {
73        Ok(b) => b,
74        Err(e) => return fail(format!("payload: {e}")),
75    };
76    let receipt: Value = match serde_json::from_slice(&payload_bytes) {
77        Ok(v) => v,
78        Err(e) => return fail(format!("payload is not valid JSON: {e}")),
79    };
80    if let Err(e) = validate_receipt(&receipt) {
81        return fail(format!("receipt schema: {e}"));
82    }
83
84    let canonical_bytes = match canonical(&receipt) {
85        Ok(b) => b,
86        Err(e) => return fail(format!("canonicalize: {e}")),
87    };
88    if payload_bytes != canonical_bytes {
89        return fail("envelope payload is not JCS-canonical");
90    }
91
92    let agent_kid = receipt["agent"]["key_id"].as_str().unwrap_or("");
93    let tool_kid = receipt["tool"]["key_id"].as_str().unwrap_or("");
94    if kid0 != agent_kid {
95        return fail(format!(
96            "keyid mismatch: signatures[0].keyid ({kid0}) != receipt.agent.key_id ({agent_kid})"
97        ));
98    }
99    if kid1 != tool_kid {
100        return fail(format!(
101            "keyid mismatch: signatures[1].keyid ({kid1}) != receipt.tool.key_id ({tool_kid})"
102        ));
103    }
104
105    let ts_str = receipt["ts"].as_str().unwrap_or("");
106    let rts = match DateTime::parse_from_rfc3339(ts_str) {
107        Ok(t) => t.with_timezone(&Utc),
108        Err(_) => return fail(format!("invalid receipt ts: {ts_str}")),
109    };
110
111    if !opts.skip_timestamp_check {
112        let now = opts.now.unwrap_or_else(Utc::now);
113        let delta_ms = (now - rts).num_milliseconds().abs();
114        if delta_ms > opts.max_clock_skew_ms {
115            return fail(format!(
116                "timestamp window exceeded: |now - ts| = {delta_ms}ms, max = {}ms",
117                opts.max_clock_skew_ms
118            ));
119        }
120    }
121
122    let pae = match envelope_pae_bytes(envelope) {
123        Ok(b) => b,
124        Err(e) => return fail(format!("pae: {e}")),
125    };
126
127    let agent_did = receipt["agent"]["did"].as_str().unwrap_or("");
128    let agent_pk = match opts.resolver.resolve(agent_did, agent_kid, rts).await {
129        Some(pk) => pk,
130        None => return fail(format!("agent DID did not resolve: {agent_did}")),
131    };
132    if !verify_sig(&agent_pk, &pae, sigs[0]["sig"].as_str().unwrap_or("")) {
133        return fail("agent signature invalid");
134    }
135
136    let tool_did = receipt["tool"]["did"].as_str().unwrap_or("");
137    let tool_pk = match opts.resolver.resolve(tool_did, tool_kid, rts).await {
138        Some(pk) => pk,
139        None => return fail(format!("tool DID did not resolve: {tool_did}")),
140    };
141    if !verify_sig(&tool_pk, &pae, sigs[1]["sig"].as_str().unwrap_or("")) {
142        return fail("tool signature invalid");
143    }
144
145    if let Some(pt) = &opts.plaintext {
146        if let Some(args) = &pt.args {
147            let recomputed = match sha256_hash(args) {
148                Ok(h) => h,
149                Err(e) => return fail(format!("args hash: {e}")),
150            };
151            let expected = receipt["call"]["args_hash"].as_str().unwrap_or("");
152            if recomputed != expected {
153                return fail(format!(
154                    "plaintext args_hash mismatch: expected {expected}, got {recomputed}"
155                ));
156            }
157        }
158        if let Some(response) = &pt.response {
159            let recomputed = match sha256_hash(response) {
160                Ok(h) => h,
161                Err(e) => return fail(format!("response hash: {e}")),
162            };
163            let expected = receipt["result"]["response_hash"].as_str().unwrap_or("");
164            if recomputed != expected {
165                return fail(format!(
166                    "plaintext response_hash mismatch: expected {expected}, got {recomputed}"
167                ));
168            }
169        }
170    }
171
172    VerifyResult {
173        ok: true,
174        receipt: Some(receipt),
175        error: None,
176    }
177}
178
179fn verify_sig(pk_bytes: &[u8], message: &[u8], sig_b64: &str) -> bool {
180    if pk_bytes.len() != 32 {
181        return false;
182    }
183    let mut pk_arr = [0u8; 32];
184    pk_arr.copy_from_slice(pk_bytes);
185    let pk = match VerifyingKey::from_bytes(&pk_arr) {
186        Ok(k) => k,
187        Err(_) => return false,
188    };
189    let sig_bytes = match STANDARD.decode(sig_b64) {
190        Ok(b) => b,
191        Err(_) => return false,
192    };
193    if sig_bytes.len() != 64 {
194        return false;
195    }
196    let mut sig_arr = [0u8; 64];
197    sig_arr.copy_from_slice(&sig_bytes);
198    let sig = Signature::from_bytes(&sig_arr);
199    pk.verify(message, &sig).is_ok()
200}