1use once_cell::sync::Lazy;
4use regex::Regex;
5use serde_json::Value;
6
7use crate::error::Error;
8
9pub const PROTOCOL_VERSION: &str = "tp/0.1";
10pub const PAYLOAD_TYPE: &str = "application/vnd.agent-toolprint+json";
11
12static HASH_HEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^sha256:[0-9a-f]{64}$").unwrap());
13static BASE64: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[A-Za-z0-9+/]*={0,2}$").unwrap());
14static NONCE32: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[A-Za-z0-9+/]{43}=$").unwrap());
15static DID_KEY: Lazy<Regex> =
16 Lazy::new(|| Regex::new(r"^did:key:z[1-9A-HJ-NP-Za-km-z]+$").unwrap());
17static UUID_RE: Lazy<Regex> = Lazy::new(|| {
18 Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap()
19});
20static RFC3339: Lazy<Regex> = Lazy::new(|| {
21 Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$").unwrap()
22});
23
24const RECEIPT_ALLOWED: &[&str] = &[
25 "v", "id", "ts", "agent", "tool", "call", "result", "nonce", "parent",
26];
27const RECEIPT_REQUIRED: &[&str] = &["v", "id", "ts", "agent", "tool", "call", "result", "nonce"];
28const PARTY_ALLOWED: &[&str] = &["did", "key_id"];
29const CALL_ALLOWED: &[&str] = &["name", "args_hash"];
30const RESULT_ALLOWED: &[&str] = &["status", "response_hash"];
31const ENVELOPE_ALLOWED: &[&str] = &["payloadType", "payload", "signatures"];
32const SIG_ALLOWED: &[&str] = &["keyid", "sig"];
33
34fn ensure_object<'a>(
35 value: &'a Value,
36 where_: &str,
37) -> Result<&'a serde_json::Map<String, Value>, Error> {
38 value
39 .as_object()
40 .ok_or_else(|| Error::Invalid(format!("{where_}: expected object")))
41}
42
43fn ensure_string<'a>(value: &'a Value, where_: &str) -> Result<&'a str, Error> {
44 value
45 .as_str()
46 .ok_or_else(|| Error::Invalid(format!("{where_}: expected string")))
47}
48
49fn check_pattern(s: &str, re: &Regex, where_: &str) -> Result<(), Error> {
50 if re.is_match(s) {
51 Ok(())
52 } else {
53 Err(Error::Invalid(format!(
54 "{where_}: does not match required pattern"
55 )))
56 }
57}
58
59fn check_keys(
60 obj: &serde_json::Map<String, Value>,
61 allowed: &[&str],
62 where_: &str,
63) -> Result<(), Error> {
64 for k in obj.keys() {
65 if !allowed.contains(&k.as_str()) {
66 return Err(Error::Invalid(format!("{where_}: unknown key {k:?}")));
67 }
68 }
69 Ok(())
70}
71
72fn validate_party(value: &Value, where_: &str) -> Result<(), Error> {
73 let obj = ensure_object(value, where_)?;
74 check_keys(obj, PARTY_ALLOWED, where_)?;
75 let did = obj
76 .get("did")
77 .ok_or_else(|| Error::Invalid(format!("{where_}: missing 'did'")))?;
78 let kid = obj
79 .get("key_id")
80 .ok_or_else(|| Error::Invalid(format!("{where_}: missing 'key_id'")))?;
81 let did_s = ensure_string(did, &format!("{where_}.did"))?;
82 check_pattern(did_s, &DID_KEY, &format!("{where_}.did"))?;
83 let kid_s = ensure_string(kid, &format!("{where_}.key_id"))?;
84 if kid_s.is_empty() {
85 return Err(Error::Invalid(format!(
86 "{where_}.key_id: must be non-empty"
87 )));
88 }
89 Ok(())
90}
91
92pub fn validate_receipt(value: &Value) -> Result<(), Error> {
93 let obj = ensure_object(value, "receipt")?;
94 check_keys(obj, RECEIPT_ALLOWED, "receipt")?;
95 for req in RECEIPT_REQUIRED {
96 if !obj.contains_key(*req) {
97 return Err(Error::Invalid(format!(
98 "receipt: missing required key {req:?}"
99 )));
100 }
101 }
102 if obj["v"].as_str() != Some(PROTOCOL_VERSION) {
103 return Err(Error::Invalid(format!(
104 "receipt.v: expected {PROTOCOL_VERSION:?}, got {:?}",
105 obj["v"]
106 )));
107 }
108 let id = ensure_string(&obj["id"], "receipt.id")?;
109 check_pattern(id, &UUID_RE, "receipt.id")?;
110 let ts = ensure_string(&obj["ts"], "receipt.ts")?;
111 check_pattern(ts, &RFC3339, "receipt.ts")?;
112 validate_party(&obj["agent"], "receipt.agent")?;
113 validate_party(&obj["tool"], "receipt.tool")?;
114
115 let call = ensure_object(&obj["call"], "receipt.call")?;
116 check_keys(call, CALL_ALLOWED, "receipt.call")?;
117 let name = ensure_string(
118 call.get("name")
119 .ok_or_else(|| Error::Invalid("receipt.call: missing 'name'".into()))?,
120 "receipt.call.name",
121 )?;
122 if name.is_empty() {
123 return Err(Error::Invalid(
124 "receipt.call.name: must be non-empty".into(),
125 ));
126 }
127 let args_hash = ensure_string(
128 call.get("args_hash")
129 .ok_or_else(|| Error::Invalid("receipt.call: missing 'args_hash'".into()))?,
130 "receipt.call.args_hash",
131 )?;
132 check_pattern(args_hash, &HASH_HEX, "receipt.call.args_hash")?;
133
134 let result = ensure_object(&obj["result"], "receipt.result")?;
135 check_keys(result, RESULT_ALLOWED, "receipt.result")?;
136 let status = ensure_string(
137 result
138 .get("status")
139 .ok_or_else(|| Error::Invalid("receipt.result: missing 'status'".into()))?,
140 "receipt.result.status",
141 )?;
142 if status != "ok" && status != "error" {
143 return Err(Error::Invalid(format!(
144 "receipt.result.status: must be 'ok' or 'error', got {status:?}"
145 )));
146 }
147 let resp_hash = ensure_string(
148 result
149 .get("response_hash")
150 .ok_or_else(|| Error::Invalid("receipt.result: missing 'response_hash'".into()))?,
151 "receipt.result.response_hash",
152 )?;
153 check_pattern(resp_hash, &HASH_HEX, "receipt.result.response_hash")?;
154
155 let nonce = ensure_string(&obj["nonce"], "receipt.nonce")?;
156 check_pattern(nonce, &NONCE32, "receipt.nonce")?;
157
158 if let Some(parent) = obj.get("parent") {
159 let p = ensure_string(parent, "receipt.parent")?;
160 check_pattern(p, &UUID_RE, "receipt.parent")?;
161 }
162 Ok(())
163}
164
165pub fn validate_envelope(value: &Value) -> Result<(), Error> {
166 let obj = ensure_object(value, "envelope")?;
167 check_keys(obj, ENVELOPE_ALLOWED, "envelope")?;
168 for req in ENVELOPE_ALLOWED {
169 if !obj.contains_key(*req) {
170 return Err(Error::Invalid(format!("envelope: missing key {req:?}")));
171 }
172 }
173 if obj["payloadType"].as_str() != Some(PAYLOAD_TYPE) {
174 return Err(Error::Invalid(format!(
175 "envelope.payloadType: expected {PAYLOAD_TYPE:?}, got {:?}",
176 obj["payloadType"]
177 )));
178 }
179 let payload = ensure_string(&obj["payload"], "envelope.payload")?;
180 check_pattern(payload, &BASE64, "envelope.payload")?;
181
182 let sigs = obj["signatures"]
183 .as_array()
184 .ok_or_else(|| Error::Invalid("envelope.signatures: expected array".into()))?;
185 if sigs.is_empty() || sigs.len() > 2 {
186 return Err(Error::Invalid(format!(
187 "envelope.signatures: must have 1 or 2 entries, got {}",
188 sigs.len()
189 )));
190 }
191 for (i, s) in sigs.iter().enumerate() {
192 let where_ = format!("envelope.signatures[{i}]");
193 let so = ensure_object(s, &where_)?;
194 check_keys(so, SIG_ALLOWED, &where_)?;
195 let kid = ensure_string(
196 so.get("keyid")
197 .ok_or_else(|| Error::Invalid(format!("{where_}: missing 'keyid'")))?,
198 &format!("{where_}.keyid"),
199 )?;
200 if kid.is_empty() {
201 return Err(Error::Invalid(format!("{where_}.keyid: must be non-empty")));
202 }
203 let sig = ensure_string(
204 so.get("sig")
205 .ok_or_else(|| Error::Invalid(format!("{where_}: missing 'sig'")))?,
206 &format!("{where_}.sig"),
207 )?;
208 check_pattern(sig, &BASE64, &format!("{where_}.sig"))?;
209 }
210 Ok(())
211}