From 8d442ccae0d89ccd3f38bb5c8a68986c772e9ef1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:34:50 +0000 Subject: [PATCH] node:crypto: default to SHA256 for EC/DSA keys in one-shot sign/verify with null algorithm When crypto.sign(null, data, ecKey) or crypto.verify(null, ...) is called with an EC or DSA key, OpenSSL v3 (used by Node.js) queries the key's default digest via OSSL_PKEY_PARAM_DEFAULT_DIGEST and receives SHA256. BoringSSL has no such fallback and fails EVP_DigestSignInit with EVP_R_NO_DEFAULT_DIGEST. Bun already worked around this for RSA keys by explicitly selecting SHA256 when the algorithm argument is null or undefined. Extend the same treatment to EC and DSA keys (isSigVariant) so the one-shot sign and verify paths match Node.js. --- .../bindings/node/crypto/CryptoSignJob.cpp | 12 ++++-- ...11029-crypto-verify-null-algorithm.test.ts | 40 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp index 972328ee33e..c17e0ac74e8 100644 --- a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp +++ b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp @@ -388,22 +388,26 @@ std::optional SignJobCtx::fromJS(JSGlobalObject* globalObject, Throw // return 0; // } // + // EC and DSA providers do the same thing: + // - providers/implementations/keymgmt/ec_kmgmt.c returns "SHA256" + // - providers/implementations/keymgmt/dsa_kmgmt.c returns DSA_DEFAULT_MD ("SHA256") + // // BoringSSL Difference: // ===================== // BoringSSL (used by Bun) does not have this automatic default mechanism. - // When NULL is passed as the digest to EVP_DigestVerifyInit for RSA keys, + // When NULL is passed as the digest to EVP_DigestVerifyInit for RSA/EC/DSA keys, // BoringSSL returns error 0x06000077 (NO_DEFAULT_DIGEST). // // This Fix: // ========= // To achieve Node.js/OpenSSL compatibility, we explicitly set SHA256 as the - // default digest for RSA keys when no algorithm is specified, matching the - // OpenSSL behavior documented above. + // default digest for RSA, EC, and DSA keys when no algorithm is specified, + // matching the OpenSSL behavior documented above. // // For Ed25519/Ed448 keys (one-shot variants), we intentionally leave digest // as null since these algorithms perform their own hashing internally and // don't require a separate digest algorithm. - if (keyObject.asymmetricKey().isRsaVariant()) { + if (keyObject.asymmetricKey().isRsaVariant() || keyObject.asymmetricKey().isSigVariant()) { digest = Digest::FromName("SHA256"_s); } } diff --git a/test/regression/issue/11029-crypto-verify-null-algorithm.test.ts b/test/regression/issue/11029-crypto-verify-null-algorithm.test.ts index e8ce993cfc7..d7a284ccd12 100644 --- a/test/regression/issue/11029-crypto-verify-null-algorithm.test.ts +++ b/test/regression/issue/11029-crypto-verify-null-algorithm.test.ts @@ -1,5 +1,6 @@ -import { expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import crypto from "crypto"; +import { promisify } from "util"; // Regression test for issue #11029 // crypto.verify() should support null/undefined algorithm parameter @@ -107,6 +108,43 @@ test("crypto.verify cross-verification between null and explicit SHA256", () => expect(isVerifiedWithSHA256).toBe(true); }); +describe.each(["prime256v1", "secp384r1", "secp521r1"])("crypto.sign/verify with null algorithm for EC (%s)", curve => { + const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: curve }); + const data = Buffer.from("test data"); + + test.each([null, undefined])("sync sign(%p)", algorithm => { + const signature = crypto.sign(algorithm, data, privateKey); + expect(signature).toBeInstanceOf(Buffer); + expect(crypto.verify(algorithm, data, publicKey, signature)).toBe(true); + expect(crypto.verify(algorithm, Buffer.from("wrong data"), publicKey, signature)).toBe(false); + }); + + test("async sign(null)", async () => { + const signature = await promisify(crypto.sign)(null, data, privateKey); + expect(signature).toBeInstanceOf(Buffer); + expect(await promisify(crypto.verify)(null, data, publicKey, signature)).toBe(true); + }); + + test("null defaults to SHA256 (cross-verification)", () => { + // Node.js/OpenSSL default the digest to SHA256 for EC keys when algorithm is null. + const signedWithSha256 = crypto.sign("sha256", data, privateKey); + expect(crypto.verify(null, data, publicKey, signedWithSha256)).toBe(true); + + const signedWithNull = crypto.sign(null, data, privateKey); + expect(crypto.verify("sha256", data, publicKey, signedWithNull)).toBe(true); + }); + + test("sign(null) with ieee-p1363 encoding", () => { + const signature = crypto.sign(null, data, { key: privateKey, dsaEncoding: "ieee-p1363" }); + expect(signature).toBeInstanceOf(Buffer); + expect(crypto.verify(null, data, { key: publicKey, dsaEncoding: "ieee-p1363" }, signature)).toBe(true); + }); + + test("verify(null) returns false for a bad signature instead of throwing", () => { + expect(crypto.verify(null, data, publicKey, Buffer.alloc(70))).toBe(false); + }); +}); + test("crypto.createVerify should also work with RSA keys", () => { const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048,