1use base64::Engine as _;
14use base64::engine::general_purpose::STANDARD as B64;
15use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
16use rand::rngs::OsRng;
17use serde_json::Value;
18use sha2::{Digest, Sha256};
19use std::collections::BTreeMap;
20use std::ops::Range;
21use thiserror::Error;
22
23use crate::canonical::canonical;
24
25pub const EVENT_SCHEMA_VERSION: &str = "v3.1";
35
36pub fn schema_major(schema_version: &str) -> &str {
43 schema_version.split('.').next().unwrap_or(schema_version)
44}
45
46pub static KIND_RANGES: &[(KindClass, Range<u32>)] = &[
53 (KindClass::Regular, 1000..10000),
54 (KindClass::Replaceable, 10000..20000),
55 (KindClass::Ephemeral, 20000..30000),
56 (KindClass::Addressable, 30000..40000),
57];
58
59pub fn kinds() -> &'static [(u32, &'static str)] {
61 &[
62 (1, "decision"), (100, "heartbeat"), (1000, "decision"),
65 (1001, "claim"),
66 (1002, "ack"),
67 (1100, "agent_card"),
68 (1101, "trust_add_key"),
69 (1102, "trust_revoke_key"),
70 (1200, "wire_open"),
71 (1201, "wire_close"),
72 ]
73}
74
75pub fn kinds_map() -> BTreeMap<u32, &'static str> {
78 kinds().iter().copied().collect()
79}
80
81#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
82pub enum KindClass {
83 Regular,
84 Replaceable,
85 Ephemeral,
86 Addressable,
87}
88
89impl KindClass {
90 pub fn as_str(self) -> &'static str {
91 match self {
92 KindClass::Regular => "regular",
93 KindClass::Replaceable => "replaceable",
94 KindClass::Ephemeral => "ephemeral",
95 KindClass::Addressable => "addressable",
96 }
97 }
98}
99
100pub fn kind_class(kind: u32) -> Option<KindClass> {
102 match kind {
105 1 => return Some(KindClass::Regular),
106 100 => return Some(KindClass::Ephemeral),
107 _ => {}
108 }
109 for (cls, range) in KIND_RANGES {
110 if range.contains(&kind) {
111 return Some(*cls);
112 }
113 }
114 None
115}
116
117pub fn canonical_event(value: &Value, strict: bool) -> Vec<u8> {
120 canonical(value, strict)
121}
122
123pub use crate::canonical::canonical as canonical_value;
125
126pub fn compute_event_id(msg: &Value) -> String {
129 let bytes = canonical(msg, true);
130 let digest = Sha256::digest(&bytes);
131 hex::encode(digest)
132}
133
134pub fn fingerprint(public_key: &[u8]) -> String {
137 let digest = Sha256::digest(public_key);
138 hex::encode(&digest[..4])
139}
140
141pub fn make_key_id(handle: &str, public_key: &[u8]) -> String {
142 format!("{handle}:{}", fingerprint(public_key))
143}
144
145pub fn b64encode(bytes: &[u8]) -> String {
148 B64.encode(bytes)
149}
150
151pub fn b64decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
152 B64.decode(s)
153}
154
155pub fn generate_keypair() -> ([u8; 32], [u8; 32]) {
159 let sk = SigningKey::generate(&mut OsRng);
160 let pk = sk.verifying_key();
161 (sk.to_bytes(), pk.to_bytes())
162}
163
164#[derive(Debug, Error)]
167pub enum SignError {
168 #[error("private key must be 32 bytes, got {0}")]
169 BadPrivateLen(usize),
170 #[error("public key must be 32 bytes, got {0}")]
171 BadPublicLen(usize),
172}
173
174#[derive(Debug, Error)]
175pub enum VerifyError {
176 #[error("missing field: {0}")]
177 MissingField(&'static str),
178 #[error("event_id mismatch — body was tampered after signing")]
179 EventIdMismatch,
180 #[error("signer {0:?} not in trust")]
181 UnknownAgent(String),
182 #[error("key {0:?} not found for agent {1:?}")]
183 UnknownKey(String, String),
184 #[error("key {0:?} for agent {1:?} is deactivated")]
185 DeactivatedKey(String, String),
186 #[error("signature decode failed")]
187 BadSignature,
188 #[error("signature did not verify")]
189 SignatureRejected,
190}
191
192pub fn sign_message_v31(
195 msg: &Value,
196 private_key: &[u8],
197 public_key: &[u8],
198 agent: &str,
199) -> Result<Value, SignError> {
200 if private_key.len() != 32 {
201 return Err(SignError::BadPrivateLen(private_key.len()));
202 }
203 if public_key.len() != 32 {
204 return Err(SignError::BadPublicLen(public_key.len()));
205 }
206 let mut sk_bytes = [0u8; 32];
207 sk_bytes.copy_from_slice(private_key);
208 let sk = SigningKey::from_bytes(&sk_bytes);
209
210 let event_id = compute_event_id(msg);
211 let raw = hex::decode(&event_id).expect("compute_event_id always returns valid hex");
212 let sig = sk.sign(&raw);
213
214 let mut out = msg.as_object().cloned().unwrap_or_default();
215 out.insert("event_id".into(), Value::String(event_id));
216 out.insert(
217 "public_key_id".into(),
218 Value::String(make_key_id(agent, public_key)),
219 );
220 out.insert(
221 "signature".into(),
222 Value::String(b64encode(&sig.to_bytes())),
223 );
224 Ok(Value::Object(out))
225}
226
227pub fn verify_message_v31(msg: &Value, trust: &Value) -> Result<(), VerifyError> {
232 let from = msg
233 .get("from")
234 .and_then(Value::as_str)
235 .ok_or(VerifyError::MissingField("from"))?;
236 let handle = crate::agent_card::display_handle_from_did(from);
240
241 let public_key_id = msg
242 .get("public_key_id")
243 .and_then(Value::as_str)
244 .ok_or(VerifyError::MissingField("public_key_id"))?;
245
246 let signature_b64 = msg
247 .get("signature")
248 .and_then(Value::as_str)
249 .ok_or(VerifyError::MissingField("signature"))?;
250
251 let event_id = msg
252 .get("event_id")
253 .and_then(Value::as_str)
254 .ok_or(VerifyError::MissingField("event_id"))?;
255
256 let recomputed = compute_event_id(msg);
257 if recomputed != event_id {
258 return Err(VerifyError::EventIdMismatch);
259 }
260
261 let agent = trust
262 .get("agents")
263 .and_then(|a| a.get(handle))
264 .ok_or_else(|| VerifyError::UnknownAgent(handle.to_string()))?;
265
266 let public_keys = agent
267 .get("public_keys")
268 .and_then(Value::as_array)
269 .ok_or_else(|| VerifyError::UnknownKey(public_key_id.to_string(), handle.to_string()))?;
270
271 let key_record = public_keys
272 .iter()
273 .find(|k| k.get("key_id").and_then(Value::as_str) == Some(public_key_id))
274 .ok_or_else(|| VerifyError::UnknownKey(public_key_id.to_string(), handle.to_string()))?;
275
276 let active = key_record
277 .get("active")
278 .and_then(Value::as_bool)
279 .unwrap_or(true);
280 if !active {
281 return Err(VerifyError::DeactivatedKey(
282 public_key_id.to_string(),
283 handle.to_string(),
284 ));
285 }
286
287 let pk_b64 = key_record
288 .get("key")
289 .and_then(Value::as_str)
290 .ok_or(VerifyError::MissingField("key"))?;
291 let pk_bytes = b64decode(pk_b64).map_err(|_| VerifyError::BadSignature)?;
292 if pk_bytes.len() != 32 {
293 return Err(VerifyError::BadSignature);
294 }
295 let mut pk_arr = [0u8; 32];
296 pk_arr.copy_from_slice(&pk_bytes);
297 let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| VerifyError::BadSignature)?;
298
299 let sig_bytes = b64decode(signature_b64).map_err(|_| VerifyError::BadSignature)?;
300 if sig_bytes.len() != 64 {
301 return Err(VerifyError::BadSignature);
302 }
303 let mut sig_arr = [0u8; 64];
304 sig_arr.copy_from_slice(&sig_bytes);
305 let sig = Signature::from_bytes(&sig_arr);
306
307 let raw = hex::decode(event_id).map_err(|_| VerifyError::BadSignature)?;
308 vk.verify(&raw, &sig)
309 .map_err(|_| VerifyError::SignatureRejected)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use serde_json::json;
316
317 fn trust_for(handle: &str, pub_key: &[u8]) -> Value {
318 let kid = make_key_id(handle, pub_key);
319 json!({
320 "agents": {
321 handle: {
322 "public_keys": [
323 {"key_id": kid, "key": b64encode(pub_key), "active": true}
324 ]
325 }
326 }
327 })
328 }
329
330 #[test]
331 fn kind_ranges_disjoint() {
332 let mut seen = std::collections::HashSet::new();
333 for (_, rng) in KIND_RANGES {
334 for k in rng.clone() {
335 assert!(seen.insert(k), "kind {k} in multiple ranges");
336 }
337 }
338 }
339
340 #[test]
341 fn kind_class_known_ranges() {
342 assert_eq!(kind_class(20000), Some(KindClass::Ephemeral));
343 assert_eq!(kind_class(29999), Some(KindClass::Ephemeral));
344 assert_eq!(kind_class(1000), Some(KindClass::Regular));
345 assert_eq!(kind_class(9999), Some(KindClass::Regular));
346 assert_eq!(kind_class(10000), Some(KindClass::Replaceable));
347 assert_eq!(kind_class(19999), Some(KindClass::Replaceable));
348 assert_eq!(kind_class(30000), Some(KindClass::Addressable));
349 }
350
351 #[test]
352 fn kind_class_special_cases() {
353 assert_eq!(kind_class(1), Some(KindClass::Regular));
354 assert_eq!(kind_class(100), Some(KindClass::Ephemeral));
355 }
356
357 #[test]
358 fn kind_class_unknown_returns_none() {
359 assert_eq!(kind_class(99999), None);
360 assert_eq!(kind_class(7), None);
361 }
362
363 #[test]
364 fn v01_does_not_ship_v02_kinds() {
365 let names = kinds_map();
366 for deferred in [1900, 1901, 10500] {
367 assert!(
368 !names.contains_key(&deferred),
369 "v0.2+ kind {deferred} leaked into v0.1"
370 );
371 }
372 }
373
374 #[test]
375 fn fingerprint_is_8_hex() {
376 let fp = fingerprint(&[0u8; 32]);
377 assert_eq!(fp.len(), 8);
378 u32::from_str_radix(&fp, 16).expect("hex");
379 }
380
381 #[test]
382 fn make_key_id_format() {
383 let (_, pk) = generate_keypair();
384 let kid = make_key_id("paul", &pk);
385 assert!(kid.starts_with("paul:"));
386 assert_eq!(kid.split(':').nth(1).unwrap().len(), 8);
387 }
388
389 #[test]
390 fn generate_keypair_returns_32_byte_pair() {
391 let (sk, pk) = generate_keypair();
392 assert_eq!(sk.len(), 32);
393 assert_eq!(pk.len(), 32);
394 }
395
396 #[test]
397 fn sign_verify_roundtrip() {
398 let (sk, pk) = generate_keypair();
399 let msg = json!({
400 "timestamp": "2026-05-09T00:00:00Z",
401 "from": "paul",
402 "type": "decision",
403 "kind": 1,
404 "subject": "test",
405 "body": {"content": "hello"},
406 });
407 let signed = sign_message_v31(&msg, &sk, &pk, "paul").unwrap();
408 assert!(signed.get("event_id").is_some());
409 assert!(signed.get("public_key_id").is_some());
410 assert!(signed.get("signature").is_some());
411 verify_message_v31(&signed, &trust_for("paul", &pk)).unwrap();
412 }
413
414 #[test]
415 fn verify_rejects_tampered_body() {
416 let (sk, pk) = generate_keypair();
417 let msg = json!({"from": "paul", "type": "decision", "body": {"content": "original"}});
418 let mut signed = sign_message_v31(&msg, &sk, &pk, "paul").unwrap();
419 signed["body"]["content"] = json!("tampered");
420 let err = verify_message_v31(&signed, &trust_for("paul", &pk)).unwrap_err();
421 assert!(matches!(err, VerifyError::EventIdMismatch));
422 }
423
424 #[test]
425 fn verify_accepts_did_wire_prefix_in_from() {
426 let (sk, pk) = generate_keypair();
427 let msg = json!({"from": "did:wire:paul", "type": "decision", "body": {}});
428 let signed = sign_message_v31(&msg, &sk, &pk, "paul").unwrap();
429 verify_message_v31(&signed, &trust_for("paul", &pk)).unwrap();
430 }
431
432 #[test]
433 fn verify_rejects_unknown_agent() {
434 let (sk, pk) = generate_keypair();
435 let msg = json!({"from": "paul", "type": "decision", "body": {}});
436 let signed = sign_message_v31(&msg, &sk, &pk, "paul").unwrap();
437 let trust = json!({"agents": {"willard": {"public_keys": []}}});
438 let err = verify_message_v31(&signed, &trust).unwrap_err();
439 assert!(matches!(err, VerifyError::UnknownAgent(h) if h == "paul"));
440 }
441
442 #[test]
443 fn verify_rejects_inactive_key() {
444 let (sk, pk) = generate_keypair();
445 let msg = json!({"from": "paul", "type": "decision", "body": {}});
446 let signed = sign_message_v31(&msg, &sk, &pk, "paul").unwrap();
447 let mut trust = trust_for("paul", &pk);
448 trust["agents"]["paul"]["public_keys"][0]["active"] = json!(false);
449 let err = verify_message_v31(&signed, &trust).unwrap_err();
450 assert!(matches!(err, VerifyError::DeactivatedKey(_, _)));
451 }
452
453 #[test]
454 fn compute_event_id_is_64_hex() {
455 let v = json!({"from": "paul", "type": "test"});
456 let eid = compute_event_id(&v);
457 assert_eq!(eid.len(), 64);
458 for c in eid.chars() {
459 assert!(c.is_ascii_hexdigit());
460 }
461 }
462}