Skip to main content

wire/
signing.rs

1//! Ed25519 sign-over-event_id (Nostr NIP-01 style).
2//!
3//! Sign flow:
4//!   1. Compute SHA-256 over canonical bytes of `msg` (strict: drops event_id).
5//!   2. That 32-byte digest IS the `event_id` (hex-encoded for transport).
6//!   3. Sign the raw 32-byte digest. The signature commits to event_id, which
7//!      transitively commits to the canonical body — tamper anything, the
8//!      digest changes, the signature fails.
9//!
10//! Why sign the id and not the body: lets relays/index layers cite events by
11//! id without re-canonicalizing every body. Same property Nostr exploits.
12
13use 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
25// ---------- event schema version ----------
26
27/// Schema version tag stamped on every signed event by 0.5.11+. Pull-side
28/// verification rejects events whose schema_version's *major* component
29/// disagrees with this — see `pull::process_events`.
30///
31/// Legacy events without this field are accepted (we can't retroactively
32/// stamp 0.5.10 traffic), so the field's *absence* is fine; only its
33/// *presence with a wrong major* is a hard reject.
34pub const EVENT_SCHEMA_VERSION: &str = "v3.1";
35
36/// Major component of a `v<major>.<minor>` schema_version. Used to decide
37/// whether a received event is wire-compatible.
38///
39/// Today: every Wire schema is v3.x; major == "v3". A 0.5.12 binary might
40/// start emitting v4.0 events; older 0.5.x binaries see major=v4 and bail
41/// instead of attempting to decode an incompatible shape.
42pub fn schema_major(schema_version: &str) -> &str {
43    schema_version
44        .split('.')
45        .next()
46        .unwrap_or(schema_version)
47}
48
49// ---------- kind ranges ----------
50
51/// Disjoint kind-id ranges. Mirrors v3 protocol; v0.1 ships a strict subset.
52///
53/// v0.2+ kinds (file_share=1900, file_revoke=1901, registry_revocation=10500)
54/// are deliberately ABSENT — see ANTI_FEATURES.md.
55pub 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
62/// v0.1 named kinds. Anything not here is unknown to this version.
63pub fn kinds() -> &'static [(u32, &'static str)] {
64    &[
65        (1, "decision"),    // Nostr-compat short text — special-cased to Regular
66        (100, "heartbeat"), // ephemeral liveness ping — special-cased to Ephemeral
67        (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
78/// `kinds()` as a `BTreeMap` for membership tests. Allocated per call —
79/// callers that need it hot should cache.
80pub 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
103/// Classify a kind id. `None` means unknown — caller decides how to handle.
104pub fn kind_class(kind: u32) -> Option<KindClass> {
105    // Documented out-of-range special cases (Nostr NIP-01 compatibility +
106    // v3 heartbeat carve-out). Keep these explicit, not a hidden lookup.
107    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
120// ---------- canonical re-export (keeps call sites symmetric with Python) ----------
121
122pub fn canonical_event(value: &Value, strict: bool) -> Vec<u8> {
123    canonical(value, strict)
124}
125
126// Public alias matching Python `signing.canonical(...)` import path.
127pub use crate::canonical::canonical as canonical_value;
128
129// ---------- event_id ----------
130
131pub 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
137// ---------- key id + fingerprint ----------
138
139pub 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
148// ---------- base64 helpers ----------
149
150pub 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
158// ---------- key generation ----------
159
160/// Returns `(private_key_bytes, public_key_bytes)` — both 32 bytes, raw.
161pub 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// ---------- sign / verify ----------
168
169#[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
195/// Sign a message. Returns the canonical wire form: original fields + the
196/// computed `event_id`, `public_key_id`, `signature`.
197pub 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
230/// Verify a signed message against a trust dict (see `trust` module).
231///
232/// Returns `Ok(())` iff: event_id matches recomputed, signer's key is in
233/// trust + active, and the Ed25519 signature validates over the event_id.
234pub 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    // v0.5.7+: DID may include a `-<8-hex>` pubkey suffix
240    // (`did:wire:paul-abc12345`). Trust map is keyed by the bare handle,
241    // so strip both the `did:wire:` prefix AND the optional pubkey suffix.
242    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)] // kept for v0.6 — once a caller exists, drop the allow.
316fn 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}