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:
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() {