lib

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

commit a8cbaf8f98fd0a7d4e0232feefc2c3bf005f853f
parent 808a3cebd2878b0562f061430589926252f5726c
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 18:33:03 +0000

event: verify wire envelope conversion

- make public wire-to-envelope conversion verify canonical event ids
- keep unchecked envelope materialization crate-private for explicit signed-event internals
- propagate domain envelope errors through the wire error surface
- validate with event, codec, store, outbox, and workspace checks

Diffstat:
Mcrates/event/src/draft.rs | 4++--
Mcrates/event/src/wire.rs | 85++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 86 insertions(+), 3 deletions(-)

diff --git a/crates/event/src/draft.rs b/crates/event/src/draft.rs @@ -480,7 +480,7 @@ impl RadrootsSignedEvent { } let envelope = wire .clone() - .into_envelope() + .into_envelope_unchecked_id() .map_err(RadrootsSignedEventError::Envelope)?; Ok(Self { envelope, @@ -495,7 +495,7 @@ impl RadrootsSignedEvent { ) -> Result<Self, RadrootsSignedEventError> { let envelope = wire .clone() - .into_envelope() + .into_envelope_unchecked_id() .map_err(RadrootsSignedEventError::Envelope)?; Ok(Self { envelope, diff --git a/crates/event/src/wire.rs b/crates/event/src/wire.rs @@ -126,6 +126,7 @@ pub enum RadrootsEventWireError { actual: usize, }, CanonicalEventId(RadrootsCanonicalEventIdError), + Envelope(RadrootsEventEnvelopeError), EventIdMismatch { declared: String, computed: String, @@ -181,6 +182,7 @@ impl fmt::Display for RadrootsEventWireError { write!(f, "event wire extra JSON bytes {actual} exceed {max}") } Self::CanonicalEventId(error) => write!(f, "{error}"), + Self::Envelope(error) => write!(f, "{error}"), Self::EventIdMismatch { declared, computed } => write!( f, "event wire id mismatch: declared {declared}, computed {computed}" @@ -198,6 +200,12 @@ impl From<RadrootsCanonicalEventIdError> for RadrootsEventWireError { } } +impl From<RadrootsEventEnvelopeError> for RadrootsEventWireError { + fn from(value: RadrootsEventEnvelopeError) -> Self { + Self::Envelope(value) + } +} + #[cfg_attr( any(feature = "serde", test), derive(serde::Serialize, serde::Deserialize) @@ -278,7 +286,15 @@ impl RadrootsNip01EventWire { Ok(()) } - pub fn into_envelope(self) -> Result<RadrootsEventEnvelope, RadrootsEventEnvelopeError> { + pub fn into_envelope(self) -> Result<RadrootsEventEnvelope, RadrootsEventWireError> { + self.verify_id()?; + self.into_envelope_unchecked_id() + .map_err(RadrootsEventWireError::Envelope) + } + + pub(crate) fn into_envelope_unchecked_id( + self, + ) -> Result<RadrootsEventEnvelope, RadrootsEventEnvelopeError> { RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: self.id, author: self.pubkey, @@ -668,6 +684,73 @@ mod tests { ); } + #[test] + fn into_envelope_verifies_id_before_domain_conversion() { + let wire = + RadrootsNip01EventWire::parse_json(valid_event_json("hello", default_tags()).as_str()) + .expect("wire"); + + let envelope = wire.clone().into_envelope().expect("envelope"); + assert_eq!(envelope.id_str(), wire.id); + assert_eq!(envelope.content(), "hello"); + + let mut tampered_id = wire.clone(); + tampered_id.id = hex_64('f'); + assert!(matches!( + tampered_id.into_envelope(), + Err(RadrootsEventWireError::EventIdMismatch { .. }) + )); + + let mut tampered_content = wire; + tampered_content.content = "tampered".to_owned(); + assert!(matches!( + tampered_content.into_envelope(), + Err(RadrootsEventWireError::EventIdMismatch { .. }) + )); + } + + #[test] + fn into_envelope_ignores_extra_for_id_and_propagates_domain_limits() { + let mut value = valid_event_value("hello", default_tags()); + value + .as_object_mut() + .expect("object") + .insert("client".to_owned(), json!("radroots-test")); + let wire = RadrootsNip01EventWire::parse_json(raw_json(&value).as_str()).expect("wire"); + let envelope = wire.into_envelope().expect("envelope"); + assert_eq!(envelope.content(), "hello"); + + let content = core::iter::repeat_n('x', DEFAULT_CONTENT_MAX_BYTES + 1).collect::<String>(); + let tags = default_tags(); + let pubkey = hex_64('a'); + let id = compute_canonical_nip01_event_id( + pubkey.as_str(), + 1_700_000_000, + 1, + &tags, + content.as_str(), + ) + .expect("event id") + .into_string(); + let wire = RadrootsNip01EventWire { + id, + pubkey, + created_at: 1_700_000_000, + kind: 1, + tags, + content, + sig: hex_128('b'), + extra: Default::default(), + }; + + assert!(matches!( + wire.into_envelope(), + Err(RadrootsEventWireError::Envelope( + RadrootsEventEnvelopeError::ContentTooLarge { .. } + )) + )); + } + #[cfg(feature = "serde")] #[test] fn serde_flatten_preserves_extra_fields() {