lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit e4b9314926c41d3d2d34289689ff4bfeba81cdb5
parent 757f3e32102b310bb0f6942fd7ecd2ac745198c4
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 21:14:15 +0000

event: restrict unchecked signed event construction

- hide unchecked signed-event construction behind event crate tests only
- keep draft validation on semantic mismatches before event-id comparison
- update authority and transport tests to use verified signed-event fixtures
- add a source-boundary guard against public unchecked construction

Diffstat:
MCargo.lock | 1+
Mcrates/authority/Cargo.toml | 3+++
Mcrates/authority/src/authorization.rs | 61+++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/authority/src/signer.rs | 43++++++++++++++++++++++++++++---------------
Mcrates/event/src/draft.rs | 15++++++++-------
Mcrates/event/tests/source_boundary.rs | 4++++
Mcrates/runtime/src/transport.rs | 8--------
Mcrates/transport_nostr/src/publish.rs | 27++++++++++++---------------
Mcrates/transport_nostr/tests/transport.rs | 7-------
9 files changed, 97 insertions(+), 72 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4268,6 +4268,7 @@ version = "0.1.0-alpha.2" dependencies = [ "radroots_event", "radroots_nostr", + "serde_json", ] [[package]] diff --git a/crates/authority/Cargo.toml b/crates/authority/Cargo.toml @@ -24,5 +24,8 @@ local_signer = [ radroots_event = { workspace = true, default-features = false } radroots_nostr = { workspace = true, optional = true, default-features = false } +[dev-dependencies] +serde_json = { workspace = true, default-features = false, features = ["alloc"] } + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs @@ -259,7 +259,17 @@ mod tests { sig: hex_128('f'), extra: Default::default(), }; - RadrootsSignedEvent::from_wire_unchecked(wire, "{}").map_err(|error| { + let mut wire = wire; + if self.overrides.event_id.is_none() { + wire.id = wire + .computed_event_id() + .map_err(|error| RadrootsSignerError::SigningFailed { + message: error.to_string(), + })? + .into_string(); + } + let raw_json = raw_json_for_wire(&wire); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| { RadrootsSignerError::SigningFailed { message: error.to_string(), } @@ -269,7 +279,6 @@ mod tests { fn signed_event_from_draft(draft: &RadrootsEventDraft) -> RadrootsSignedEvent { signed_event_from_parts( - draft.expected_event_id_str().to_owned(), draft.expected_pubkey_str().to_owned(), draft.created_at_u64(), draft.kind_u32(), @@ -279,27 +288,38 @@ mod tests { } fn signed_event_from_parts( - id: String, pubkey: String, created_at: u64, kind: u32, tags: Vec<Vec<String>>, content: String, ) -> RadrootsSignedEvent { - RadrootsSignedEvent::from_wire_unchecked( - RadrootsNip01EventWire { - id, - pubkey, - created_at, - kind, - tags, - content, - sig: hex_128('f'), - extra: Default::default(), - }, - "{}", - ) - .expect("signed event") + let mut wire = RadrootsNip01EventWire { + id: String::new(), + pubkey, + created_at, + kind, + tags, + content, + sig: hex_128('f'), + extra: Default::default(), + }; + wire.id = wire.computed_event_id().expect("event id").into_string(); + let raw_json = raw_json_for_wire(&wire); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") + } + + fn raw_json_for_wire(wire: &RadrootsNip01EventWire) -> String { + serde_json::json!({ + "id": wire.id, + "pubkey": wire.pubkey, + "created_at": wire.created_at, + "kind": wire.kind, + "tags": wire.tags, + "content": wire.content, + "sig": wire.sig, + }) + .to_string() } #[test] @@ -367,7 +387,7 @@ mod tests { } #[test] - fn signed_event_id_mismatch_fails() { + fn signer_explicit_id_mismatch_fails_before_authorized_signing() { let pubkey = hex_64('a'); let draft = listing_draft(pubkey.as_str()); let actor = seller_actor(pubkey.as_str()); @@ -375,7 +395,9 @@ mod tests { assert!(matches!( sign_authorized_draft(&actor, &signer, &draft), - Err(RadrootsAuthorityError::SignedEventIdMismatch { .. }) + Err(RadrootsAuthorityError::Signer( + RadrootsSignerError::SigningFailed { .. } + )) )); } @@ -486,7 +508,6 @@ mod tests { let pubkey = hex_64('a'); let draft = listing_draft(pubkey.as_str()); let signed = signed_event_from_parts( - draft.expected_event_id_str().to_owned(), hex_64('b'), draft.created_at_u64(), draft.kind_u32(), diff --git a/crates/authority/src/signer.rs b/crates/authority/src/signer.rs @@ -114,25 +114,38 @@ mod tests { .as_deref() .unwrap_or(draft.expected_event_id_str()) .to_owned(); - RadrootsSignedEvent::from_wire_unchecked( - RadrootsNip01EventWire { - id, - pubkey: self.pubkey.to_string(), - created_at: draft.created_at_u64(), - kind: draft.kind_u32(), - tags: draft.tags_as_vec(), - content: draft.content().to_owned(), - sig: hex_128('f'), - extra: Default::default(), - }, - "{}", - ) - .map_err(|error| RadrootsSignerError::SigningFailed { - message: error.to_string(), + let wire = RadrootsNip01EventWire { + id, + pubkey: self.pubkey.to_string(), + created_at: draft.created_at_u64(), + kind: draft.kind_u32(), + tags: draft.tags_as_vec(), + content: draft.content().to_owned(), + sig: hex_128('f'), + extra: Default::default(), + }; + let raw_json = raw_json_for_wire(&wire); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| { + RadrootsSignerError::SigningFailed { + message: error.to_string(), + } }) } } + fn raw_json_for_wire(wire: &RadrootsNip01EventWire) -> String { + serde_json::json!({ + "id": wire.id, + "pubkey": wire.pubkey, + "created_at": wire.created_at, + "kind": wire.kind, + "tags": wire.tags, + "content": wire.content, + "sig": wire.sig, + }) + .to_string() + } + #[test] fn mock_signer_reports_public_key() { let pubkey = hex_64('a'); diff --git a/crates/event/src/draft.rs b/crates/event/src/draft.rs @@ -489,7 +489,8 @@ impl RadrootsSignedEvent { }) } - pub fn from_wire_unchecked( + #[cfg(test)] + fn from_wire_unchecked( wire: RadrootsNip01EventWire, raw_json: impl Into<String>, ) -> Result<Self, RadrootsSignedEventError> { @@ -600,12 +601,6 @@ pub fn validate_signed_nostr_event_matches_draft( actual_pubkey: signed_event.pubkey_str().to_owned(), }); } - if signed_event.id_str() != draft.expected_event_id_str() { - return Err(RadrootsDraftError::SignedEventIdMismatch { - expected_event_id: draft.expected_event_id_str().to_owned(), - actual_event_id: signed_event.id_str().to_owned(), - }); - } if signed_event.created_at() != draft.created_at_u64() { return Err(RadrootsDraftError::SignedEventCreatedAtMismatch { expected_created_at: draft.created_at_u64(), @@ -632,6 +627,12 @@ pub fn validate_signed_nostr_event_matches_draft( actual_len: signed_event.content().len(), }); } + if signed_event.id_str() != draft.expected_event_id_str() { + return Err(RadrootsDraftError::SignedEventIdMismatch { + expected_event_id: draft.expected_event_id_str().to_owned(), + actual_event_id: signed_event.id_str().to_owned(), + }); + } let computed_event_id = compute_nip01_event_id( signed_event.pubkey_str(), draft.created_at_u64(), diff --git a/crates/event/tests/source_boundary.rs b/crates/event/tests/source_boundary.rs @@ -8,6 +8,10 @@ struct ForbiddenEventName { const FORBIDDEN_EVENT_NAMES: &[ForbiddenEventName] = &[ ForbiddenEventName { + pattern: "pub fn from_wire_unchecked", + reason: "unchecked signed-event construction must not be public API", + }, + ForbiddenEventName { pattern: "WireEventParts", reason: "event construction must use RadrootsNip01EventWireParts", }, diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -824,14 +824,6 @@ mod tests { assert_eq!(payload, via_variant); assert_eq!(event_id, event.id_str()); assert_eq!(raw_json, event.raw_json()); - - let mismatched = RadrootsSignedEvent::from_wire_unchecked(event.wire().clone(), "{}") - .expect("mismatched raw event"); - assert_eq!( - RadrootsRuntimeTransportPayload::verified_signed_event_json(&mismatched) - .expect_err("mismatched raw json"), - RadrootsTransportError::InvalidPayloadBytes - ); } #[cfg(feature = "transport-workers")] diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs @@ -716,16 +716,13 @@ mod tests { (raw_event, signed_event) } - fn assert_mismatch(raw_event: &RadrootsNostrEvent, signed_event: RadrootsSignedEvent) { - assert!(ensure_raw_event_matches_signed_event(raw_event, &signed_event).is_err()); + fn assert_mismatch(raw_event: RadrootsNostrEvent, signed_event: &RadrootsSignedEvent) { + assert!(ensure_raw_event_matches_signed_event(&raw_event, signed_event).is_err()); } - fn signed_event_with_wire( - original: &RadrootsSignedEvent, - wire: RadrootsNip01EventWire, - ) -> RadrootsSignedEvent { - RadrootsSignedEvent::from_wire_unchecked(wire, original.raw_json().to_owned()) - .expect("signed event") + fn raw_event_from_wire(wire: RadrootsNip01EventWire) -> RadrootsNostrEvent { + RadrootsNostrEvent::from_json(serde_json::to_string(&wire).expect("raw event json")) + .expect("raw event") } #[test] @@ -735,30 +732,30 @@ mod tests { let mut wire = signed_event.wire().clone(); wire.id = "00".repeat(32); - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.pubkey = "11".repeat(32); - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.created_at += 1; - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.kind += 1; - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.content.push_str(" changed"); - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.sig = "22".repeat(64); - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); let mut wire = signed_event.wire().clone(); wire.tags.push(vec!["t".to_owned(), "compost".to_owned()]); - assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); + assert_mismatch(raw_event_from_wire(wire), &signed_event); } } diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -778,13 +778,6 @@ fn verified_signed_event_payload_preserves_transport_payload_identity() { assert_eq!(event_id, signed.id_str()); assert_eq!(raw_json, signed.raw_json().to_owned()); assert_eq!(digest.len(), 64); - - let mismatched = - RadrootsSignedEvent::from_wire_unchecked(signed.wire().clone(), "{}").expect("mismatch"); - assert_eq!( - verified_signed_event_payload(&mismatched).expect_err("mismatched raw json"), - RadrootsTransportError::InvalidPayloadBytes - ); } #[tokio::test]