Repository navigation
fix(rsa): reject garbage in nested DigestAlgorithm SEQUENCE - #1160
shaswata56 wants to merge 3 commits into
Conversation
The fix for CVE-2026-33894 only checked the element count of the outer DigestInfo SEQUENCE. asn1.validate() ignores extra children, so extra unconsumed elements inside the nested DigestAlgorithm SEQUENCE were not rejected, leaving enough slack for the same class of low public exponent (e=3) PKCS#1 v1.5 signature forgery (Bleichenbacher-style) via garbage bytes placed inside the nested structure instead of the outer one. Require the nested DigestAlgorithm SEQUENCE to contain exactly the algorithm OID plus an optional NULL parameters element, rejecting anything else. Regression test vectors generated with tests/pocs/cve-2026-85393.js model the forgery from issue digitalbazaar#1149. Fixes CVE-2026-85393 (GHSA-86w9-cpqp-85rv). Signed-off-by: Shaswata Das <shaswata56@gmail.com>
|
Thanks for adding the PoC script and the positive DigestInfo tests. Having those in the repo is useful. A couple of things that should help whoever reviews this: Overlap with #1152. The lib/rsa.js change here is the same as the one in #1152 (Krysthyan, opened 2026-09-09), comment text included. Both diffs produce the same file (a4264822b..430905451). If this builds on #1152, it would help to say so in the description. It might also be simpler to offer the PoC and the new tests to #1152, so maintainers have one fewer duplicate PR to sort through. The count-only check leaves some cases open. Requiring the inner DigestAlgorithm SEQUENCE to have 2 elements (or 1) doesn't cover two variants, because both keep the element count valid: NULL with content: forge's parser doesn't require a NULL to be empty, so the parameters field can carry attacker-chosen bytes (100 bytes is tested) while the inner SEQUENCE stays at exactly 2 elements. For transparency: I'm the author of #1159. It takes a different approach: it re-encodes the expected DigestInfo canonically from the parsed OID and digest, then requires a byte-for-byte match. That rejects all of these variants with one comparison. I'm not attached to which PR lands. Whichever one does, it would be good to include regression tests for the NULL-with-content and long-form-length cases. |
Element-count checks alone do not close CVE-2026-85393: forge's parser accepts a NULL parameters field that carries content bytes and non-minimal long-form length octets, so a count-valid DigestInfo can still hold attacker-chosen slack bytes usable for the same low-exponent (e=3) forgery. Identified by Matthew Conroy in review of this PR and confirmed independently by subvincdebian in digitalbazaar#1159. Replace the element-count checks with a single canonical-encoding comparison: re-encode the expected DigestInfo from the parsed OID and digest (_encodeDigestInfo) and require the decoded bytes to match. Any encoding slack -- extra elements in either SEQUENCE, NULL parameters with content, non-minimal length octets -- changes the re-encoded bytes, so one comparison rejects all variants. Both DigestAlgorithm forms that occur in practice (with NULL parameters, and without them) remain accepted. Add regression tests for the count-preserving variants: NULL parameters with content bytes, non-minimal long-form length octets, and a garbage element in the parameters slot. Fixes CVE-2026-85393 (GHSA-86w9-cpqp-85rv). Signed-off-by: Shaswata Das <shaswata56@gmail.com>
Thanks @matthewscottconroy — you're right on both counts, and the count-only check is now gone. I reproduced both count-preserving bypasses locally against the previous patch using the 889cee0 replaces the count checks with the canonical re-encoding comparison from your review Regression tests added for all three count-preserving variants (NULL with content, long-form On #1152: the description has credited it since the PR was opened ("Supersedes #1152 — fix And agreed on consolidation: if maintainers would rather land #1159, no objection from me. |
|
Some ecosystem context that may help review, since I checked whether this lenience exists elsewhere: forged the same malformed-DigestInfo vectors (e=3, garbage element in the inner DigestAlgorithm SEQUENCE; NULL parameters carrying content bytes; garbage element in the outer SEQUENCE; non-minimal long-form outer length) against the other mainstream JS RSA verifiers —
So forge appears to be the outlier on DigestInfo lenience among JS verifiers, which suggests the canonical-compare direction here matches where the rest of the ecosystem already is. Happy to share the vector-generation harness if useful. |
…E-2026-85393) Backport of the unmerged upstream fix digitalbazaar#1160 for GHSA-86w9-cpqp-85rv. node-forge <= 1.4.0 only checks the element count of the outer DigestInfo SEQUENCE, so garbage inside the nested DigestAlgorithm SEQUENCE, a NULL with content bytes, or non-minimal length octets all pass validation and allow Bleichenbacher-style signature forgery against low-exponent keys. Verification now re-encodes the expected DigestInfo from the parsed OID and digest and requires a byte match. Keyboard carries this fork only until upstream publishes a release that includes the fix; drop the override in remote-oauth-server once it does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1160 +/- ##
==========================================
+ Coverage 80.25% 80.29% +0.03%
==========================================
Files 42 42
Lines 9597 9605 +8
==========================================
+ Hits 7702 7712 +10
+ Misses 1895 1893 -2
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Generating three 2048-bit RSA key pairs at runtime exceeds the 30s mocha timeout on CI legs without native crypto (Node 6.x/8.x), causing flaky timeouts. Use the existing PEM fixtures instead; the test now runs in milliseconds.
|
Hi, any chance this can be merged soon? |
|
The advisory for CVE-2026-85393 (GHSA-86w9-cpqp-85rv) has been public since Sep 3 with Converged design — canonical re-encode + compare (build the expected DigestInfo with the library's own DER encoder, require the decrypted bytes to match):
Targeted element checks — #1157, #1158, #1161: subsets of canonical compare; #1159's vectors demonstrate bypasses of shape/count-only checks (non-empty NULL parameters, non-minimal lengths), which canonical compare rejects by construction. Proposal: ship one of the canonical-compare PRs as 1.4.1. I'm happy to do the legwork either way — fold #1162's shared-encoder refactor into this branch (or rebase onto whichever PR is preferred) and assemble a combined regression suite from all six. @matthewscottconroy — you know these vectors best, would value your call on which candidate should land. @davidlehn — anything I can take off your plate to get a 1.4.1 out the door? |
- [rsa] Require a canonical DER DigestInfo in RSASSA PKCS#1 v1.5 signature verification by re-encoding the expected DigestInfo and comparing bytes, as in RFC 8017 8.2.2. Rejects extra DigestAlgorithm elements, NULL parameters with content, non-minimal lengths, and non-canonical OIDs. - Address GHSA-2g46-22mc-6gwp (CVE-2026-85393): - GHSA-2g46-22mc-6gwp - Address GHSA-6qrw-m8cv-hcxf: - GHSA-6qrw-m8cv-hcxf - Based on work in PRs #1159 (@matthewscottconroy), #1160 (@shaswata56), #1161 (@nitrog7), and #1162 (@RoastFried-RF). - Add PoC from PR #1160. - Add tests for canonical and non-canonical DigestInfo encodings, and for every supported digest algorithm with and without NULL parameters. Co-authored-by: Matthew Conroy <matthew.conroy@ibm.com> Co-authored-by: Shaswata Das <shaswata56@gmail.com>
|
Thank you. The keyless e=3 forgery vector, the e=1 test approach and the PoC (now |
Fix RSA PKCS#1 v1.5 forgery via nested DigestAlgorithm garbage (CVE-2026-85393)
Summary
RSASSA-PKCS1-v1_5 verification accepted signatures whose decrypted block contains a structurally "valid" but non-canonical
DigestInfo. forge's BER parser andasn1.validate()intentionally tolerate malformed encodings, which leaves attacker-controlled slack bytes inside the block — aNULLparameters field carrying arbitrary content, non-minimal long-form length octets, or extra/garbage elements in the nested SEQUENCEs. That slack is exactly what a low-exponent (e.g.e = 3) PKCS#1 v1.5 forgery needs to control the digest bytes while keeping validation happy.Approach
After
asn1.validate()confirms the DigestInfo structure, the expectedDigestInfois re-encoded canonically from the parsed OID and digest (_encodeDigestInfo) and compared byte-for-byte against the decoded block. One comparison rejects every lenient-encoding variant at once — there is no enumeration of malformed shapes to get wrong.Details:
DigestAlgorithmforms that occur in practice are accepted: with the emptyNULLparameters field (as encoded by forge and OpenSSL) and, for the algorithms that permit it, without it — selected by whether the validator captured aparameterselement._parseAllDigestBytestesting flag keeps tolerating trailing bytes; in the default modeasn1.fromDer()has already rejected them, making this an exact match.Tests
tests/unit/rsa.js: regression vectors for the count-preserving variants —NULLparameters with content bytes (100-byte payload), non-minimal long-form length octets on the inner/outer SEQUENCE, garbage element in the parameters slot — plus the original forged low-exponent vectors and positive controls (forge- and OpenSSL-encoded valid signatures).tests/pocs/cve-2026-85393.js: self-contained PoC demonstrating the forgery against the previous behavior.Credits & relationship to other PRs
lib/rsa.jshardening there is reused with credit. This PR was opened fresh because Fix RSA PKCS#1 v1.5 forgery via nested DigestAlgorithm garbage #1152's branch has a stale base, adescribe.onlythat masked the rest of the RSA suite, and a regression vector that errors at the wrong check when actually executed.e = 3ande = 65537.