node:crypto: validate EC JWK private scalar matches public point in createPrivateKey#33752
node:crypto: validate EC JWK private scalar matches public point in createPrivateKey#33752robobun wants to merge 2 commits into
Conversation
WalkthroughThis change adds a validation step to EC JWK private key import in KeyObject.cpp, checking for exceptions after decoding the private scalar and calling checkKey() to validate consistency, throwing CRYPTO_INVALID_JWK on failure. New tests verify this behavior across multiple curves. ChangesEC JWK Import Validation
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 7:05 AM PT - Jul 8th, 2026
❌ @robobun, your commit cc6043a has 1 failures in 🧪 To try this PR locally: bunx bun-pr 33752That installs a local version of the PR into your bun-33752 --bun |
There was a problem hiding this comment.
LGTM — small, well-tested tightening of EC JWK validation to match Node.
Extended reasoning...
Overview
This PR adds a single ec.checkKey() call (wrapping BoringSSL's EC_KEY_check_key) after setting the private scalar in KeyObject::getKeyObjectHandleFromJwk, so that an EC JWK whose d does not correspond to its (x, y) is rejected with ERR_CRYPTO_INVALID_JWK at import time — matching Node.js and Bun's own WebCrypto importer. It also adds a missing RETURN_IF_EXCEPTION after base64-decoding d, mirroring the sibling x/y decode sites just above. New tests in crypto.key-objects.test.ts cover mismatched, truncated, and leading-zero-padded d across P-256/P-384/P-521, plus a positive self-verify control.
Security risks
None introduced. This change only tightens input validation (fails closed) using the standard BoringSSL key-consistency check; it cannot cause previously-rejected keys to be accepted. Rejecting inconsistent JWKs is a correctness/safety improvement — the prior behavior silently produced a KeyObject that couldn't verify its own signatures.
Level of scrutiny
While this is crypto code, the change is mechanical: a 4-line addition calling an existing, already-used ncrypto helper on the exact error path pattern used by the surrounding code, and it directly mirrors both Node's crypto_ec.cc and Bun's CryptoKeyEC::platformImportJWKPrivate. It is also the sibling of the already-merged #32910 (same fix for OKP JWKs in the same function). Low scrutiny is appropriate.
Other factors
No CODEOWNERS entry covers this path, no outstanding human review comments, and the multi-agent bug hunter found nothing. The added RETURN_IF_EXCEPTION is an unambiguous fix for a missing exception check. Test coverage is thorough (positive control, two rejection cases, one acceptance-of-equivalent-encoding case, three curves).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/node/crypto/crypto.key-objects.test.ts`:
- Around line 613-646: The curve matrix in the createPrivateKey EC JWK rejection
suite is manually expanded with a for loop around test(), but this test file
should use parameterized helpers instead. Refactor the "createPrivateKey rejects
inconsistent EC JWK" block to use describe.each() or test.each() for the
namedCurve cases, keeping the existing assertions and symbols like
generateKeyPairSync, createPrivateKey, createPublicKey, and verify unchanged
inside the shared test body.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a5c98878-638c-4abf-83eb-5c050b64f33d
📒 Files selected for processing (2)
src/jsc/bindings/node/crypto/KeyObject.cpptest/js/node/crypto/crypto.key-objects.test.ts
There was a problem hiding this comment.
No issues found — the change is small, strictly tightens validation, and mirrors both Node's crypto_ec.cc and Bun's own JSECDHPrototype.cpp checkKey() usage, but key-import validation in node:crypto is worth a human sign-off.
Extended reasoning...
Overview
The PR adds an EC_KEY_check_key call (via the existing ECKeyPointer::checkKey() wrapper) after setting the private scalar in the EC-JWK branch of KeyObject::getKeyObjectHandleFromJwk, so a JWK whose d doesn't correspond to its (x, y) now throws ERR_CRYPTO_INVALID_JWK instead of producing a self-inconsistent KeyObject. It also adds the missing RETURN_IF_EXCEPTION after base64-decoding d, matching every other decodeJwkString call site in the same function. New tests cover mismatched, truncated, and leading-zero-padded d across P-256/P-384/P-521, with a positive control that a valid key still imports and self-verifies.
Security risks
This is a hardening change: it makes key import strictly more restrictive (fail-closed) and cannot cause previously-rejected keys to be accepted. The only behavioral risk would be rejecting a key that Node accepts, but the PR cites Node's identical check and includes the padded-d case to guard against over-rejection. MarkPopErrorOnReturn is already active in this scope, so the extra OpenSSL error-queue entry from a failed EC_KEY_check_key is cleaned up.
Level of scrutiny
This is production crypto key-parsing code in src/jsc/bindings/node/crypto/. Even though the diff is five native lines using an existing wrapper in the same pattern as JSECDHPrototype.cpp:98, key-import validation is exactly the surface where a human reviewer should confirm the Node-parity claim and the padded-scalar acceptance behavior.
Other factors
No CODEOWNERS cover this path. No prior reviews or outstanding comments. The bug-hunting pass found nothing. The added RETURN_IF_EXCEPTION is independently correct per the file's own conventions. Tests are well-structured (positive + three negative variants per curve, exact error code asserted). The PR is a sibling of #32910 (same fix for OKP), which suggests the pattern has already been accepted once.
|
The diff is ready. The two CI runs so far each hit a single unrelated infra flake:
Neither touches crypto; both are SQL infra on a single lane each. |
What does this PR do?
crypto.createPrivateKey({format: "jwk"})currently accepts an EC private JWK whoseddoes not correspond to its(x, y), producing a KeyObject whose derived public key cannot verify its own signatures:A truncated
dis similarly accepted and re-exports as a different scalar. Bun's owncrypto.subtle.importKey("jwk", ...)already rejects both inputs in the same process, so the two JWK importers disagreed with each other.The cause is that
KeyObject::getKeyObjectHandleFromJwksets the public point viaEC_KEY_set_public_key_affine_coordinates(which validates the point is on the curve) and the private scalar viaEC_KEY_set_private_key(which does not cross-check against the public point), then wraps the result without ever callingEC_KEY_check_key. Node performs that check after setting the private key, as does Bun's WebCryptoCryptoKeyEC::platformImportJWKPrivate.This PR calls
ec.checkKey()(the existingECKeyPointerwrapper aroundEC_KEY_check_key) aftersetPrivateKeyand throwsERR_CRYPTO_INVALID_JWKon failure, matching Node. It also adds the missingRETURN_IF_EXCEPTIONafter base64-decodingd.Sibling of #32910, which makes the analogous fix for OKP private JWKs in the same function.
How did you verify your code works?
New tests in
test/js/node/crypto/crypto.key-objects.test.tscover mismatchedd, truncatedd, and leading-zero-paddedd(which Node accepts, since the scalar value is unchanged) for P-256/P-384/P-521. They fail on currentmain(USE_SYSTEM_BUN=1 bun test) and pass withbun bd test; the rest of the file (88 tests) is unaffected. Also verified the test expectations match Node v26.3.0 for all three curves.