Skip to content

webcrypto: reject importKey("jwk") with a missing or wrong kty with DataError - #32901

Closed
robobun wants to merge 4 commits into
mainfrom
farm/5d960045/jwk-kty-dataerror
Closed

webcrypto: reject importKey("jwk") with a missing or wrong kty with DataError#32901
robobun wants to merge 4 commits into
mainfrom
farm/5d960045/jwk-kty-dataerror

Conversation

@robobun

@robobun robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

crypto.subtle.importKey("jwk", ...) with a JWK object missing the kty member threw TypeError instead of DataError.

try {
  await crypto.subtle.importKey("jwk", { k: "AAECAwQFBgcICQoLDA0ODw" }, { name: "AES-GCM" }, true, ["encrypt"]);
} catch (e) { console.log(e.name); }
before after / Node.js / Chrome
output TypeError DataError

The W3C WebCrypto spec defines JsonWebKey.kty as an optional DOMString. Each algorithm's Import Key operation is then responsible for "If the kty field of jwk is not <expected>, then throw a DataError."

Bun inherited WebKit's IDL which marks kty as required, so the generated WebIDL dictionary conversion threw TypeError: Member JsonWebKey.kty is required and must be an instance of DOMString before the per-algorithm check ever ran. DataError is a DOMException and TypeError is a JS Error, so code that distinguishes "bad key material" (DataError: reject the key, try the next one in the JWKS) from "programmer error" (TypeError: crash) takes the wrong branch on every malformed JWK from an untrusted source.

Two changes:

  1. JSJsonWebKey.cpp: drop the required-member TypeError for kty. A missing kty leaves result.kty as a null String, which fails each algorithm's keyData.kty != "<expected>" check and surfaces DataError via the normal exceptionCallback(DataError, ...) path. JsonWebKey.idl loses required to stay in sync.
  2. CryptoKeyOKP.cpp: hoist the keyData.kty != "OKP" check above the curve switch in importJwkInternal. It previously lived only in the Ed25519 arm; the X25519 arm checked only crv, so with the WebIDL guard gone an X25519 JWK with a missing kty would have imported successfully instead of rejecting. This also fixes the pre-existing case where a wrong kty (for example "EC") was silently accepted for X25519.

How did you verify your code works?

In test/js/web/crypto/web-crypto.test.ts:

  • it.each over AES-GCM, HMAC, RSA-OAEP, ECDSA, Ed25519, X25519: missing kty rejects with DataError. The OKP rows use a well-formed 32-byte x so the missing kty is the only reason the import is rejected.
  • wrong kty: X25519: { kty: "EC", crv: "X25519", x } rejects with DataError. On current Bun this imports successfully, so this test fails without the CryptoKeyOKP.cpp change on its own.
  • The unwrapKey tests that used {foo:"bar"} to exercise the JWK dictionary-conversion exception path no longer do (that object converts cleanly now that kty is optional), so those fixtures switch to an invalid key_ops enum value. The {foo:"bar"} case is kept as coverage for the kty-less DataError path.
bun bd test test/js/web/crypto/web-crypto.test.ts    # 23 pass
USE_SYSTEM_BUN=1 bun test <same file>                # 8 fail (the new and changed assertions)

No regressions in the existing OKP JWK import coverage: test/js/deno/crypto/webcrypto.test.ts, test/js/bun/crypto/x25519-derive-bits.test.ts, and Node's test-webcrypto-derivebits-cfrg.js, test-webcrypto-derivekey-cfrg.js, test-webcrypto-sign-verify.js, and test-webcrypto-wrap-unwrap.js all pass.

The W3C WebCrypto spec defines JsonWebKey.kty as an optional dictionary
member (not `required`). Each algorithm's Import Key operation is then
responsible for checking 'If the kty field of jwk is not <expected>, then
throw a DataError'.

Bun inherited WebKit's IDL which marks kty as `required`, so the WebIDL
dictionary conversion threw a TypeError before the per-algorithm kty
check ever ran. Code distinguishing DataError (bad key material) from
TypeError (programmer error) took the wrong branch.

Drop the required-member TypeError so a missing kty falls through to the
existing per-algorithm kty check, which already surfaces DataError for
every algorithm (AES, HMAC, RSA, EC, OKP).
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 credits.

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 90f1699d-eca7-4fd3-8396-511101f59ba0

📥 Commits

Reviewing files that changed from the base of the PR and between 8706328 and 7b24a67.

📒 Files selected for processing (4)
  • src/jsc/bindings/webcrypto/CryptoKeyOKP.cpp
  • src/jsc/bindings/webcrypto/JSJsonWebKey.cpp
  • src/jsc/bindings/webcrypto/JsonWebKey.idl
  • test/js/web/crypto/web-crypto.test.ts

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

@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:42 AM PT - Jun 28th, 2026

@robobun, your commit 7b24a67 has 1 failures in Build #66043 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32901

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

bun-32901 --bun

Comment thread src/jsc/bindings/webcrypto/JSJsonWebKey.cpp
Comment thread src/jsc/bindings/webcrypto/JSJsonWebKey.cpp
Comment thread test/js/web/crypto/web-crypto.test.ts
CryptoKeyOKP::importJwkInternal only compared keyData.kty against "OKP"
in the Ed25519 arm; the X25519 arm checked crv only. With kty no longer
required at the WebIDL layer, an X25519 JWK with a missing (or wrong)
kty would import successfully instead of rejecting with DataError.

Hoist the kty check above the curve switch so it covers both curves,
and drop required from kty in JsonWebKey.idl to keep the IDL in sync
with the hand-edited converter.

The unwrapKey tests that used {foo:"bar"} to exercise the dictionary
conversion error path no longer do (that object converts cleanly now),
so switch those fixtures to an invalid key_ops enum value and keep the
{foo:"bar"} case as coverage for the kty-less DataError path.
@robobun robobun changed the title webcrypto: reject importKey jwk with missing kty with DataError, not TypeError webcrypto: reject importKey("jwk") with a missing or wrong kty with DataError Jun 27, 2026
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

The review caught a real gap: CryptoKeyOKP::importJwkInternal only compared keyData.kty against "OKP" in the Ed25519 arm, so with the WebIDL required guard removed, an X25519 JWK with a missing kty would have imported successfully instead of rejecting. Confirmed on the unpatched build, where { kty: "EC", crv: "X25519", x } also imports successfully today (the same missing check, reachable without this PR).

af209fb addresses all three review comments:

  • Hoists the keyData.kty != "OKP" check above the curve switch in CryptoKeyOKP::importJwkInternal so it covers X25519 as well as Ed25519, with missing kty: X25519 and wrong kty: X25519 test cases. The wrong-kty case fails on current Bun (it imports), so it proves the hoist on its own.
  • Drops required from kty in JsonWebKey.idl so the IDL stays in sync with the converter.
  • The unwrapKey fixtures that relied on {foo:"bar"} to make the JWK dictionary conversion throw no longer did after this change. They now use an invalid key_ops enum value so they still exercise the scope.exception() / m_pendingPromises branch, and the {foo:"bar"} case is kept as coverage for the kty-less DataError path.

Existing OKP JWK coverage still passes: test/js/deno/crypto/webcrypto.test.ts, test/js/bun/crypto/x25519-derive-bits.test.ts, and Node's test-webcrypto-derivebits-cfrg.js, test-webcrypto-derivekey-cfrg.js, test-webcrypto-sign-verify.js, and test-webcrypto-wrap-unwrap.js.

@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.

Thanks — all three of my earlier points are addressed (hoisted OKP kty check + X25519 test rows, .idl synced, leak/regression fixtures swapped to key_ops:["bogus"]). I didn't find anything else; deferring to a human only because this loosens the WebIDL-level guard on a crypto key-import path.

Extended reasoning...

Overview

This PR changes crypto.subtle.importKey("jwk", ...) to reject a missing kty with DataError (a DOMException) instead of TypeError, matching the W3C spec, Node.js, and Chrome. It does so by removing the throwRequiredMemberTypeError branch for kty in the generated JsonWebKey dictionary converter (JSJsonWebKey.cpp), dropping required from JsonWebKey.idl, and — to close the gap that removal exposed — hoisting if (keyData.kty != "OKP"_s) return nullptr; above the curve switch in CryptoKeyOKP::importJwkInternal so X25519 now validates kty too. Tests add an it.each over six algorithms (including X25519) for missing kty, a wrong-kty X25519 case, and rework the adjacent unwrapKey regression tests so the dictionary-conversion-throws / m_pendingPromises leak path is still exercised via an invalid key_ops enum value.

Prior feedback resolved

In my earlier review I flagged (🔴) that removing the dictionary-level guard let an X25519 JWK with no kty import successfully, (🟡) that the .idl was now out of sync, and (🟡) that the {foo:"bar"} leak fixture no longer reached the scope.exception() branch it was guarding. All three are now addressed exactly as suggested, and the bug-hunting pass on the updated diff found nothing new. I re-checked that every CryptoKey*::importJwk (AES, HMAC, RSA, EC, OKP) has a kty != "<expected>" comparison, so no other algorithm relied on the removed WebIDL guard.

Security risks

This is in the WebCrypto key-import path, so it is security-adjacent. The net effect is strictly tightening or error-type-only: every input that was previously rejected is still rejected (just with DataError instead of TypeError), and X25519 now additionally rejects wrong/missing kty where it previously accepted a wrong kty. No cryptographic operation, key material handling, or accept-set is widened. I don't see an exploit vector here.

Level of scrutiny

Medium. The diff is small (~5 lines of production code plus tests) and mechanically follows the spec text, but it removes a blanket input-validation guard in favor of per-algorithm checks, which is the kind of change where a missed branch silently accepts malformed input. I already caught one such branch (X25519) in the first round and it's now fixed and tested; a second pair of eyes on the per-algorithm coverage seems prudent given the area.

Other factors

The one CI failure (test/js/node/tls/node-tls-connect.test.ts) is unrelated to this change. No CODEOWNERS entry covers src/jsc/bindings/webcrypto/. Test coverage for the new behavior is thorough and includes a Node/Chrome-parity assertion in the PR description.

@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 — all three review points from the earlier pass are addressed in af209fb, and every JWK import path now has its own kty check.

Extended reasoning...

Overview

Four files: drop the required-member TypeError for kty in JSJsonWebKey.cpp (3 lines), hoist the keyData.kty != "OKP" check above the curve switch in CryptoKeyOKP.cpp so it covers X25519 as well as Ed25519 (5 lines moved), drop required from JsonWebKey.idl (1 word), and add/adjust tests in web-crypto.test.ts. The net behavior change is (a) missing kty now rejects with DataError instead of TypeError — matching the W3C spec, Node.js, and Chrome — and (b) X25519 JWK import with a wrong kty (e.g. "EC") now rejects instead of being silently accepted, which fixes a pre-existing validation gap.

Security risks

This is WebCrypto input validation, not key material or cipher logic. The change is net-tightening: a previously-accepted malformed X25519 JWK is now rejected, and the missing-kty case still rejects (just with a different, spec-correct error type). I re-grepped every importJwk path (CryptoKeyAES, CryptoKeyHMAC, CryptoKeyRSA, CryptoKeyEC, CryptoKeyOKP) and each now compares keyData.kty against its expected literal, so removing the dictionary-level guard leaves no algorithm uncovered. No new attack surface.

Level of scrutiny

Moderate — crypto-adjacent but a small, mechanical, spec-aligned validation change. The production diff is ~10 lines and the logic is a straightforward string comparison whose failure path (return nullptrexceptionCallback(DataError)) is the existing, well-exercised one for every other invalid JWK field.

Other factors

My earlier review flagged the X25519 gap, the IDL drift, and the lost scope.exception() regression coverage; af209fb addresses all three exactly as suggested, the inline comments are resolved, and the bug-hunting re-run on the current revision found nothing. Test coverage is solid: an it.each over six algorithms for missing kty, a dedicated wrong-kty X25519 case that fails on current Bun, and the unwrapKey leak regression test was migrated to a fixture (key_ops: ["bogus"]) that still throws inside the dictionary converter. The author also reports the existing OKP/WebCrypto suites (Deno, Node parallel tests, x25519-derive-bits) still pass.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Status for a maintainer: the change is complete and reviewed; the red CI is infrastructure, not this diff.

Across all three Buildkite runs of this PR (65692, 65751, 66043), no crypto test ever failed, and test/js/web/crypto/web-crypto.test.ts passed on every lane that ran it, including ASAN. The only recurring error is test/js/node/tls/node-tls-connect.test.ts ("should have peer certificate"), which asserts cert.infoAccess["OCSP - URI"] on the certificate served by the live bun.sh host. It failed in all three runs on the x64-asan lane and passes locally on the same commit, so it depends on which certificate that agent's network path is served, not on anything here. The remaining failures were one-off externals (a live codeload.github.com 400, MySQL container startup, the Docker-backed autobahn suite) or Buildkite agents expiring before their jobs ran.

For context: none of the 15 most recently finished builds on this pipeline passed, on any branch, each failing on a different set of external-service or infrastructure tests.

I am not going to keep pushing empty commits to re-run CI. Locally, bun bd test test/js/web/crypto/web-crypto.test.ts passes (23 tests), USE_SYSTEM_BUN=1 fails the 8 new and changed assertions as expected, and the Deno, Node CFRG, and x25519 WebCrypto suites pass. Ready for a maintainer; a pipeline rebuild should be all that is needed.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: both parts of this change landed separately.

Verified on current main (bdb7382): test/js/web/crypto/web-crypto.test.ts from this branch, run unmodified against a debug build of main, passes (23 pass, two consecutive runs); the seven cases this PR adds (missing kty: AES-GCM/HMAC/RSA-OAEP/ECDSA/Ed25519/X25519 and wrong kty: X25519) all pass when run on their own.

@robobun robobun closed this Aug 13, 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.

1 participant