Skip to content

fix(nostr): reconcile JWK key path to accept both Y-parities (#571) - #577

Merged
melvincarvalho merged 3 commits into
gh-pagesfrom
issue-571-multikey-jwk-parity-consistency
Jun 28, 2026
Merged

fix(nostr): reconcile JWK key path to accept both Y-parities (#571)#577
melvincarvalho merged 3 commits into
gh-pagesfrom
issue-571-multikey-jwk-parity-consistency

Conversation

@melvincarvalho

@melvincarvalho melvincarvalho commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Resolves #571.

Problem

Two code paths that resolve "is this Nostr pubkey present as a verification method?" disagreed on Y-parity:

  • Multikey f-form decoder (decodeFFormSecp256k1) accepts both 02 and 03 parity prefixes, discards the parity byte, and returns the bare x-only key.
  • JWK path (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:

"Nostr applications may generate keys with either 0x02 or 0x03 prefixes." — 0x02 for even-Y, 0x03 for odd-Y.

So the fix relaxes the JWK path to match the decoder, rather than tightening the decoder:

  • New exported helper nostrJwkYParities(xHex) returns the two genuine on-curve y-coordinates (even + odd) for an x, or null if x isn't a valid curve point.
  • pubkeyFromValidatedJwk and jwkMatchesNostrPubkey now accept a JWK whose y matches either parity — while still rejecting a fabricated / off-curve y (the real security property).
  • Removed the now-dead secp256k1 import from src/auth/nostr.js.
  • decodeFFormSecp256k1 itself 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-y guard is preserved.

Tests

  • New 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-curve y still rejected, profile extraction maps both encodings to one identity.
  • Full suite green: 988 pass, 0 fail. The existing "wrong-y" rejection test (nostr-cid-vm.test.js) uses a provably off-curve y=0, so it remains meaningful under the relaxed logic.

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.
@CLAassistant

CLAassistant commented Jun 28, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-curve y candidates (even/odd) for a given secp256k1 x.
  • Updated JWK matching (jwkMatchesNostrPubkey and pubkeyFromValidatedJwk) to accept either valid parity y, 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.

Comment thread src/auth/nostr.js Outdated
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
Comment thread src/auth/nostr-keys.js
- 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread test/nostr-key-parity.test.js
Comment thread test/nostr-key-parity.test.js Outdated
Comment thread test/nostr-key-parity.test.js
Match the rest of the suite's convention (Copilot nit): assert.equal /
notEqual / deepEqual → strictEqual / notStrictEqual / deepStrictEqual.
@melvincarvalho
melvincarvalho force-pushed the issue-571-multikey-jwk-parity-consistency branch 2 times, most recently from 2b5a393 to e233eb5 Compare June 28, 2026 08:45
@melvincarvalho
melvincarvalho requested a review from Copilot June 28, 2026 08:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@melvincarvalho
melvincarvalho merged commit 9c8b9ec into gh-pages Jun 28, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the issue-571-multikey-jwk-parity-consistency branch June 28, 2026 09:11
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multikey f-form decoder accepts odd-Y (03) while the JWK path rejects it — inconsistent parity handling

3 participants