Files
bun.sh/test/regression/issue/11029-crypto-verify-null-algorithm.test.ts
robobun f78d197523 Fix crypto.verify() with null/undefined algorithm for RSA keys (#22331)
## Summary
Fixes #11029 - `crypto.verify()` now correctly handles null/undefined
algorithm parameter for RSA keys, matching Node.js behavior.

## Problem
When calling `crypto.verify()` with a null or undefined algorithm
parameter, Bun was throwing an error:
```
error: error:06000077:public key routines:OPENSSL_internal:NO_DEFAULT_DIGEST
```

## Root Cause
The issue stems from the difference between OpenSSL (used by Node.js)
and BoringSSL (used by Bun):
- **OpenSSL v3**: Automatically provides SHA256 as the default digest
for RSA keys when NULL is passed
- **BoringSSL**: Returns an error when NULL digest is passed for RSA
keys

## Solution
This fix explicitly sets SHA256 as the default digest for RSA keys when
no algorithm is specified, achieving OpenSSL-compatible behavior.

## OpenSSL v3 Source Code Analysis

I traced through the OpenSSL v3 source code to understand exactly how it
handles null digests:

### 1. Entry Point (`crypto/evp/m_sigver.c`)
When `EVP_DigestSignInit` or `EVP_DigestVerifyInit` is called with NULL
digest:
```c
// Lines 215-220 in do_sigver_init function
if (mdname == NULL && !reinit) {
    if (evp_keymgmt_util_get_deflt_digest_name(tmp_keymgmt, provkey,
                                               locmdname,
                                               sizeof(locmdname)) > 0) {
        mdname = canon_mdname(locmdname);
    }
}
```

### 2. Default Digest Query (`crypto/evp/keymgmt_lib.c`)
```c
// Lines 533-571 in evp_keymgmt_util_get_deflt_digest_name
params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_DEFAULT_DIGEST,
                                            mddefault, sizeof(mddefault));
if (!evp_keymgmt_get_params(keymgmt, keydata, params))
    return 0;
```

### 3. RSA Provider Implementation
(`providers/implementations/keymgmt/rsa_kmgmt.c`)
```c
// Line 54: Define the default
#define RSA_DEFAULT_MD "SHA256"

// Lines 351-355: Return it for RSA keys
if ((p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_DEFAULT_DIGEST)) != NULL
    && (rsa_type != RSA_FLAG_TYPE_RSASSAPSS
        || ossl_rsa_pss_params_30_is_unrestricted(pss_params))) {
    if (!OSSL_PARAM_set_utf8_string(p, RSA_DEFAULT_MD))
        return 0;
}
```

## Implementation Details

The fix includes extensive documentation in the source code explaining:
- The OpenSSL v3 mechanism with specific file paths and line numbers
- Why BoringSSL behaves differently
- Why Ed25519/Ed448 keys are handled differently (they don't need a
digest)

## Test Plan
 Added comprehensive regression test in
`test/regression/issue/11029-crypto-verify-null-algorithm.test.ts`
 Tests cover:
  - RSA keys with null/undefined algorithm
  - Ed25519 keys with null algorithm  
  - Cross-verification between null and explicit SHA256
  - `createVerify()` compatibility
 All tests pass and behavior matches Node.js

## Verification
```bash
# Test with Bun
bun test test/regression/issue/11029-crypto-verify-null-algorithm.test.ts

# Compare with Node.js behavior
node -e "const crypto = require('crypto'); 
const {publicKey, privateKey} = crypto.generateKeyPairSync('rsa', {modulusLength: 2048});
const data = Buffer.from('test');
const sig = crypto.sign(null, data, privateKey);
console.log('Node.js verify with null:', crypto.verify(null, data, publicKey, sig));"
```

🤖 Generated with [Claude Code](https://claude.ai/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-09-02 23:30:52 -07:00

136 lines
3.9 KiB
TypeScript

import { expect, test } from "bun:test";
import crypto from "crypto";
// Regression test for issue #11029
// crypto.verify() should support null/undefined algorithm parameter
test("crypto.verify with null algorithm should work for RSA keys", () => {
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
const data = Buffer.from("test data");
// Sign with null algorithm (should use default SHA256 for RSA)
const signature = crypto.sign(null, data, privateKey);
expect(signature).toBeInstanceOf(Buffer);
// Verify with null algorithm should succeed
const isVerified = crypto.verify(null, data, publicKey, signature);
expect(isVerified).toBe(true);
// Verify with wrong data should fail
const wrongData = Buffer.from("wrong data");
const isVerifiedWrong = crypto.verify(null, wrongData, publicKey, signature);
expect(isVerifiedWrong).toBe(false);
});
test("crypto.verify with undefined algorithm should work for RSA keys", () => {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
const data = Buffer.from("test data");
const signature = crypto.sign(undefined, data, privateKey);
// Verify with undefined algorithm
const isVerified = crypto.verify(undefined, data, publicKey, signature);
expect(isVerified).toBe(true);
});
test("crypto.verify with null algorithm should work for Ed25519 keys", () => {
// Generate Ed25519 key pair (one-shot variant that doesn't need digest)
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
const data = Buffer.from("test data");
// Ed25519 should work with null algorithm (no digest needed)
const signature = crypto.sign(null, data, privateKey);
expect(signature).toBeInstanceOf(Buffer);
const isVerified = crypto.verify(null, data, publicKey, signature);
expect(isVerified).toBe(true);
});
test("crypto.verify cross-verification between null and explicit SHA256", () => {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
const data = Buffer.from("test data");
// Sign with SHA256
const signatureSHA256 = crypto.sign("SHA256", data, privateKey);
// Should be able to verify with null (defaults to SHA256 for RSA)
const isVerifiedWithNull = crypto.verify(null, data, publicKey, signatureSHA256);
expect(isVerifiedWithNull).toBe(true);
// Sign with null
const signatureNull = crypto.sign(null, data, privateKey);
// Should be able to verify with explicit SHA256
const isVerifiedWithSHA256 = crypto.verify("SHA256", data, publicKey, signatureNull);
expect(isVerifiedWithSHA256).toBe(true);
});
test("crypto.createVerify should also work with RSA keys", () => {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
const data = Buffer.from("test data");
// Create signature using createSign
const signer = crypto.createSign("SHA256");
signer.update(data);
const signature = signer.sign(privateKey);
// Verify using createVerify
const verifier = crypto.createVerify("SHA256");
verifier.update(data);
const isVerified = verifier.verify(publicKey, signature);
expect(isVerified).toBe(true);
});