Two code paths that resolve "is this Nostr pubkey present as a verification method?" disagree on Y-parity.
Lenient (Multikey f-form). decodeFFormSecp256k1 (src/auth/nostr-keys.js:75-77) accepts both 02 and 03 parity prefixes, then discards the parity byte and returns the bare x-only key:
const parity = rest.slice(0, 2);
if (parity !== '02' && parity !== '03') return null;
return rest.slice(2); // parity discarded
So a VM publicKeyMultibase: "fe70103<x>" resolves to the same identity as "fe70102<x>" (via findNostrVmInProfile, src/auth/nostr.js:612-614, and extractNostrPubkeysFromProfile, src/auth/nostr-keys.js:98-99).
Strict (JWK). jwkMatchesNostrPubkey (src/auth/nostr.js:646-668) / pubkeyFromValidatedJwk (src/auth/nostr-keys.js:44-55) reconstruct the canonical even-Y point and reject a JWK whose y is odd, with an explicit comment that this is deliberate.
So the same logical key, expressed two ways, has two different validation semantics: the Multikey form of an odd-Y point is accepted (mapped to the even-Y key), but the JWK form of it is rejected.
Not an active auth issue. Every builder emits 02 only (src/keys/provision.js:41,90; src/idp/well-known-did-nostr.js:498), so 03 is not produced today. And NIP-98 verifies the Schnorr signature against the real even-Y pubkey before this lookup, while LWS-CID verifies against the JWK. This is a latent consistency defect, not a bypass.
Resolution is a parity-design call (not picking one here):
- (a) Make the Multikey decoder strict (reject
03), matching the JWK path. Correct for NIP-98, where a signed Nostr key is BIP-340 x-only (even-Y) by definition.
- (b) Keep accepting both, and reconcile the JWK path instead — appropriate if
decodeFFormSecp256k1 is also meant to parse tweaked / taproot-derived keys, which can land on either parity (cf. the did:nostr spec note that both 02/03 occur because keys may be tweaked).
The right answer depends on whether decodeFFormSecp256k1 is only the NIP-98 matcher (even-Y) or also a general did:nostr-document parser, and should track the did:nostr spec's parity stance.
Two code paths that resolve "is this Nostr pubkey present as a verification method?" disagree on Y-parity.
Lenient (Multikey f-form).
decodeFFormSecp256k1(src/auth/nostr-keys.js:75-77) accepts both02and03parity prefixes, then discards the parity byte and returns the bare x-only key:So a VM
publicKeyMultibase: "fe70103<x>"resolves to the same identity as"fe70102<x>"(viafindNostrVmInProfile,src/auth/nostr.js:612-614, andextractNostrPubkeysFromProfile,src/auth/nostr-keys.js:98-99).Strict (JWK).
jwkMatchesNostrPubkey(src/auth/nostr.js:646-668) /pubkeyFromValidatedJwk(src/auth/nostr-keys.js:44-55) reconstruct the canonical even-Y point and reject a JWK whoseyis odd, with an explicit comment that this is deliberate.So the same logical key, expressed two ways, has two different validation semantics: the Multikey form of an odd-Y point is accepted (mapped to the even-Y key), but the JWK form of it is rejected.
Not an active auth issue. Every builder emits
02only (src/keys/provision.js:41,90;src/idp/well-known-did-nostr.js:498), so03is not produced today. And NIP-98 verifies the Schnorr signature against the real even-Y pubkey before this lookup, while LWS-CID verifies against the JWK. This is a latent consistency defect, not a bypass.Resolution is a parity-design call (not picking one here):
03), matching the JWK path. Correct for NIP-98, where a signed Nostr key is BIP-340 x-only (even-Y) by definition.decodeFFormSecp256k1is also meant to parse tweaked / taproot-derived keys, which can land on either parity (cf. the did:nostr spec note that both02/03occur because keys may be tweaked).The right answer depends on whether
decodeFFormSecp256k1is only the NIP-98 matcher (even-Y) or also a general did:nostr-document parser, and should track the did:nostr spec's parity stance.