Skip to content

Instantly share code, notes, and snippets.

@Kr0emer
Last active February 16, 2026 03:50
Show Gist options
  • Select an option

  • Save Kr0emer/93789fe6efe5519db9692d4ad1dad586 to your computer and use it in GitHub Desktop.

Select an option

Save Kr0emer/93789fe6efe5519db9692d4ad1dad586 to your computer and use it in GitHub Desktop.

jsrsasign: DSA signWithMessageHash emits invalid s=0 signature (FIPS 186-4 violation)

Summary

The DSA signing implementation in jsrsasign (all versions through 11.1.0) does not check whether the computed signature components r or s are zero. FIPS 186-4 §4.6 requires that if r = 0 or s = 0, the signer must re-select the ephemeral key k and retry. jsrsasign skips this check and outputs the invalid signature directly.

Vulnerability Type

CWE-325: Missing Required Cryptographic Step

Severity

Low

Affected Versions

All versions through 11.1.0 (latest).

Root Cause

In KJUR.crypto.DSA.signWithMessageHash, after computing:

  • r = (g^k mod p) mod q
  • s = k^-1 (H(m) + x·r) mod q

the function returns the signature without checking whether r = 0 or s = 0. FIPS 186-4 §4.6 step 5 and step 7 explicitly require this check, with a retry loop if either value is zero.

Impact

If s = 0, the signature equation s = k⁻¹(H(m) + x·r) mod q = 0 implies H(m) + x·r ≡ 0 (mod q). Anyone observing this signature and the corresponding message hash can recover the private key:

x = -H(m) · r⁻¹ mod q

Under standard DSA parameters (q ≥ 160 bits), the probability of s = 0 for a random message is approximately 1/q ≈ 2⁻¹⁶⁰, making natural occurrence extremely unlikely. However, this is a clear specification violation, and the check costs essentially nothing to implement.

PoC

The following script forces a specific ephemeral k value and chooses a message hash such that s = 0, demonstrating that jsrsasign outputs the invalid signature and that the private key can be recovered.

'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
global.navigator = { appName: 'Netscape' };
global.window = global;
const jsrsasignPath = path.resolve(__dirname, '../../jsrsasign/jsrsasign-all-min.js');
const jsrsasignCode = fs.readFileSync(jsrsasignPath, 'utf8');
vm.runInThisContext(jsrsasignCode, { filename: 'jsrsasign-all-min.js' });

const biHex = (hex) => new BigInteger(hex, 16);
// Tiny DSA domain for deterministic reproduction
const pHex = '17'; // p = 23
const qHex = '0b'; // q = 11
const gHex = '04'; // g = 4
const xHex = '03'; // private key x = 3
const p = biHex(pHex); const q = biHex(qHex);
const g = biHex(gHex); const x = biHex(xHex);

// Force ephemeral k = 2
const forcedK = new BigInteger('2', 10);
KJUR.crypto.Util.getRandomBigIntegerMinToMax = function () { return forcedK; };

// Choose hash so that s = k^-1 * (H(m) + x*r) mod q == 0
const rExpected = g.modPow(forcedK, p).mod(q);
const n = q.subtract(x.multiply(rExpected).mod(q)).mod(q);
const hashHex = n.toString(16);

const dsa = new KJUR.crypto.DSA();
dsa.setPrivateHex(pHex, qHex, gHex, null, xHex);
const sigHex = dsa.signWithMessageHash(hashHex);
const rs = dsa.parseASN1Signature(sigHex);
const r = rs[0];
const s = rs[1];

console.log('r =', r.toString(10));
console.log('s =', s.toString(10));
console.log('s === 0:', s.compareTo(BigInteger.ZERO) === 0);

// Private key recovery from s=0 signature
const xRecovered = n.negate().multiply(r.modInverse(q)).mod(q);
const xReal = x.mod(q);
console.log('x (real)      =', xReal.toString(10));
console.log('x (recovered) =', xRecovered.toString(10));
console.log('Key recovered:', xRecovered.compareTo(xReal) === 0);

Output

r = 5
s = 0
s === 0: true
x (real)      = 3
x (recovered) = 3
Key recovered: true

Suggested Fix

In signWithMessageHash, after computing r and s, add a check:

// FIPS 186-4 §4.6: if r == 0 or s == 0, select new k and retry
while (r.compareTo(BigInteger.ZERO) === 0 || s.compareTo(BigInteger.ZERO) === 0) {
  k = getRandomK();
  r = g.modPow(k, p).mod(q);
  s = k.modInverse(q).multiply(hash.add(x.multiply(r))).mod(q);
}

References

  • FIPS 186-4 §4.6 (DSA Signature Generation), steps 5 and 7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment