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:
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]