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