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:
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");