fix(nostr): reconcile JWK key path to accept both Y-parities (#571) - #577
Merged
melvincarvalho merged 3 commits intoJun 28, 2026
Merged
Conversation
The f-form Multikey decoder accepts both 02 and 03 parity prefixes and discards the parity byte, but the JWK path (pubkeyFromValidatedJwk / jwkMatchesNostrPubkey) only accepted the BIP-340 even-Y point. Same logical key, two encodings, two verdicts. The did:nostr spec (nostrcg.github.io/did-nostr) allows both: "Nostr applications may generate keys with either 0x02 or 0x03 prefixes." So fix the inconsistency by relaxing the JWK path, not tightening the decoder. - Add nostrJwkYParities(x): the two genuine on-curve y's (even + odd). - pubkeyFromValidatedJwk / jwkMatchesNostrPubkey accept either parity, still rejecting fabricated/off-curve y. - Drop now-dead secp256k1 import from nostr.js. - Add test/nostr-key-parity.test.js pinning cross-decoder agreement. Safe: the x coordinate is the Nostr identity; NIP-98 signature verification stays bound to the BIP-340 (even-Y) interpretation.
There was a problem hiding this comment.
Pull request overview
This PR fixes an inconsistency in Nostr verification-method matching by aligning the JWK verification path with the f-form Multikey decoder: both now accept either SEC1 parity prefix (02 even-Y / 03 odd-Y) for the same x-only Nostr identity, while still rejecting fabricated/off-curve y values.
Changes:
- Added
nostrJwkYParities(xHex)to compute the valid on-curveycandidates (even/odd) for a given secp256k1x. - Updated JWK matching (
jwkMatchesNostrPubkeyandpubkeyFromValidatedJwk) to accept either valid parityy, preserving the off-curve guard. - Added parity-focused regression tests to pin consistent behavior across Multikey and JWK encodings.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/auth/nostr.js |
Removes direct secp256k1 usage and delegates JWK y-parity validation to nostrJwkYParities for parity-consistent VM matching. |
src/auth/nostr-keys.js |
Introduces nostrJwkYParities and uses it to validate JWK (x,y) as genuinely on-curve for either parity. |
test/nostr-key-parity.test.js |
Adds tests covering 02/03 parity equivalence and continued rejection of off-curve/fabricated y. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { fetchCidDocument } from './cid-doc-fetch.js'; | ||
| import { normalizeControllers } from './lws-cid.js'; // shared JSON-LD controller helper | ||
| import { decodeFFormSecp256k1, extractNostrPubkeysFromProfile } from './nostr-keys.js'; // re-exported for back-compat | ||
| import { decodeFFormSecp256k1, extractNostrPubkeysFromProfile, nostrJwkYParities } from './nostr-keys.js'; // re-exported for back-compat |
- nostr.js: move the "re-exported for back-compat" comment onto the
actual `export { extractNostrPubkeysFromProfile }` line — only that
symbol is re-exported, not the whole import.
- nostrJwkYParities: normalize input to lowercase (and guard non-string)
before the hex check, matching decodeFFormSecp256k1; add coverage.
Match the rest of the suite's convention (Copilot nit): assert.equal / notEqual / deepEqual → strictEqual / notStrictEqual / deepStrictEqual.
melvincarvalho
force-pushed
the
issue-571-multikey-jwk-parity-consistency
branch
2 times, most recently
from
June 28, 2026 08:45
2b5a393 to
e233eb5
Compare
melvincarvalho
added a commit
that referenced
this pull request
Jul 2, 2026
Changes since 0.0.210: - fix(nostr): reconcile JWK key path to accept both Y-parities (#571) (#577) - fix(server-root): skip landing-page seeding in --public mode — --public no longer writes /index.html, /.acl, /index.html.acl into the served directory, restoring serve-like behaviour for servejss (#578) (#579)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #571.
Problem
Two code paths that resolve "is this Nostr pubkey present as a verification method?" disagreed on Y-parity:
decodeFFormSecp256k1) accepts both02and03parity prefixes, discards the parity byte, and returns the bare x-only key.pubkeyFromValidatedJwk/jwkMatchesNostrPubkey) reconstructed only the canonical even-Y point and rejected odd-Y.So the same logical key, expressed two ways, had two different validation verdicts.
Resolution — option (b), spec-aligned
The did:nostr draft spec explicitly allows both parities:
So the fix relaxes the JWK path to match the decoder, rather than tightening the decoder:
nostrJwkYParities(xHex)returns the two genuine on-curve y-coordinates (even + odd) for an x, ornullif x isn't a valid curve point.pubkeyFromValidatedJwkandjwkMatchesNostrPubkeynow accept a JWK whoseymatches either parity — while still rejecting a fabricated / off-curvey(the real security property).secp256k1import fromsrc/auth/nostr.js.decodeFFormSecp256k1itself is unchanged (its 02/03 behavior was already spec-correct).Why it's safe
The decoder/JWK lookups only resolve "is this x-only pubkey a VM in the profile?" — the x coordinate is the Nostr identity. The actual NIP-98 Schnorr-signature verification stays bound to the BIP-340 (even-Y) interpretation regardless of how the VM encoded the point. The off-curve-
yguard is preserved.Tests
test/nostr-key-parity.test.js(9 tests) pins cross-decoder agreement: 02/03 Multikey -> same x, even-Y/odd-Y JWK -> same x, off-curveystill rejected, profile extraction maps both encodings to one identity.nostr-cid-vm.test.js) uses a provably off-curvey=0, so it remains meaningful under the relaxed logic.