Skip to content

webcrypto: reject JWK with duplicate key_ops on importKey/unwrapKey - #33902

Open
robobun wants to merge 3 commits into
mainfrom
farm/4090b82c/jwk-key-ops-duplicates
Open

webcrypto: reject JWK with duplicate key_ops on importKey/unwrapKey#33902
robobun wants to merge 3 commits into
mainfrom
farm/4090b82c/jwk-key-ops-duplicates

Conversation

@robobun

@robobun robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What

crypto.subtle.importKey("jwk", ...) and crypto.subtle.unwrapKey(..., "jwk", ...) accepted JWKs whose key_ops array contains duplicate entries. RFC 7517 section 4.3 states "Duplicate key operation values MUST NOT be present in the array", and the WebCrypto spec requires a DataError when key_ops is invalid per RFC 7517. Node.js and Chrome both reject these.

Reproduction

const k = Buffer.from(new Uint8Array(16).fill(9)).toString("base64url");
await crypto.subtle.importKey(
  "jwk",
  { kty: "oct", k, ext: true, key_ops: ["encrypt", "encrypt", "decrypt"] },
  { name: "AES-GCM" },
  true,
  ["encrypt"],
);
// Bun before: resolves with a CryptoKey
// Node 26 / Chrome: DataError "Duplicate key operation"

The same gap held for every key class (oct/AES, oct/HMAC, EC, OKP, RSA) and for the unwrapKey JWK path.

Cause

normalizeJsonWebKey in SubtleCrypto.cpp collapsed key_ops directly into a usage bitmap via toCryptoKeyUsageBitmap, so by the time the per-class importers (CryptoKeyAES, CryptoKeyEC, CryptoKeyHMAC, CryptoKeyOKP, CryptoKeyRSA) ran their (keyData.usages & usages) != usages check, duplicates were already merged into a single bit and structurally undetectable. The source carried a "Maybe we shouldn't silently bypass duplicated usages?" note at exactly this spot.

Fix

normalizeJsonWebKey now builds the bitmap itself and returns false the first time a bit repeats. Both call sites (the toKeyData JWK branch used by importKey, and the JWK branch inside the unwrapKey decrypt callback) reject the promise with DataError on that result, matching the surrounding error handling for the adjacent JWK parse failures.

The usages argument to importKey/unwrapKey itself is untouched and still permits duplicates, which matches the spec and Node.

Verification

New tests in test/js/web/crypto/web-crypto.test.ts cover importKey across all five key classes, the unwrapKey path, the non-duplicate control cases, and that the usages argument still accepts duplicates.

USE_SYSTEM_BUN=1 bun test test/js/web/crypto/web-crypto.test.ts -t "key_ops duplicate"
  2 fail (aes/hmac/ec/okp/rsa all "imported"; unwrap dup "imported")

bun bd test test/js/web/crypto/web-crypto.test.ts
  25 pass, 0 fail

[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/crypto/web-crypto.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (e9e7fbfac)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [5.26ms]
(pass) Web Crypto > keeps event loop alive [555.29ms]
(pass) Web Crypto > has globals [3.22ms]
(pass) Web Crypto > should encrypt and decrypt [14.19ms]
(pass) Web Crypto > should verify and sign [44.96ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [17.19ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [11.61ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [2582.48ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of abort
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9c0814d55)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [0.07ms]
(pass) Web Crypto > keeps event loop alive [13.19ms]
(pass) Web Crypto > has globals [0.06ms]
(pass) Web Crypto > should encrypt and decrypt [0.41ms]
(pass) Web Crypto > should verify and sign [0.77ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [0.31ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [0.46ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [29.82ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of aborting [12.73ms]
(pass) Ed25519 > generateKey > should return CryptoKeys without namedCurve in algorithm field [0.17ms]
(pass) AES-KW wrapKey/unwrapKey with jwk format > round-trips an HMAC SHA-256 key [0.21ms]
(pass) AES-KW wrapKey/unwrapKey with jwk format > round-trips an HMAC SHA-384 key [0.05ms]
(pass) AES-KW wrapKey/unwrapKey with jwk format > round-trips an HMAC SHA-512 key [0.04ms]
(pass) AES-KW wrapKey/unwrapKey with 
... (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/mechgate.xml" test/js/web/crypto/web-crypto.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (e9e7fbfac)

test/js/web/crypto/web-crypto.test.ts:
(pass) crypto.subtle setter should not throw [4.95ms]
(pass) Web Crypto > keeps event loop alive [470.79ms]
(pass) Web Crypto > has globals [3.15ms]
(pass) Web Crypto > should encrypt and decrypt [13.99ms]
(pass) Web Crypto > should verify and sign [45.27ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are not valid JSON [23.46ms]
(pass) Web Crypto > unwrapKey JWK error handling > rejects when wrapped bytes are valid JSON but not a valid JWK [20.03ms]
(pass) Web Crypto > unwrapKey JWK error handling > does not leak DeferredPromise in m_pendingPromises on JWK parse errors [2488.79ms]
(pass) oversized inputs > rejects >2 GiB inputs instead of abort
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 663ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/8] cxx obj/src/jsc/bindings/webcrypto/SubtleCrypto.cpp.o
[2/8] gen generated_host_exports.rs
generated_host_exports.rs: 90 exports (host=3, lazy=10, generic=77, rust=0); 248 extern-C blocks audited
[3/8] gen cpp.rs (cppbind)
[3/8] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/sr
... (truncated)
diff hotspot
src/jsc/bindings/webcrypto/SubtleCrypto.cpp |  33 +++++--
 test/js/web/crypto/web-crypto.test.ts       | 134 ++++++++++++++++++++++++++++
 2 files changed, 161 insertions(+), 6 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                         reads  edits  tests
src/jsc/bindings/webcrypto/SubtleCrypto.cpp      4      3      0
test/js/web/crypto/web-crypto.test.ts            1      1      0

RFC 7517 section 4.3 requires "Duplicate key operation values MUST NOT
be present in the array", and WebCrypto importKey specifies that a
key_ops member invalid per RFC 7517 is a DataError. normalizeJsonWebKey
folded key_ops straight into a bitmap, which made duplicates invisible
to the per-key-class importers that only compare bitmasks.

normalizeJsonWebKey now detects a bit that is already set while building
the bitmap and reports failure; both call sites (importKey and the JWK
branch of unwrapKey) reject the promise with DataError on that result.
The usages argument to importKey itself is unchanged and still permits
duplicates, matching the spec and Node.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73bb8788-161c-44c7-a74e-4e2fb12e9f01

📥 Commits

Reviewing files that changed from the base of the PR and between f6e084f and 9c0814d.

📒 Files selected for processing (2)
  • src/jsc/bindings/webcrypto/SubtleCrypto.cpp
  • test/js/web/crypto/web-crypto.test.ts

Walkthrough

Changes

JWK key_ops validation

Layer / File(s) Summary
Validate JWK key operations
src/jsc/bindings/webcrypto/SubtleCrypto.cpp
normalizeJsonWebKey detects duplicate key_ops values, while JWK import and unwrap paths reject them with DataError.
Test import and unwrap rejection
test/js/web/crypto/web-crypto.test.ts
Tests cover duplicate and non-duplicate key_ops values across multiple key types, import usages, and wrapped JWKs.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main WebCrypto JWK duplicate key_ops rejection change.
Description check ✅ Passed The description covers the change and verification, though it uses custom headings instead of the exact template headings.
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.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:27 AM PT - Jul 10th, 2026

@robobun, your commit e9e7fbf has 1 failures in Build #71413 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33902

That installs a local version of the PR into your bun-33902 executable, so you can run:

bun-33902 --bun

@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 — small spec-compliance fix that mirrors the adjacent JWK error paths, with thorough test coverage.

Extended reasoning...

Overview

This PR tightens JWK validation in SubtleCrypto.cpp: normalizeJsonWebKey now returns false when key_ops contains duplicate entries, and both call sites (toKeyData for importKey, and the JWK branch inside the unwrapKey decrypt callback) reject with DataError on that result. Tests in test/js/web/crypto/web-crypto.test.ts cover all five key classes, the unwrapKey path, non-duplicate control cases, and confirm the usages argument itself still permits duplicates.

Security risks

None introduced. This is a fail-closed tightening of input validation per RFC 7517 §4.3 and the WebCrypto spec, aligning Bun with Node.js and Chrome. It rejects previously-accepted malformed JWKs rather than accepting anything new. No changes to key material handling, TLS, or crypto primitives.

Level of scrutiny

Low-to-medium. The C++ change is ~15 lines and mechanically follows established patterns: the new unwrapKey error branch is byte-for-byte identical in shape to the two adjacent error branches (!jwkObject and scope.exception()) — same m_pendingPromises.remove(index) + promise->reject(...) + return sequence. The toKeyData change reuses the existing std::nullopt-means-rejected contract that the importKey caller already handles. The bitmap loop is trivially correct since CryptoKeyUsage is a closed WebIDL enum and toCryptoKeyUsageBitmap(usage) maps each value to exactly one bit.

Other factors

The change addresses an existing "Maybe we shouldn't silently bypass duplicated usages?" comment at exactly this location. Test coverage is comprehensive with positive and negative controls, and the PR description shows the tests fail on system Bun and pass on the debug build. No CODEOWNERS match this path. No outstanding reviewer comments.

@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 — small, spec-mandated validation tightening with thorough test coverage.

Extended reasoning...

Overview

This PR changes normalizeJsonWebKey in src/jsc/bindings/webcrypto/SubtleCrypto.cpp to detect duplicate entries in a JWK's key_ops array and return false, and updates its two call sites (the toKeyData JWK branch used by importKey, and the JWK branch inside the unwrapKey decrypt callback) to reject with DataError when that happens. It adds ~130 lines of tests to test/js/web/crypto/web-crypto.test.ts covering all five key classes, the unwrapKey path, non-duplicate controls, and that the usages argument still accepts duplicates.

Security risks

None. This is validation tightening only: previously-accepted malformed input (duplicate key_ops) is now rejected per RFC 7517 §4.3 and the WebCrypto spec, matching Node.js and Chrome. No new code paths accept input; no key material handling changes.

Level of scrutiny

Low-to-medium. While this touches WebCrypto, the change is mechanical and narrowly scoped: key_ops is already a Vector<CryptoKeyUsage> (enum values validated by WebIDL conversion), so each element maps to exactly one bit and result & bit is a correct duplicate check. Both new error paths mirror the adjacent error branches exactly — including the m_pendingPromises.remove(index) cleanup in unwrapKey that prevents the DeferredPromise leak covered by an existing test in the same file.

Other factors

The bug-hunting system found no issues. The PR description includes fails-on-main / passes-on-PR evidence for both debug+ASAN and release builds. Test coverage is comprehensive with positive and negative controls. No CODEOWNERS entry covers this path, and there are no outstanding human reviewer comments.

@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is green; remaining CI failures are unrelated flake.

  • Gate (robobun/evidence) passes: the new web-crypto.test.ts tests fail without the SubtleCrypto.cpp change and pass with it on both ASAN and release builds.
  • test/js/web/crypto/web-crypto.test.ts passes on every lane in both runs.
  • Build 71404 failed on test-worker-message-port-transfer-terminate.js (SIGABRT, linux-x64-asan), a known issue tracked in worker_threads: don't abort when terminate() interrupts a lazy property builder #33418 and seen on other branches.
  • Build 71413 failed on terminal.test.ts "creates subprocess with terminal attached" (90s timeout, darwin-x64).

Neither has any code path in common with src/jsc/bindings/webcrypto/SubtleCrypto.cpp. Ready for a maintainer to merge.

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.

1 participant