Conversation
crypto.getHashes() advertises sha3-224/256/384/512 and createHash()
computes them, but crypto.sign(), crypto.verify(), createSign() and
createVerify() all threw for RSA and EC keys.
Our BoringSSL fork ships EVP_sha3_* but never wired them into the
signing paths:
- kPKCS1SigPrefixes has no SHA-3 DigestInfo entries, so RSA_sign()
fails with RSA_R_UNKNOWN_ALGORITHM_TYPE.
- pkey_ec_ctrl() rejects any digest outside SHA-1/SHA-2 with
EVP_R_INVALID_DIGEST_TYPE, even though pkey_ec_sign() passes the
digest straight to ECDSA_sign() and never reads it.
Add the four id-sha3-* DigestInfo prefixes (NIST CSOR arc
2.16.840.1.101.3.4.2.{7,8,9,10}) and allow the SHA-3 NIDs through the
ECDSA digest check. RSA-PSS already worked.
Separately, Sign.prototype.sign() threw a bare TypeError when the
signing operation failed, so err.code was undefined where crypto.sign()
reports ERR_OSSL_*. Route it through throwCryptoError() like the other
OpenSSL failures in the same function.
|
Warning Review limit reached
Next review available in: 23 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 reviews. How do review 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 refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
|
Updated 5:06 PM PT - Jul 6th, 2026
❌ @robobun, your commit c0aa5f2 has some failures in 🧪 To try this PR locally: bunx bun-pr 33518That installs a local version of the PR into your bun-33518 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
Checked, and I don't think it does — #20592 asks for This PR is the inverse problem: the digests So the two are adjacent but disjoint: #20592 is "the list is too short", this is "the advertised entries don't work for signing". Leaving it open. For what it's worth, the |
There was a problem hiding this comment.
I didn't find any issues — the DigestInfo prefixes and tests look correct — but since this patches BoringSSL's RSA/ECDSA signing tables it's worth a human sanity-check before merging.
Extended reasoning...
Overview
This PR wires SHA-3 digests into crypto.sign()/crypto.verify() by:
- Adding a vendored-source patch (
patches/boringssl/sha3-sign.patch) that (a) appends fourid-sha3-*DigestInfo prefixes tokPKCS1SigPrefixesincrypto/fipsmodule/rsa/rsa.cc.inc, and (b) whitelistsNID_sha3_{224,256,384,512}inpkey_ec_ctrl()'s digest check incrypto/evp/p_ec.cc. - Registering the patch via the existing
patches:field on theboringsslDependencyinscripts/build/deps/boringssl.ts(same mechanism already used by highway, libuv, lshpack, tinycc, etc.). - A one-line change in
src/jsc/bindings/node/crypto/JSSign.cppswappingthrowTypeErrorforthrowCryptoError(..., ERR_get_error(), ...)on thesignInto()failure path, matching the three sibling failure sites in the same function. - ~120 lines of new tests in
test/js/node/crypto/crypto.test.tsthat pin byte-exact Node/OpenSSL-produced RSA PKCS#1 v1.5 signatures, verify PSS/ECDSA signatures from OpenSSL, round-trip ECDSA, and assert tampered/wrong-digest signatures are rejected.
The referenced fixture files (rsa_private_2048.pem, rsa_public_2048.pem, ec_p256_private.pem, ec_p256_public.pem) already exist in test/js/node/crypto/fixtures/.
Security risks
The change is additive on both BoringSSL paths — it does not relax any existing check for SHA-1/SHA-2 signatures. The DigestInfo bytes match RFC 8017 §9.2 / Appendix B.1 for the NIST CSOR OIDs 2.16.840.1.101.3.4.2.{7,8,9,10} with NULL parameters (I spot-checked the DER: 06 09 60 86 48 01 65 03 04 02 08 decodes to id-sha3-256, and the outer/inner SEQUENCE and OCTET STRING lengths line up with each digest size). The ECDSA change only widens a digest whitelist; ECDSA_sign/ECDSA_verify operate on the raw digest bytes and don't care which hash produced them. The negative tests confirm tampered signatures are still rejected.
That said, this is a source-level patch to a crypto library's signature encoding/verification tables. A transcription error here would produce signatures that fail to interoperate with OpenSSL (mitigated by the pinned known-answer tests) or, in the worst case, could weaken verification. This is squarely security-sensitive.
Level of scrutiny
High. Patching crypto/fipsmodule/rsa/rsa.cc.inc and crypto/evp/p_ec.cc in vendored BoringSSL is exactly the category the approval guidelines call out for human review. The PR is well-researched (bytes derived by raw-modexp'ing an OpenSSL signature, wycheproof vectors run, fail-before/pass-after matrix documented) and the diff is small and focused, so review should be quick — but it should be a human.
Other factors
- No CODEOWNERS cover the touched paths.
- The
patches:mechanism is well-established across ~10 other deps; the build-script change is a 3-line addition following that convention. - The
JSSign.cppchange is a strict improvement in error fidelity and mirrors the neighboringthrowCryptoErrorcalls in the same function;ncrypto::ClearErrorOnReturnat the top ofjsSignProtoFuncSignensures the OpenSSL error queue starts clean, soERR_get_error()here reads the actual signing failure. - No prior human reviews or outstanding comments on the timeline; the bug hunter found nothing.
|
Agreed on the framing, and thanks for independently decoding the DER. Since the ask is a human sanity-check of the two BoringSSL tables, here are the two checks that don't require trusting anything in this PR. 1. Re-derive the DigestInfo prefixes from OpenSSL, no transcription involved. RSA PKCS#1 v1.5 is deterministic, so raw-modexp a Node signature with the public key and read the EM block back out. This is how the bytes in the patch were produced, not from a spec table: // node digestinfo.js
const crypto = require("crypto");
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
const jwk = publicKey.export({ format: "jwk" });
const b64u = s => BigInt("0x" + Buffer.from(s, "base64url").toString("hex"));
const n = b64u(jwk.n), e = b64u(jwk.e);
const modpow = (b, x, m) => { let r = 1n; b %= m; while (x > 0n) { if (x & 1n) r = r * b % m; b = b * b % m; x >>= 1n; } return r; };
for (const alg of ["sha3-224", "sha3-256", "sha3-384", "sha3-512"]) {
const sig = crypto.sign(alg, Buffer.from("x"), privateKey);
const em = Buffer.from(modpow(BigInt("0x" + sig.toString("hex")), e, n).toString(16).padStart(512, "0"), "hex");
let i = 2; // EM = 0x00 || 0x01 || 0xFF* || 0x00 || DigestInfo
while (em[i] === 0xff) i++;
const di = em.subarray(i + 1);
const digest = crypto.createHash(alg).update("x").digest();
console.log(alg, [...di.subarray(0, di.length - digest.length)].map(b => "0x" + b.toString(16).padStart(2, "0")).join(", "));
}Output is byte-for-byte the four 2. Re-run the known-answer vectors. BoringSSL already vendors them, so no new fixtures are needed: Feeding every On Node reports |
There was a problem hiding this comment.
I didn't find any issues — the DigestInfo bytes and test coverage check out — but patching BoringSSL's RSA/ECDSA signing tables is the kind of change I'd rather a human sign off on.
Extended reasoning...
Overview
This PR wires SHA-3 digests into BoringSSL's signing paths via a source patch (patches/boringssl/sha3-sign.patch) applied through the existing patches mechanism in scripts/build/deps/boringssl.ts. The patch adds four DigestInfo prefix entries to kPKCS1SigPrefixes in rsa.cc.inc and widens the ECDSA digest whitelist in p_ec.cc. A one-line change in JSSign.cpp swaps a bare throwTypeError for throwCryptoError(..., ERR_get_error(), ...) to match the sibling failure sites in the same function. Tests pin Node/OpenSSL-produced signatures (deterministic PKCS#1 v1.5) plus verify-only PSS/ECDSA vectors and negative cases.
Security risks
The change is purely additive — new table rows and a widened whitelist — and doesn't touch any existing SHA-1/SHA-2 entries or the padding/verification logic itself. I independently checked the DER: the OID arc 2.16.840.1.101.3.4.2.{7,8,9,10} encodes to 60 86 48 01 65 03 04 02 {07,08,09,0a}, the outer/inner SEQUENCE lengths (0x2d/0x31/0x41/0x51 and 0x0d) and OCTET STRING lengths (0x1c/0x20/0x30/0x40) all agree with RFC 8017 §9.2 note 1, and the digest-length constants match. The author's wycheproof run (2334 vectors including negative cases) and byte-identity check against Node are the right validations. That said, the kPKCS1SigPrefixes table drives both RSA_sign and RSA_verify's constant-time comparison, so a transcription error here would be a real interop/security concern — hand-entered crypto constants are exactly where a second pair of human eyes is warranted.
Level of scrutiny
High. This is a source patch to the vendored crypto library's core signing/verification tables. The JSSign.cpp and build-script changes are trivial and I'd approve them on their own; the BoringSSL patch is small and looks correct, but "patches vendored BoringSSL" is a category I'm not comfortable auto-approving regardless of how good the verification story is.
Other factors
The patches field on Dependency is an established mechanism (scripts/build/source.ts:426) already used by several other deps, so the delivery vehicle is conventional. Test fixtures referenced by the new tests exist on disk. No CODEOWNERS entries cover these paths. No bugs were found by the bug-hunting pass. The PR thread already lays out reproducible self-checks (modexp derivation, wycheproof), which should make human review quick.
CI status: green on the diff, red only on macOS artifact-download infraThe diff is passing everywhere a binary actually ran. The only red is the macOS artifact-download step, not a test failure, and it is not caused by this change. Two builds, same story:
The failure is an artifact download timing out, with zero tests run: The I've used my one retrigger. Rather than spam more empty commits, flagging for a maintainer: the change is ready and the remaining red is a macOS S3/agent hiccup unrelated to the diff. A re-run of that one lane should clear it. Summary of what's in the PR, unchanged:
|
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-06, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Repro
crypto.getHashes()advertisessha3-256andcreateHash()/createHmac()compute it correctly, but every signing entry point throws, for both RSA and EC keys.Node signs and verifies
sha3-*for RSA and EC, so code that feature-detects a digest viagetHashes()before signing (the jose/jsonwebtoken pattern) is correct on Node and fails at runtime on Bun.Cause
Our BoringSSL fork ships
EVP_sha3_*(added in #29323) but never wired them into the signing paths:kPKCS1SigPrefixesincrypto/fipsmodule/rsa/rsa.cc.inchas no SHA-3DigestInfoentries, soRSA_sign()bails withRSA_R_UNKNOWN_ALGORITHM_TYPE.pkey_ec_ctrl()incrypto/evp/p_ec.ccrejects any digest outside SHA-1/SHA-2 withEVP_R_INVALID_DIGEST_TYPE— even thoughpkey_ec_sign()/pkey_ec_verify()hand the digest straight toECDSA_sign()/ECDSA_verify()and never readdctx->md.RSA-PSS was already fine: it routes through
RSA_sign_pss_mgf1(), which takes theEVP_MDdirectly and needs noDigestInfo.Separately,
Sign.prototype.sign()threw a bareTypeErrorwhen the signing operation itself failed, soerr.codewasundefinedwhere the one-shotcrypto.sign()reportsERR_OSSL_*. The three sibling failure sites in the same function already usethrowCryptoError().Fix
patches/boringssl/sha3-sign.patch(the build system's mechanism for source patches to agithub-archivedep):id-sha3-*DigestInfoprefixes. The OIDs come from the NIST CSOR arc2.16.840.1.101.3.4.2.{7,8,9,10}, with the NULL parameters RFC 8017 specifies for PKCS#1 v1.5. The exact bytes were read back out of OpenSSL's own output rather than transcribed: sign with Node, raw-modexp the signature with the public key, and read the EM block.NID_sha3_{224,256,384,512}through the ECDSA digest check.JSSign.cpp: route thesignInto()failure throughthrowCryptoError(..., ERR_get_error(), ...), matching the one-shot path andCryptoErrorList::capture().createCryptoError()falls back to a plainErrorwith the original message when OpenSSL queued nothing, so this is never worse than theTypeErrorit replaces.Verification
Signatures are interoperable with OpenSSL, not merely self-consistent:
rsa_signature_*_sha3_*andecdsa_*_sha3_*cases pass, matching Node 1:1 — including every negative vector (wrong OID, BER-encoded padding, tampered signatures), so malformed signatures are still rejected.The regression test pins the Node-produced signatures rather than round-tripping, so a wrong
DigestInfoencoding would fail it.Fail-before / pass-after, per half
JSSign.cppfix keptsrc/reverted, BoringSSL patch kept905 pass, 0 failacrosstest/js/node/crypto/Note the patch lives in
patches/+scripts/build/, so asrc/-only revert does not exercise it; the middle row is the fail-before for that half.Unchanged:
test/js/web/crypto/(40 pass). The threetest/js/node/tls/failures on this branch reproduce on released Bun or are a 5s default-timeout on a debug+ASAN test that spawns three TLS children (passes at 30s).Scope:
shake128/shake256and BLAKE2 still throw for signing, as they do on Node for RSA. TheRSA-SHA3-*signature-algorithm aliases are still missing, butgetHashes()does not advertise them either, so there is no contradiction there.The patch can be dropped once the fork carries the change and
BORINGSSL_COMMITmoves.