lib

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

commit 00111c8cdee94fbc8b2c332c8fb70cc1127c2fd5
parent 5f87264fe5e1e8fef740a4b8ab6cda4476e044aa
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Jul 2026 12:53:52 +0000

event: cover malformed NIP-01 envelopes

- require secp256k1-valid x-only author keys before signature verification
- map canonical-length non-curve authors to malformed_envelope
- execute a canonical raw event vector through public verification
- align packaged fixtures and alpha release contract documentation

Diffstat:
MCHANGELOG.md | 4+++-
Mcontracts/conformance/vectors/profile/verified_event.v1.json | 10++++++++++
Mcontracts/events/profile-metadata.md | 14++++++++++----
Mcontracts/releases/1.0.0-alpha.1.toml | 2+-
Mcrates/event_codec/src/verification.rs | 3++-
Mcrates/event_codec/tests/fixtures/profile_verified_event.v1.json | 10++++++++++
Mcrates/event_codec/tests/verified_profile_conformance.rs | 8++++++++
7 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -11,7 +11,9 @@ publish policy both pass for the same source revision. - Generic NIP-01 identifier and signature verification is now independent of knowledge decoding, and every dynamic Nostr kind conversion rejects values - above `65535` instead of truncating them. + above `65535` instead of truncating them. Canonical-length author keys that + are not valid secp256k1 curve points now return `malformed_envelope` instead + of `signature_invalid`. - Calendar authoring and admission now use explicit NIP-52 authored, parsed, and admitted states for date events, time events, calendars, and RSVPs. Kind `31922` no longer emits uppercase `D`; kind `31923` derives integer UTC-day diff --git a/contracts/conformance/vectors/profile/verified_event.v1.json b/contracts/conformance/vectors/profile/verified_event.v1.json @@ -35,6 +35,16 @@ }, { "expected": { + "error": "malformed_envelope" + }, + "id": "event_verify_malformed_curve_pubkey_001", + "input": { + "event_json": "{\"id\":\"8d71bc1e4126e8b16c7e9229487d9028df9508c156d5e501d1487eda799af161\",\"pubkey\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"created_at\":1800000107,\"kind\":0,\"tags\":[],\"content\":\"malformed-curve-pubkey\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}" + }, + "kind": "event.verify_nip01.malformed_envelope" + }, + { + "expected": { "error": "kind_out_of_range", "kind": 65536 }, diff --git a/contracts/events/profile-metadata.md b/contracts/events/profile-metadata.md @@ -28,8 +28,11 @@ not require a Radroots marker tag. `event.verify_nip01` and its `RadrootsSignatureVerifiedEvent` result are available through the codec's `nostr` feature without enabling `knowledge`. -Knowledge contract validation and decoding are a later optional stage and -cannot be substituted for general event verification. +Envelope conversion requires the author to be a valid secp256k1 x-only public +key; a canonical-length hex value that is not a curve point returns +`malformed_envelope`, not `signature_invalid`. Knowledge contract validation and +decoding are a later optional stage and cannot be substituted for general event +verification. The direct legacy `RadrootsProfile` codec, Profile-specific Nostr/network publish helpers, and replica Profile draft emission were removed in the @@ -160,5 +163,8 @@ admission and replacement use mirrored under `crates/event_codec/tests/fixtures/` for published-package tests. The dispatchers execute every vector against the public APIs and require canonical and packaged copies to be byte-for-byte equal when the workspace -contract is present. Consumers enable `serde_json` for metadata operations and -both `serde_json` and `nostr` for cryptographic Profile admission. +contract is present. The verified-event suite includes a canonical-id raw event +whose author is not a valid secp256k1 curve point, proving the stable +`malformed_envelope` mapping. Consumers enable `serde_json` for metadata +operations and both `serde_json` and `nostr` for cryptographic Profile +admission. diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -87,4 +87,4 @@ semver_impacts = [ "add_conformance_vector", "change_exported_algorithm_behavior", ] -summary = "Expose knowledge-independent NIP-01 verification and verified Profile admission while rejecting every out-of-range Nostr kind conversion." +summary = "Expose knowledge-independent NIP-01 verification and verified Profile admission while rejecting out-of-range Nostr kinds and malformed secp256k1 author keys." diff --git a/crates/event_codec/src/verification.rs b/crates/event_codec/src/verification.rs @@ -146,7 +146,8 @@ fn raw_event_from_radroots( ) -> Result<nostr::Event, RadrootsNip01VerificationError> { let id = nostr::EventId::from_hex(event.id_str()) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; - let public_key = nostr::PublicKey::from_hex(event.author_str()) + let public_key = nostr::secp256k1::XOnlyPublicKey::from_str(event.author_str()) + .map(nostr::PublicKey::from) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; let kind = u16::try_from(event.kind_u32()).map_err(|_| { RadrootsNip01VerificationError::KindOutOfRange { diff --git a/crates/event_codec/tests/fixtures/profile_verified_event.v1.json b/crates/event_codec/tests/fixtures/profile_verified_event.v1.json @@ -35,6 +35,16 @@ }, { "expected": { + "error": "malformed_envelope" + }, + "id": "event_verify_malformed_curve_pubkey_001", + "input": { + "event_json": "{\"id\":\"8d71bc1e4126e8b16c7e9229487d9028df9508c156d5e501d1487eda799af161\",\"pubkey\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"created_at\":1800000107,\"kind\":0,\"tags\":[],\"content\":\"malformed-curve-pubkey\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}" + }, + "kind": "event.verify_nip01.malformed_envelope" + }, + { + "expected": { "error": "kind_out_of_range", "kind": 65536 }, diff --git a/crates/event_codec/tests/verified_profile_conformance.rs b/crates/event_codec/tests/verified_profile_conformance.rs @@ -89,6 +89,7 @@ fn execute(vector: &Vector) { "event.verify_nip01.valid" => verify_valid(vector), "event.verify_nip01.invalid_id" => verify_invalid_id(vector), "event.verify_nip01.invalid_signature" => verify_invalid_signature(vector), + "event.verify_nip01.malformed_envelope" => verify_malformed_envelope(vector), "event.verify_nip01.kind_overflow" => verify_kind_overflow(vector), "profile.verify_and_admit.valid" => profile_admit_valid(vector), "profile.verify_and_admit.invalid_kind" => profile_admit_invalid_kind(vector), @@ -132,6 +133,13 @@ fn verify_invalid_signature(vector: &Vector) { assert_eq!(error, RadrootsNip01VerificationError::SignatureInvalid); } +fn verify_malformed_envelope(vector: &Vector) { + let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json"))) + .expect_err("invalid secp256k1 public key must fail envelope conversion"); + assert_eq!(error.code(), expected_str(vector, "error")); + assert_eq!(error, RadrootsNip01VerificationError::MalformedEnvelope); +} + fn verify_kind_overflow(vector: &Vector) { let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json"))) .expect_err("out-of-range event kind must fail");