Files
bun.sh/test/regression/issue/24399.test.ts
robobun df5d0fcfa1 fix: ensure EC private key JWK "d" field has correct length (#24400)
## Summary

Fixes incorrect JWK "d" field length for exported elliptic curve private
keys. The "d" field is now correctly padded to ensure RFC 7518
compliance.

## Problem

When exporting EC private keys to JWK format, the "d" field would
sometimes be shorter than required by RFC 7518 because
`convertToBytes()` doesn't pad the result when the BIGNUM has leading
zeros. This caused incompatibility with Chrome's strict validation,
though Node.js and Firefox would accept the malformed keys.

Expected lengths per RFC 7518:
- P-256: 32 bytes → 43 base64url characters
- P-384: 48 bytes → 64 base64url characters  
- P-521: 66 bytes → 88 base64url characters

## Solution

Changed `src/bun.js/bindings/webcrypto/CryptoKeyECOpenSSL.cpp:420` to
use `convertToBytesExpand(privateKey, keySizeInBytes)` instead of
`convertToBytes(privateKey)`, ensuring the private key is padded with
leading zeros when necessary. This matches the behavior already used for
the x and y public key coordinates.

## Test plan

-  Added regression test in `test/regression/issue/24399.test.ts` that
generates multiple keys for each curve and verifies correct "d" field
length
-  Test fails with `USE_SYSTEM_BUN=1 bun test` (reproduces the bug)
-  Test passes with `bun bd test` (verifies the fix)
-  Existing crypto tests pass

Fixes #24399

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-05 13:49:13 -08:00

54 lines
2.2 KiB
TypeScript

import { expect, test } from "bun:test";
const CURVE_CONFIGS = [
{ curve: "P-256", expectedLength: 43 }, // 32 bytes = 43 base64url characters
{ curve: "P-384", expectedLength: 64 }, // 48 bytes = 64 base64url characters
{ curve: "P-521", expectedLength: 88 }, // 66 bytes = 88 base64url characters
] as const;
test("ECDSA exported JWK fields have correct length", async () => {
for (const { curve, expectedLength } of CURVE_CONFIGS) {
// Generate 10 keys to ensure we catch padding issues (which occur ~50% of the time for P-521)
for (let i = 0; i < 10; i++) {
const { privateKey } = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: curve }, true, ["sign"]);
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
expect(jwk.d).toBeDefined();
expect(jwk.d!.length).toBe(expectedLength);
expect(jwk.x!.length).toBe(expectedLength);
expect(jwk.y!.length).toBe(expectedLength);
}
}
});
test("ECDH exported JWK fields have correct length", async () => {
for (const { curve, expectedLength } of CURVE_CONFIGS) {
// Generate 10 keys to ensure we catch padding issues
for (let i = 0; i < 10; i++) {
const { privateKey } = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: curve }, true, ["deriveBits"]);
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
expect(jwk.d).toBeDefined();
expect(jwk.d!.length).toBe(expectedLength);
expect(jwk.x!.length).toBe(expectedLength);
expect(jwk.y!.length).toBe(expectedLength);
}
}
});
test("exported JWK can be re-imported and used for signing", async () => {
const { privateKey } = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, true, ["sign"]);
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
expect(jwk.d!.length).toBe(88);
// Re-import the key
const importedKey = await crypto.subtle.importKey("jwk", jwk, { name: "ECDSA", namedCurve: "P-521" }, true, ["sign"]);
// Verify we can use it for signing
const data = new TextEncoder().encode("test data");
const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-384" }, importedKey, data);
expect(signature.byteLength).toBeGreaterThan(0);
});