ANT-2026-3ZHWVDTX · kjur/jsrsasign
other high
Severity Claude high · Security research firm - · Maintainer -
Anthropic's analysis of this finding, sealed at approval.
ANT-2026-3ZHWVDTX: [security] off-by-one DSA signature validation error; different than CVE-2026-45{99,01,02}
In jsrsasign's DSA verification (src/dsa-2.0.js:238-248), the FIPS 186-4 requirement 0 < s < q is implemented with a non-strict upper bound, so s = q is accepted. jsbn's modInverse returns 0 rather than throwing when the input is non-invertible, so w = s^-1 mod q becomes 0, forcing u1 = u2 = 0 and v = (g^0 * y^0) mod p mod q = 1. The attacker-chosen constant signature (r=1, s=q) therefore satisfies v == r for every message hash and every public key. This is a universal forgery: any DSA-signed data verified with jsrsasign can be spoofed without the private key. A PoC confirms the forged signature verifies on arbitrary messages under jsrsasign 11.1.1.
Target
Project: kjur/jsrsasign
Version: 11.1.1
Location: src/dsa-2.0.js:244 (also line 240)
Technical Details
Line 244 tests s.compareTo(q) > 0, rejecting only s > q and allowing s = q, contrary to FIPS 186-4 §4.7 (and the code's own comment). Because jsbn's BigInteger.modInverse returns 0 for non-invertible inputs (ext/jsbn2.js:517) instead of raising, and the verify path never checks w != 0, the degenerate w=0 silently propagates to produce v=1, which matches a forged r=1. Line 240 has the same off-by-one shape for r (allowing r=0 and r=q) but those values are not exploitable.
Reproduction
- Obtain the target's public DSA parameter q (part of the public key / domain params).
- Construct the DER-encoded signature with r = 1 and s = q.
- Submit this signature alongside any message/payload of the attacker's choosing.
- jsrsasign computes w = q^-1 mod q = 0 (jsbn returns 0), so u1 = u2 = 0, v = 1 = r, and verification succeeds.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Change the upper-bound comparisons on lines 240 and 244 from > 0 to >= 0, i.e. if (BigInteger.ZERO.compareTo(s) >= 0 || s.compareTo(q) >= 0) throw ..., enforcing strict 0 < r < q and 0 < s < q per FIPS 186-4. Patch attached by reporter.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-3ZHWVDTX.
Reference: ANT-2026-3ZHWVDTX
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
https://github.com/kjur/jsrsasign/releases/tag/11.1.2
Dates from discovery through public reveal.
- 2026-03-27 Sent to maintainer
- 2026-03-27 Maintainer acknowledged
- 2026-04-12 Patch released
- 2026-05-14 Reported to tracker
- 2026-08-17 Publicly revealed
SHA-3-512 hash:
23d31eff537f9f05e4c6dbe25239e82d3c709fe63e91cb3308694e68da47051e7e5d85c5bbf686fcca3736e6e129a1826c80579dd13948428e66aab8abf234f6
Committed 2026-03-28 21:27 PT
Revealed 2026-08-17 17:36 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-3ZHWVDTX",
"bug_class": "Improper Verification of Cryptographic Signature",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-05-14T22:41:47+00:00",
"description": "In jsrsasign's DSA verification (src/dsa-2.0.js:238-248), the FIPS 186-4 requirement 0 < s < q is implemented with a non-strict upper bound, so s = q is accepted. jsbn's `modInverse` returns 0 rather than throwing when the input is non-invertible, so w = s^-1 mod q becomes 0, forcing u1 = u2 = 0 and v = (g^0 * y^0) mod p mod q = 1. The attacker-chosen constant signature (r=1, s=q) therefore satisfies v == r for every message hash and every public key. This is a universal forgery: any DSA-signed data verified with jsrsasign can be spoofed without the private key. A PoC confirms the forged signature verifies on arbitrary messages under jsrsasign 11.1.1.",
"discovered_at": null,
"location": "src/dsa-2.0.js:244 (also line 240)",
"poc_sha256": null,
"preimage_version": 1,
"project": "kjur/jsrsasign",
"reproduction": [
"1. Obtain the target's public DSA parameter q (part of the public key / domain params).",
"2. Construct the DER-encoded signature with r = 1 and s = q.",
"3. Submit this signature alongside any message/payload of the attacker's choosing.",
"4. jsrsasign computes w = q^-1 mod q = 0 (jsbn returns 0), so u1 = u2 = 0, v = 1 = r, and verification succeeds."
],
"technical_details": "Hi Kenji --\n\nI've found a vulnerability that's looks quite similar to the recent\n\nCVE-2026-4599/4600/4601 fixes in 11.1.1---the same file\n\n(unfortunately) has one more. The verify-side range check at\n\nsrc/dsa-2.0.js line 244 allows s=q, and jsbn's modInverse returns 0\n\nfor non-invertible inputs rather than throwing, so (r=1, s=q) is a\n\nuniversal forgery: it verifies against any message under any DSA\n\npublic key.\n\n(Up-front disclosure: an LLM found this bug. I validated it myself\n\nagainst jsrsasign 11.1.1 from npm and confirmed the forged signature\n\nverifies on arbitrary messages. I stand by this report personally.)\n\nThe bug is the same off-by-one shape as CVE-2026-4601, which fixed the\n\nr=0/s=0 case on the *sign* side. That is, the verify side has:\n\n // src/dsa-2.0.js:238-248\n\n // 3.1. 0 < r < q\n\n if (BigInteger.ZERO.compareTo(r) > 0 || r.compareTo(q) > 0) //\n\nallows r=0, r=q\n\n throw \"invalid DSA signature\";\n\n // 3.2. 0 < s < q\n\n if (BigInteger.ZERO.compareTo(s) >= 0 || s.compareTo(q) > 0) //\n\nallows s=q <-- BUG\n\n throw \"invalid DSA signature\";\n\n // 4. get w where w = s^-1 mod q\n\n var w = s.modInverse(q); // jsbn returns 0 when s == 0 (mod q)\n\nBut FIPS 186-4 section 4.7 requires strict inequalities---the comment above\n\nexactly says this!---but s.compareTo(q) > 0 rejects only s > q, not s = q.\n\nWhen s = q, modInverse is being asked for the inverse of q mod q, which\n\ndoesn't exist. The jsbn library handles this by returning 0\n\n(ext/jsbn2.js:517, the x.signum() == 0 branch after x = this.mod(m)).\n\nThe verify code never checks w != 0. Therefore we can calculate that:\n\n w = 0\n\n u1 = z*0 mod q = 0\n\n u2 = r*0 mod q = 0\n\n v = (g^0 * y^0) mod p mod q = 1\n\nSetting r = 1 makes v == r hold for every message hash z and every\n\npublic key y. The forged signature is the constant DER sequence\n\nSEQUENCE { INTEGER 1, INTEGER q }.\n\nRunning the attached poc.js (sorry---base64 encoded to evade gmails\n\nmalware filter) against jsrsasign 11.1.1 should give hte following:\n\n [*] Testing jsrsasign 11.1.1\n\n [*] Forged signature (r=1, s=q): 301a020101021500996f967f6c8e388d...\n\n verify(hash=0000000000000000...) = true\n\n verify(hash=deadbeefdeadbeef...) = true\n\n verify(hash=aaf4c61ddcc5e8a2...) = true\n\n verify(hash=3c52e0e5c87f4b6a...) = true\n\n >>> VULNERABLE: (r=1, s=q) verifies on all messages <<<\n\nThe r check at line 240 has the same two off-by-ones (it allows r=0\n\nand r=q), but neither of these leads to a bug. I've included both in\n\nthe patch for completeness since they're the same FIPS 186-4\n\nrequirement.\n\nThe fix is a short change on line 244 (and the same on 240):\n\n - if (BigInteger.ZERO.compareTo(s) >= 0 || s.compareTo(q) > 0)\n\n + if (BigInteger.ZERO.compareTo(s) >= 0 || s.compareTo(q) >= 0)\n\nA patch is attached.\n\nPlease let me know if you have any questions.\n\nNicholas",
"title": "[security] off-by-one DSA signature validation error; different than CVE-2026-45{99,01,02}",
"vendor_severity": null
}