Skip to content

fix(nodejs)!: complete TCP JWK authentication support - #73

Open
glasstiger wants to merge 4 commits into
mainfrom
ia_jwk_fix
Open

glasstiger wants to merge 4 commits into
mainfrom
ia_jwk_fix

Conversation

@glasstiger

@glasstiger glasstiger commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Completes the two remaining TCP authentication items from #72. The primary public/private-key consistency fix is already on main.

  • Accept complete EC P-256 private JWKs through extraOptions.jwk in Sender.fromConfig(), Sender.fromEnv(), and the SenderOptions constructor and factories.
  • Preserve existing full-JWK precedence over connection-string username/token, and reject the new extra option outside TCP/TCPS rather than silently ignoring authentication.
  • Validate a full JWK when the sender is created. The key must be an object with a non-empty kid, kty: "EC", crv: "P-256", and d, x and y that form a P-256 key pair. Previously the key was first used when the server's challenge arrived, inside a socket listener, where an error escaped connect() as an unhandled rejection and terminated the process. That happened for a key Node could not import, such as one without kty and crv, and on Node.js 20 to 24 also for a d outside the curve's range, such as one written in hex, which Node imports but cannot sign with. The factories and new Sender({ jwk }) now fail with an error that can be caught, and the error never quotes the key.
  • Check the key pair in the client instead of relying on crypto.createPrivateKey(), which enforces it only from Node.js 26. The client derives the public point from d with ECDH and compares it with x and y as integers, the way Node decodes them, so the same keys are accepted on every supported version.
  • If the challenge-response handshake still fails, connect() rejects with the error and destroys the socket instead of the process terminating.
  • Use SHA256 instead of the legacy RSA-SHA256 alias for EC challenge signing. The signatures are unchanged: both names produce DER-encoded ECDSA-SHA256 on Node.js 20, 22, 24 and 26.
  • Add generated-key TCP/TCPS handshake tests for both factories, cryptographically verify the signatures, cover the rejected key shapes on both creation paths (including a mismatched pair and a hex-encoded d), cover a failed handshake, and pin the published TypeScript declarations.
  • Update the READMEs and API comments to list every required JWK member, and regenerate the committed API reference as required by CONTRIBUTING.md. Most generated-file changes are TypeDoc source-link refreshes.

Breaking changes

CONTRIBUTING.md treats a change to the errors a published API throws as breaking, even when it is a fix:

  • Creating a TCP/TCPS sender with a full JWK, through the jwk option or extraOptions.jwk, now throws when the key is incomplete, not an EC P-256 key, or not a key pair. Such keys previously failed when the server's challenge arrived, terminating the process, and a JWK whose x and y belong to a different key than d authenticated on Node.js 20 to 24.
  • extraOptions.jwk is rejected for transports other than TCP/TCPS.

Validation

  • Full Vitest suite: 1,296 tests passed, including containerized integration tests.
  • TCP transport and options suites: passed on Node.js 20.11, 22.17, 24.3 and 26.8.
  • Distribution suite: 39 tests passed.
  • Real-browser suite: 10 tests passed.
  • Source, browser, test, and benchmark typechecks passed.
  • ESM, CommonJS, and TypeScript 4.9 declaration checks passed.
  • ESLint, benchmark lint, full formatting check, package-artifact checks, and documentation reference tests passed.

Closes #72.

Accept full P-256 private JWKs through Sender and SenderOptions factories, retaining TCP/TCPS credential precedence and rejecting the new extra option on unsupported transports.\n\nUse SHA256 for EC challenge signing, verify factory handshakes with generated keys, pin the published declarations, and refresh the documentation.\n\nFixes #72
@glasstiger glasstiger changed the title fix(nodejs): complete TCP JWK authentication support fix(nodejs): fix TCP JWK authentication support Oct 16, 2026
@glasstiger

Copy link
Copy Markdown
Collaborator Author

Review: approve

Reviewed at level 3, comparing 26dcaa71 → a3fc1217.

No actionable defects found in JWK forwarding, credential precedence, TCP/TCPS signing, transport rejection, caller compatibility, or published declarations.

Validation

All checks passed locally on Node.js 24.3.0:

  • 1,284 tests, including containerized integration tests.
  • 39 distribution tests covering ESM/CJS, package boundaries, and browser bundling.
  • 10 real-browser tests.
  • Source, browser, test, and benchmark typechecks.
  • Published ESM/CJS and TypeScript 4.9 declaration checks.
  • ESLint, benchmark lint, formatting, and package-artifact checks.

Regression-test efficacy was checked in isolation: removing JWK forwarding, removing the transport guard, or restoring the old digest identifier each caused the corresponding new tests to fail.

Summary

  • Correctness gate: pass.
  • Test gate: pass; 0 admitted coverage gaps.
  • Findings: 0 Critical / 0 Moderate / 0 Minor.
  • Admitted split: 0 in-diff / 0 out-of-diff breakage.
  • Regressions/tradeoffs: none identified; explicit extra-option JWKs now reject unsupported transports as documented.
  • Submodules: none changed.
  • Limitation: the full supported Node-version matrix was not independently rerun.

Disposable worktrees were removed. The primary Git working tree remains clean.

@glasstiger glasstiger changed the title fix(nodejs): fix TCP JWK authentication support fix(nodejs): complete TCP JWK authentication support Oct 16, 2026
A JWK passed through extraOptions.jwk or the programmatic jwk option was
first used when the server's challenge arrived, inside a socket listener.
A key that Node could not import, such as one without kty and crv, escaped
connect() as an unhandled rejection and terminated the process.

Validate the key when the TCP transport is created: it must be an object
with a non-empty kid, kty set to EC, crv set to P-256, and d, x and y that
form a P-256 key pair. The errors never quote the key, which carries the
private scalar.

List kty and crv in the option's documentation, note the validation in the
READMEs, drop the README's reference to an undocumented workaround, and
regenerate the API reference.
validateJwk() relied on crypto.createPrivateKey() to prove that d, x and
y form a P-256 key pair, but Node.js enforces that only from v26. Up to
v24 it imports a JWK whose x and y belong to another key, and a d outside
the curve's range, such as one written in hex, which crypto.sign() then
rejects at the server's challenge. That error still escaped connect() as
an unhandled rejection and ended the process.

Derive the public point from d with ECDH, which rejects an out-of-range
scalar on every version, and require it to equal x and y. The members are
compared as integers, the way Node.js decodes them, so an encoding with a
leading zero byte still names the same key.

Catch errors from the challenge-response handshake in the socket listener:
connect() now rejects with the error and destroys the socket instead of
the process terminating.

BREAKING CHANGE: Creating a TCP/TCPS sender now validates a full JWK
passed through the jwk option or extraOptions.jwk, and throws when it is
incomplete, not an EC P-256 key, or not a key pair. Such keys previously
failed when the server's challenge arrived, ending the process, and a JWK
whose x and y belong to a different key than d authenticated on Node.js
20 to 24. extraOptions.jwk is rejected for transports other than TCP/TCPS.
@glasstiger glasstiger changed the title fix(nodejs): complete TCP JWK authentication support fix(nodejs)!: complete TCP JWK authentication support Oct 17, 2026
@glasstiger

Copy link
Copy Markdown
Collaborator Author

Review: approve

Completed a level 3 review, comparing 26dcaa71 → c5814b84.

No actionable defects found in JWK forwarding, credential precedence, key validation, TCP/TCPS signing and error handling, caller compatibility, or published declarations.

Validation

Passed locally on Node.js 24.3.0:

  • 1,297 tests, including containerized QuestDB integration tests.
  • 39 distribution tests covering package boundaries, ESM/CJS loading, and browser bundling.
  • 10 real-browser tests.
  • Source, browser, test, and benchmark typechecks.
  • Published ESM/CJS and TypeScript 4.9 declaration checks.
  • ESLint, benchmark lint, formatting, and package-artifact checks.

The 96 focused options/transport tests also passed on Node.js 22.17.0.

Regression-test efficacy was checked in isolation: removing forwarding, transport rejection, key validation, pair consistency, integer-coordinate handling, or handshake error containment—and restoring the old digest identifier—caused the corresponding tests to fail.

Summary

  • Correctness gate: pass.
  • Test gate: pass; 0 admitted coverage gaps.
  • Severity distribution: 0 Critical / 0 Moderate / 0 Minor.
  • Admitted split: 0 in-diff / 0 out-of-diff breakage.
  • Regressions/tradeoffs: no unintended regressions identified. Earlier rejection of invalid full JWKs and rejection of unsupported extra-option transports are documented breaking changes.
  • Submodules: none changed.
  • Limitation: the full supported Node-version matrix was not independently rerun.

Disposable worktrees were removed. The primary working tree remains clean.

@glasstiger

Copy link
Copy Markdown
Collaborator Author

Review: approve

Completed a level 3 review, comparing 26dcaa71 → c5814b84. Generated docs/** files were not reviewed as source.

Nothing blocking was found. The only item is an optional commit-message fix.

Critical

None.

Moderate

None.

Minor

Coverage

The test gate passes, with no coverage gaps.

  • Reverting each production change on its own turns the matching new tests red: the jwk forwarding, the non-TCP rejection, the validateJwk() call, the integer coordinate comparison, the connect() try/catch, the socket destroy, and the SHA256 digest name.
  • The one exception is the key-pair check. Removing it is caught on Node.js 20 but not on Node.js 26, because Node.js 26 rejects a mismatched pair on its own. CI's Node.js 20 and 22 jobs still catch it.

Validation

Run locally, comparing base and head:

  • Sender.fromConfig with { jwk } (Node.js 20.11, 24.3, 26.8): at base the key is ignored and the sender connects without authenticating. At head it authenticates with the key's kid and a signature that verifies.
  • JWKs without kty/crv, or with a hex-encoded d (Node.js 20.11, 24.3, 26.8): at base these crash the process with an unhandled rejection. At head they are rejected when the sender is created, and the error never quotes the key.
  • TCPS with signing forced to fail (Node.js 20.11, 22.17, 24.3, 26.8): connect() rejects, the socket is destroyed, and nothing escapes as an unhandled rejection. A later close() followed by connect() authenticates normally.
  • Digest name (Node.js 20.11, 22.17, 24.3, 26.8): "SHA256" and "RSA-SHA256" produce the same kind of signature, DER-encoded ECDSA-SHA256.
  • Gates: the options and TCP transport suites pass (96/96) on all four versions. The source and test type checks, ESLint, Prettier, the published-declaration checks (ESM, CJS and TypeScript 4.9) and the docs-reference test pass on Node.js 24.3.

Summary

  • Correctness gate: pass.
  • Test gate: pass; 0 admitted coverage gaps.
  • Severity distribution: 0 Critical / 0 Moderate / 1 Minor.
  • Admitted split: 1 in-diff (commit metadata) / 0 out-of-diff breakage.
  • Intended breaking changes, both disclosed in the PR description and in the BREAKING CHANGE footer of commit ed6f55fd:
    • A JWK whose x/y don't belong to d now fails when the sender is created. On Node.js 20–24 that key used to authenticate.
    • extraOptions.jwk is now rejected for transports other than TCP/TCPS.
  • Submodules: none changed.
  • Limitations: the full containerized test suite, pnpm test:dist and the browser suite were not rerun. The oldest Node.js tested was 20.11.0, below the package's 20.18.1 minimum.

This branch has not been deployed

No deployments
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.

TCP ILP auth: constructJwk() hardcodes the example keypair's x/y — auth fails with any real token (Invalid JWK EC key)

1 participant