Skip to content

webcrypto: report the modulus bit length as RsaKeyAlgorithm.modulusLength - #42073

Closed
robobun wants to merge 3 commits into
mainfrom
robobun/f266241c/rsa-imported-modulus-length
Closed

robobun wants to merge 3 commits into
mainfrom
robobun/f266241c/rsa-imported-modulus-length

Conversation

@robobun

@robobun robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Main covers this PR. PR #47559 (bc55d56) made the same one-line change in getRSAModulusLength and added a test for it. The test of this PR passes on a build of bc55d56. Nothing here remains to merge. The text below is the PR as it was.

Problem

  • algorithm.modulusLength of an imported RSA key is the byte length of n times 8, not the bit length of the modulus. A JWK or SPKI key with a 2047-, 2041- or 2049-bit modulus reports 2048, 2048 and 2056. Node v26.12.1 reports 2047, 2041 and 2049. A modulusLength >= 2048 gate then passes a 2041-bit key in Bun only.
  • The cause is getRSAModulusLength (src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp:41), which returns RSA_size(rsa) * 8. RSA_size is the modulus size in bytes, rounded up.

Fix

  • Return RSA_bits(rsa), which is BN_num_bits(n).
  • CryptoKeyRSA::keySizeInBits() uses the same helper. Its three callers compare the result with zero, so nothing else changes. Generated keys are unaffected: BoringSSL generates exactly modulusLength bits.
  • Verified: test/js/web/crypto/web-crypto.test.ts ("RsaKeyAlgorithm.modulusLength of an imported key", fails on main). Also the vendored test-webcrypto-*-rsa.js, test-crypto-key-objects-to-crypto-key.js and test-webcrypto-cryptokey-clone-transfer.js.

Background

  • RsaKeyAlgorithm.modulusLength is "the length, in bits, of the RSA public modulus" (WebCrypto, RsaKeyAlgorithm dictionary).
  • CryptoKeyRSA::algorithm() builds that dictionary from the RSA* inside the key's EVP_PKEY for every RSA key, whether generated or imported through jwk, spki or pkcs8.

Downsides

  • An imported key whose modulus is 2047 bits now reports 2047, not 2048. Code that compares modulusLength with 2048 gets a different answer for such a key, as it does in Node.
  • No cost per call: RSA_bits is BN_num_bits(n), and RSA_size was BN_num_bytes(n).
Notes

Repro:

const S = crypto.subtle, b64u = u => Buffer.from(u).toString("base64url");
const kp = await S.generateKey({ name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256" }, true, ["encrypt","decrypt"]);
const N = Buffer.from((await S.exportKey("jwk", kp.publicKey)).n, "base64url");
for (const [label, n] of [["2047-bit", (x => (x[0] = 0x40 | (x[0] & 0x3f), x))(Uint8Array.from(N))], ["2041-bit", (x => (x[0] = 1, x))(Uint8Array.from(N))], ["2049-bit", new Uint8Array([1, ...N])]])
  console.log(label, await S.importKey("jwk", { kty: "RSA", n: b64u(n), e: "AQAB" }, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"]).then(k => k.algorithm.modulusLength, e => e.name));

bun 1.4.3 and main (11c3671): 2048 / 2048 / 2056. node v26.12.1: 2047 / 2041 / 2049. With this change: 2047 / 2041 / 2049. The run that found this reported 2047 and 2041 for Chromium 148 too.

The test imports public keys whose modulus has a rewritten leading byte. Such a modulus is not a product of the original primes, so the keys are useless for encryption, but public key import cannot check that and modulusLength only reads n.

The branch has main (73b5310) merged. There was no conflict.


[auto-merge] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/web/crypto/web-crypto.test.ts
bun test v1.4.3 (f42e98025)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [3.59ms]
(pass) Web Crypto > keeps event loop alive [351.45ms]
(pass) Web Crypto > has globals [3.14ms]
(pass) Web Crypto > should encrypt and decrypt [14.66ms]
(pass) Web Crypto > should verify and sign [38.43ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [15.40ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [10.67ms]
(pass) Web Crypto > unwrapKey JWK error handling > settles when JsonWebKey dictionary conversion itself throws [10.39ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [2483.93ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of aborting [310.08ms]
(pass) RSA-PSS saltLength > rejects values that do not fit in an int instead of treating them as the -1/-2 selectors [43.51
... (truncated)

release without fix: 1 FAILED
bun test v1.4.3-canary.1 (f42e98025)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [0.06ms]
(pass) Web Crypto > keeps event loop alive [8.87ms]
(pass) Web Crypto > has globals [0.06ms]
(pass) Web Crypto > should encrypt and decrypt [0.44ms]
(pass) Web Crypto > should verify and sign [0.67ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [0.52ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [0.20ms]
(pass) Web Crypto > unwrapKey JWK error handling > settles when JsonWebKey dictionary conversion itself throws [0.16ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [25.67ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of aborting [7.89ms]
(pass) RSA-PSS saltLength > rejects values that do not fit in an int instead of treating them as the -1/-2 selectors [6.86ms]
429 |     expect({
430 |       2048: await modulusLength(n2048),
431 |       2047: await modulusLength(withLeadingByte(0x40 | (n2048[0] & 0x3f))),
432 |       2041: awai
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/web/crypto/web-crypto.test.ts
bun test v1.4.3 (f42e98025)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [3.21ms]
(pass) Web Crypto > keeps event loop alive [258.62ms]
(pass) Web Crypto > has globals [3.20ms]
(pass) Web Crypto > should encrypt and decrypt [13.57ms]
(pass) Web Crypto > should verify and sign [36.03ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [15.29ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [9.80ms]
(pass) Web Crypto > unwrapKey JWK error handling > settles when JsonWebKey dictionary conversion itself throws [9.96ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [2469.31ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of aborting [371.47ms]
(pass) RSA-PSS saltLength > rejects values that do not fit in an int instead of treating them as the -1/-2 selectors [74.24ms
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 587ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/136] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 242 extern-C blocks audited
[2/136] gen cpp.rs (cppbind)
[3/136] gen JS modules (bundle-modules)
Preprocess modules (7464ms)
Bundle modules (48ms)
Postprocesss modules (26ms)
Bundle Functions (495ms)
Generate Code (35ms)

[8.08s] Bundled "src/js" for production
  2594 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[3/135] cargo bun_runtime → libbun_runtime.a
^[[1m^[[92m   Compiling^[[0m bun_mimalloc_sys v0.0.0 (/workspace/bun/src/mimalloc_sys)
^[[1m^[[92m   Compiling^[[0m bun_alloc v0.0.0 (/workspace/bun/src/bun_alloc)
^[[1m^[[92m   Compiling^[[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
^[[1m^[[92m   Compiling^[[0m bun_libdeflate_sys v0.0.0 (/workspace/bun/src/libdeflate_sys)
^[[1m^[[92m   Compiling^[[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
^[[1m^[[92m   Compiling^[[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
^[[1m^[[92m   Compiling^[[0m bun_boringssl_sys v0.0
... (truncated)
diff hotspot
src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp |  2 +-
 test/js/web/crypto/web-crypto.test.ts              | 33 ++++++++++++++++++++++
 2 files changed, 34 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                reads  edits  tests
src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp      1      1     20
test/js/web/crypto/web-crypto.test.ts                   6      6     19

…ngth

CryptoKeyRSA::algorithm() computed modulusLength as RSA_size() * 8, the
byte length of n times 8. For an imported key whose modulus is not a
multiple of 8 bits long (2047, 2041, 2049 bits) this reported 2048, 2048
and 2056. Node and Chromium report the bit length. Use RSA_bits().
@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2939125 and c78a76c.

Files selected for processing: 1
  • test/js/web/crypto/web-crypto.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

RSA modulus length now comes directly from OpenSSL’s RSA_bits() API. Web Crypto tests cover imported RSA JWK moduli of 2041, 2047, 2048, and 2049 bits.

Changes

RSA modulus length

Layer / File(s) Summary
Report RSA modulus bit lengths
src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp, test/js/web/crypto/web-crypto.test.ts
getRSAModulusLength uses RSA_bits(). Tests check the reported lengths for imported RSA JWK moduli.

Suggested reviewers: jarred-sumner, alii

Merge Risk: Minimal · up to c78a7

The change reports actual RSA modulus bit lengths and adds targeted import tests. No actionable merge-blocking risk was identified.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: reporting the RSA modulus bit length through RsaKeyAlgorithm.modulusLength.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description states that the change duplicates PR #47559 (bc55d56), which already landed the same one-line fix and test on main, so nothing remains to merge. It is otherwise complete: it explain…
Full details: Description check

Explanation

The description states that the change duplicates PR #47559 (bc55d56), which already landed the same one-line fix and test on main, so nothing remains to merge. It is otherwise complete: it explains the problem (modulusLength is computed as RSA_size * 8, which is byte length rounded up, not the bit length), gives the fix (RSA_bits), notes the downside (2047-bit imported keys now report 2047), and includes verification (the new test fails on main and passes with the fix, plus vendored Node tests), with a repro. It covers the template's 'What does this PR do?' and 'How did you verify your code works?' content, though it is laid out as Problem/Fix/Background/Downsides rather than the template headings.

Finishing Touches
Fix failing CI checks
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — RSA_bits(rsa) is the correct primitive for the spec's "length, in bits, of the RSA modulus", and the only other consumers of this helper just compare against zero.

What was reviewed:

  • Confirmed getRSAModulusLength feeds only CryptoKeyRSA::algorithm() (the field being fixed) and keySizeInBits(), whose three RSA callers (RSA_OAEP, RSASSA_PKCS1_v1_5, RSA_PSS) all use it as a != 0 gate — no behavioral change there.
  • Test covers byte-aligned (2048), sub-byte (2047, 2041), and over-byte (2049) moduli; the last three would report 2048/2048/2056 under the old RSA_size*8 path, so the assertion is load-bearing.
  • Test placement and shape follow the repo conventions (existing file, single .toEqual on a composite, no network/timing).
Extended reasoning...

Overview

This PR changes one line in src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp: the static helper getRSAModulusLength(RSA*) now returns RSA_bits(rsa) instead of RSA_size(rsa) * 8. RSA_size returns the modulus size in bytes (rounded up), so multiplying by 8 loses the low bits of the true bit-length; RSA_bits returns BN_num_bits(n) directly. The WebCrypto spec defines RsaKeyAlgorithm.modulusLength as the bit-length of the modulus, so the new value is spec-correct and matches Node and Chromium. A new test block in test/js/web/crypto/web-crypto.test.ts imports JWK public keys with 2048/2047/2041/2049-bit moduli and asserts the exact reported bit-lengths.

Security risks

None introduced. The change is a read-only reporting fix on an already-parsed RSA*; no new input parsing, allocation, or trust boundary is touched. If anything it hardens callers that gate on modulusLength >= N, since a 2041-bit key will no longer masquerade as 2048-bit. RSA_bits on a null-n RSA is not reachable here because the helper's own null guard on rsa remains, and every path that produces this RSA* sets n.

Level of scrutiny

Low. This is a one-line substitution between two well-documented BoringSSL primitives with a clear semantic difference, applied at the layer that owns the invariant (the C++ helper feeding the algorithm dictionary). I grepped every caller of getRSAModulusLength and CryptoKeyRSA::keySizeInBits(): the former is used only in algorithm() (the target of the fix) and keySizeInBits(); the latter is called only in CryptoAlgorithmRSA_OAEP.cpp, CryptoAlgorithmRSASSA_PKCS1_v1_5.cpp, and CryptoAlgorithmRSA_PSS.cpp, each as if (!rsaKey.keySizeInBits()). Since RSA_bits and RSA_size*8 agree on zero-ness, those paths are unaffected. The EC/OKP keySizeInBits hits in the grep are unrelated types.

Other factors

The test is placed in the existing WebCrypto test file per repo convention, asserts a single composite object via .toEqual, and covers the variant matrix (exact byte boundary plus three non-byte-aligned cases that each fail under the old code — 2047→2048, 2041→2048, 2049→2056). No CODEOWNERS entry covers src/jsc/bindings/webcrypto/ or the test file. The bug hunt exited on dry_streak with no findings and no prior review activity on the PR.

… types

The test directory is type-checked without lib.dom, so the global
RsaKeyAlgorithm type does not exist there.
@robobun

robobun commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — a one-line fix that makes RsaKeyAlgorithm.modulusLength report the modulus bit length, matching Node and the spec.

What was reviewed:

  • Both callers of getRSAModulusLength: algorithm() (the reported dictionary) and keySizeInBits(), whose three RSA consumers only compare against zero, so no sign/encrypt/decrypt path changes.
  • Sibling sites: node:crypto's KeyObject already reports the bit count of n, and no other RSA_size8 / EVP_PKEY_size8 sites exist in src/, so this was the only byte-rounded reporter.
  • The new test's fixtures: the 256-byte modulus has its top bit set (2048), and the three rewrites produce exactly 2047/2041/2049-bit values; the assertion fails on the old code (2048/2048/2056).
Extended reasoning...

The change replaces RSA_size(rsa) * 8 with RSA_bits(rsa) in the static helper at src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp:45 and adds one describe block to test/js/web/crypto/web-crypto.test.ts. It touches the WebCrypto RSA key bindings, but only the reported modulusLength metadata; the keySizeInBits() consumers in the RSA-OAEP, RSASSA-PKCS1-v1_5 and RSA-PSS algorithms only check for a nonzero value, so no cryptographic operation is affected. The node:crypto KeyObject path already used the bit count of n, so the fix brings WebCrypto in line with the existing in-tree behavior rather than introducing a new convention. The diff is tiny, self-contained, and the new test exercises the exact boundary cases (2047, 2041, 2049 bits) that distinguish the old and new behavior; the commits since the earlier review only merged main and adjusted the test's type cast.

@robobun

robobun commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

This branch has main (73b5310) merged. There was no conflict.

CI on c78a76c (build 134795) is finished. 187 jobs passed. No failure is in a WebCrypto test, and none comes from this change:

  • One darwin x64 - test-bun shard failed in test/cli/check/check.test.ts, a test of the type checker ("finishes within 10 s on conditions nested deeper than the stack allows", bun check exited with 137). The same test needed a retry on ubuntu 25.04 x64 in this build, and also in build 134837 of another branch.
  • Two windows 11 aarch64 - test-bun shards did not run their tests. They stopped with buildkite-agent artifact download timed out after 120s for step 'windows-aarch64-build-bun'.
  • Six test-aot-drawn jobs failed in test/aot/. That lane is soft-fail, and it fails on main too (for example build 134732).

The diff is one line (RSA_size(rsa) * 8 to RSA_bits(rsa)) and its test. It is ready for a maintainer. A retry of the three failed shards completes the run.

@robobun

robobun commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Main covers this PR.

PR #47559 (bc55d56) made the same one-line change in getRSAModulusLength (RSA_size(rsa) * 8 to RSA_bits(rsa), src/jsc/bindings/webcrypto/CryptoKeyRSAOpenSSL.cpp). The file on main is byte for byte the file of this PR. It also added the test "JWK import validation > reports the modulus length in bits" to test/js/web/crypto/web-crypto.test.ts.

I checked this on a debug build of bc55d56:

  • The test of this PR ("RsaKeyAlgorithm.modulusLength of an imported key") passes there: 2048, 2047, 2041 and 2049 bits.
  • A JWK with a 2,047-bit modulus reports modulusLength 2047, as Node v26.3.0 does. Bun 1.4.3 (1.4.3-canary.1+367d939d9) reports 2048.

This PR has nothing that main lacks. The one difference is that its test has three more modulus sizes than the test on main (2048, 2041, 2049). They go through the same line.

I did not rebase it, because the rebase leaves only those three sizes in a test. A maintainer can close it.

@robobun

robobun commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this is covered by #47559, which landed the same change to getRSAModulusLength on main (bc55d56) with a test.

@robobun robobun closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants