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