mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
fix: reject non-32-byte pubkeys when decoding npub/nprofile
A Nostr pubkey is x-only, exactly 32 bytes, but NPub.parse/NProfile.parse never checked the length — they hex-encoded whatever bytes the bech32/TLV carried. A malformed npub/nprofile that some clients encode with the full 33-byte COMPRESSED secp256k1 key (0x02/0x03 prefix) therefore round-tripped its 66-char hex straight into a `p`/`q` tag via the quote/mention path, and a strict relay (relay29 / pyramid.fiatjaf.com) rejected the whole group message: blocked: schema validation failed: tag[..]: invalid pubkey value '02977dcf…c3402' ... pubkey should be 64-char hex We never generate compressed keys ourselves (Nip01Crypto.pubKeyCreate strips the prefix byte); this is purely inbound malformed input. Enforce the 32-byte length at the decode boundary so the bad entity never becomes a mention/quote tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
+3
-1
@@ -47,7 +47,9 @@ data class NProfile(
|
||||
val hex = tlv.firstAsHex(TlvTypes.SPECIAL) ?: return null
|
||||
val relay = tlv.asStringList(TlvTypes.RELAY) ?: emptyList()
|
||||
|
||||
if (hex.isBlank()) return null
|
||||
// x-only 32-byte pubkey only (64 hex chars). Reject a 33-byte compressed
|
||||
// key or any other malformed length — see NPub.parse for why it matters.
|
||||
if (hex.length != 64) return null
|
||||
|
||||
return NProfile(hex, relay.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) })
|
||||
}
|
||||
|
||||
@@ -32,7 +32,11 @@ data class NPub(
|
||||
) : IPubKeyEntity {
|
||||
companion object {
|
||||
fun parse(bytes: ByteArray): NPub? {
|
||||
if (bytes.isEmpty()) return null
|
||||
// A Nostr pubkey is x-only, exactly 32 bytes. Reject anything else —
|
||||
// notably a 33-byte COMPRESSED secp256k1 key (0x02/0x03 prefix) that some
|
||||
// clients wrongly encode into an npub. Passing its 66-char hex through into
|
||||
// a `p`/`q` tag gets the whole event rejected by strict relays (relay29).
|
||||
if (bytes.size != 32) return null
|
||||
return NPub(bytes.toHexKey())
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip19Bech32
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
@@ -460,4 +461,26 @@ class NIP19ParserTest {
|
||||
withBom.bechToBytes().toNsec(),
|
||||
)
|
||||
}
|
||||
|
||||
// A 33-byte COMPRESSED secp256k1 key (0x02 prefix). Nostr pubkeys are x-only 32 bytes; some
|
||||
// clients wrongly encode the compressed form. Parsing must reject it so its 66-char hex never
|
||||
// reaches a `p`/`q` tag (strict relays like relay29 reject the whole event otherwise).
|
||||
private val compressedPubKey = "02977dcfdd3ce61572b652e0eade0fd519108be00a74b00064eb35b746cf6c3402"
|
||||
|
||||
@Test
|
||||
fun rejects_compressed_pubkey_encoded_as_npub() {
|
||||
assertEquals(null, NPub.parse(compressedPubKey.hexToByteArray()))
|
||||
assertEquals(null, Nip19Parser.uriToRoute("nostr:" + NPub.create(compressedPubKey))?.entity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejects_compressed_pubkey_encoded_as_nprofile() {
|
||||
assertEquals(null, Nip19Parser.uriToRoute("nostr:" + NProfile.create(compressedPubKey, emptyList()))?.entity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accepts_valid_xonly_pubkey() {
|
||||
val xOnly = "bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365"
|
||||
assertEquals(xOnly, NPub.parse(xOnly.hexToByteArray())?.hex)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user