Skip to content

fix(deno_crypto): return errors instead of aborting on malformed WebCrypto keys - #727

Merged
nyannyacha merged 1 commit into
mainfrom
fix/crypto-import-key-panics
Aug 31, 2026
Merged

fix(deno_crypto): return errors instead of aborting on malformed WebCrypto keys#727
nyannyacha merged 1 commit into
mainfrom
fix/crypto-import-key-panics

Conversation

@nyannyacha

Copy link
Copy Markdown
Contributor

What

Two crypto.subtle paths in deno_crypto 0.196.0 panic on user-supplied key material. The panics happen inside op functions that cannot unwind, so they abort the whole process instead of surfacing a JS exception — a single edge function can take the runtime down.

1. EC importKey("pkcs8", ...)import_key.rs:683 unwraps the conversion of the PKCS#8 AlgorithmIdentifier parameters into a named-curve OID. A key that encodes explicit EC parameters (a SEQUENCE) instead of a curve OID aborts:

thread 'tokio-runtime-worker' panicked at deno_crypto-0.196.0/import_key.rs:683:10:
called `Result::unwrap()` on an `Err` value: Error { kind: TagUnexpected {
  expected: Some(Tag(0x06: OBJECT IDENTIFIER)), actual: Tag(0x30: SEQUENCE) }, position: None }
...
panic in a function that cannot unwind

2. importKey("raw", ...) for X25519/X448/Ed25519 — the key length is never validated, so a shorter key (e.g. an empty Uint8Array) is stored as-is and the subsequent deriveBits aborts in op_crypto_derive_bits_{x25519,x448} when converting the slice to a fixed-size array. Same shape, found while auditing for the pattern above.

How

Both are fixed upstream — denoland/deno#32410 and denoland/deno#33944 — but the releases carrying them (deno v2.7.10+) require a deno_core bump we can't take here. So this vendors deno_crypto 0.196.0 under vendor/ (same pattern as deno_fetch/deno_http/deno_telemetry) and cherry-picks just those two fixes.

The vendored tree is otherwise byte-identical to the published crate. The Rust diff against pristine 0.196.0 is:

  • import_key.rs — one line, .unwrap().map_err(|_| ImportKeyError::MalformedParameters)?
  • x25519.rs / x448.rsderive_bits returns Result instead of expect-ing, plus a new InvalidKeyLength variant
  • 00_crypto.js — length validation on the raw/jwk import paths (X448's jwk path was also missing the op_crypto_base64url_decode try/catch that X25519/Ed25519 already had)

deno/runtime/errors.rs maps the new variants to DOMExceptionDataError.

The remaining 26 panic sites in the crate were checked and are unreachable: the ECDH pk.unwrap()s in lib.rs sit behind is_some() guards, and PBKDF2's NonZeroU32::new().unwrap() / assert!(length % 8 == 0) are guarded in JS, which throws OperationError first.

Verification

Built locally and driven through a function that hits each path.

Before (fix reverted, everything else identical) — one request kills the process:

19: deno_crypto::import_key::op_crypto_import_key::op_crypto_import_key::v8_fn_ptr
      at vendor/deno_crypto/import_key.rs:154:1
thread caused non-unwinding panic. aborting.

After — DOMExceptions, process survives repeated hits, valid keys unaffected:

ec-pkcs8-seq-params:  DOMExceptionDataError - malformed parameters
x25519-raw-short:     DOMException DataError - Invalid key data
x448-raw-short:       DOMException DataError - Invalid key data
ed25519-raw-short:    DOMException DataError - Invalid key data
ec-pkcs8-valid:       OK (round-trip import succeeded)
x25519-valid:         OK (32 bytes)

cargo check --workspace is clean.

🤖 Generated with Claude Code

…rypto keys

Two `crypto.subtle` paths in deno_crypto 0.196.0 panic on user-supplied key
material. Because the panics happen inside op functions that cannot unwind,
they abort the whole process rather than surfacing a JS exception, so a single
edge function can take the runtime down.

1. `importKey("pkcs8", ...)` for EC keys unwraps the conversion of the PKCS#8
   AlgorithmIdentifier parameters into a named-curve OID. A key that encodes
   explicit EC parameters (a SEQUENCE) instead of a curve OID aborts with:

     called `Result::unwrap()` on an `Err` value: Error { kind: TagUnexpected {
       expected: Some(Tag(0x06: OBJECT IDENTIFIER)), actual: Tag(0x30: SEQUENCE) } }

2. `importKey("raw", ...)` for X25519/X448/Ed25519 never validates the key
   length, so a shorter key (e.g. an empty Uint8Array) is stored as-is and the
   subsequent `deriveBits` aborts in `op_crypto_derive_bits_{x25519,x448}` when
   converting the slice to a fixed-size array.

Both are fixed upstream (denoland/deno#32410 and denoland/deno#33944), but the
releases carrying them require a deno_core bump we cannot take here, so vendor
deno_crypto 0.196.0 and cherry-pick the two fixes. The vendored tree is
otherwise byte-identical to the published crate.

Malformed input now raises a DataError DOMException and the worker survives.
Valid keys are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nyannyacha
nyannyacha merged commit ca66c0e into main Aug 31, 2026
5 checks passed
@nyannyacha
nyannyacha deleted the fix/crypto-import-key-panics branch August 31, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants