node:crypto: reject RSA modulusLength that BoringSSL would round down - #36096
node:crypto: reject RSA modulusLength that BoringSSL would round down#36096robobun wants to merge 1 commit into
Conversation
generateKeyPair('rsa', { modulusLength: 1023 }) silently returned an
896-bit key because BoringSSL's RSA_generate_key_ex masks the request
with ~127. Reject non-multiples of 128 with ERR_OUT_OF_RANGE instead of
returning a weaker key than the caller asked for.
|
Status: fix applied and verified, waiting on maintainer. Reproduced with: Passes with: CI (build 83325): the only red lane is |
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
There was a problem hiding this comment.
No bugs found, but this changes user-facing node:crypto behavior in a way that diverges from Node/OpenSSL (rejecting non-multiple-of-128 modulusLength where Node honors it exactly), so it's worth a human confirming reject-over-round-up is the intended contract.
What was reviewed:
- Verified the
ERR::OUT_OF_RANGEoverload matches (ErrorCode.cpp:953) and produces the message the test asserts on. - Grepped every
modulusLength:intest/js/node/— the only non-multiple-of-128 values are intest-crypto-keygen-bit-length.js(already skips onopenssl_is_boringssl) and the pre-existingtest.skip(513)in this file, so no other tests break. - Check placement mirrors the existing
publicExponentguard just below and covers bothrsaandrsa-pssbefore the branch.
Extended reasoning...
Overview
Adds a 5-line guard in RsaKeyPairJobCtx::fromJS (CryptoGenRsaKeyPair.cpp) that rejects modulusLength % 128 != 0 with ERR_OUT_OF_RANGE, plus two tests in crypto.key-objects.test.ts covering generateKeyPairSync and the sync-throw path of async generateKeyPair. Also rewords the skip comment on the existing 513-bit test.
Security risks
The change is security-positive: it replaces a silent downgrade (BoringSSL's bits &= ~127 masking, e.g. 3000→2944) with a loud, catchable error. There's no new parsing of untrusted input — modulusLength is already run through validateUint32 before the new check reads it. modulusLength: 0 still passes the modulo check but is rejected downstream by BoringSSL as before.
Level of scrutiny
Deferring because this is node:crypto and the fix encodes an API design decision: Node+OpenSSL generates 1023/3000/4095-bit keys exactly, so this PR intentionally diverges from Node by rejecting inputs Node accepts. The alternative (round up to the next multiple of 128, or leave the silent round-down) isn't discussed in the diff, and per the repo's API-design guidance a maintainer should sign off on the chosen contract. The sibling WebCrypto fix (#33477) apparently set precedent, but confirming consistency there is worth a human glance.
Other factors
The implementation itself looks correct and idiomatic — it sits right next to the identically-structured publicExponent < 3 || even guard, uses the same ERR::OUT_OF_RANGE(scope, global, name, msg, actual) overload, and returns std::nullopt after setting the exception. Tests fail on system Bun (1023→896, 3000→2944) and pass on the debug build per the PR body. No exception-scope issues: modulusLengthValue is a plain JSValue already fetched and validated above the new lines.
|
On reject vs round-up: went with reject to match #33477's choice for the WebCrypto path and because the issue sketch named "validate/reject (or exactly honor)" as the options. Rounding up would also leave Happy to switch to round-up if that's preferred; it's a one-line change from |
Fixes #5740.
Repro
On
mainthis silently returns a weaker key than requested:Node v26 (OpenSSL) honors 1023, 3000, and 4095 exactly.
Cause
RsaKeyPairJobCtx::fromJSpassesmodulusLengthstraight toEVP_PKEY_CTX_set_rsa_keygen_bits, and BoringSSL'sRSA_generate_key_exmasks it withbits &= ~127(vendor/boringssl/crypto/fipsmodule/rsa/rsa_impl.cc.inc:736). Nothing on the way in checked for that rounding and nothing on the way out compared the generated modulus against the request.This is the
node:cryptosibling of #33477; that PR fixes only the WebCrypto path (CryptoKeyRSA::generatePair), which does not reachgenerateKeyPair.Fix
Reject a
modulusLengththat is not a multiple of 128 withERR_OUT_OF_RANGEinRsaKeyPairJobCtx::fromJS, before the key is generated. The check sits ahead of thersa/rsa-pssbranch so it covers both variants. Sizes BoringSSL generates exactly (512, 1024, 2048, 3072, 4096, ...) are unchanged.After:
Node's own
test-crypto-keygen-bit-length.jsalready skips onprocess.features.openssl_is_boringsslfor this exact limitation, so no upstream test is affected.Verification
New tests in
test/js/node/crypto/crypto.key-objects.test.tscover bothgenerateKeyPairSyncand the asyncgenerateKeyPair.test output
test-crypto-keygen-async-rsa.js,test-crypto-keygen-sync.js, andtest-crypto-keygen-bit-length.jsstill pass.no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/crypto/crypto.key-objects.test.ts