Skip to content

Verify dsa-sha1 signatures #655

Open

Description

@cjbarth

Problem

XML Signature 1.1 §6.1 requires three signature algorithms: RSAwithSHA256, ECDSAwithSHA256, and "DSAwithSHA1 (signature verification)". xml-crypto has the first and #530 tracks the second. Nothing tracks the third: a SignatureMethod of http://www.w3.org/2000/09/xmldsig#dsa-sha1 makes checkSignature() throw signature algorithm '…#dsa-sha1' is not supported.

Only verification is required. The same section lists "DSAwithSHA1 (signature generation)" as optional, and FIPS 186-5, Appendix E, says "DSA is no longer approved for digital signature generation. DSA may be used to verify signatures generated prior to the implementation date of this standard."

This came from node-saml, not from a user: no issue here, in node-saml or in passport-saml mentions DSA. SAML bindings §3.4.4.1 says DSAwithSHA1 and RSAwithSHA1 "MUST be supported" for the signature on the Redirect binding, and since node-saml/node-saml#479 node-saml takes those algorithms from new SignedXml().SignatureAlgorithms. It can verify dsa-sha1 only once xml-crypto does. node-saml's half is node-saml/node-saml#491.

What other implementations do

Measured with one document, signed by JDK 21 with a 1024-bit DSA key. Its SignatureValue is 40 octets.

Implementation Verifies it by default Notes
JDK 21, javax.xml.crypto.dsig No "It is forbidden to use algorithm …#dsa-sha1 when secure validation is enabled". jdk.xml.dsig.secureValidationPolicy disallows dsa-sha1, rsa-sha1, ecdsa-sha1 and the sha1 digest. It verifies with secure validation off.
.NET, System.Security.Cryptography.Xml 10.0.12 Yes SignedXml.CheckSignature(DSA) returns True.
xml-crypto a311adc No Throws … is not supported.

Reproduction

Save as repro.js in the repo root and run npm run build && node repro.js. Tested at a311adc on Node 26.9.0.

repro.js
const crypto = require("crypto");
const { SignedXml } = require("./lib");
const { RsaSha1 } = require("./lib/signature-algorithms");

const DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1";

// Signed by JDK 21 (javax.xml.crypto.dsig) with a 1024-bit DSA key.
const xml =
  '<root><child>data</child><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>o6nDTRHHAOWLcD3x7BcGQ1yqK8ka4y9JGBhR/zgNv0k=</DigestValue></Reference></SignedInfo><SignatureValue>k+mCmbaRlgle444fEnu+Lzah0aVZ8gCFqYfgqs5pPxkK5BZZhfpwYg==</SignatureValue></Signature></root>';
const dsaPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBtzCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9E
AMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f
6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv
8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7LvKtc
NrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwky
jMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/h
WuWfBpKLZl6Ae1UlZAFMO/7PSSoDgYQAAoGARFpJZtOk/WWFi/aPGG3J/Cyi4/dw
Pwcc2aH/lAWBJDuvZ3w/4KjSKjsKB+MgzPwvG/PSOWkRKihJLOK8kqU02xvJN4gy
KxJdOQt/9MQA4B2DDM/uF5pM8laxsJ8UBgZipLu643yHcSVh8SuwQvhbTAzSY3mg
WdNYDbSB234lZ/w=
-----END PUBLIC KEY-----
`;

class DsaSha1 {
  getSignature() {
    throw new Error("dsa-sha1 is for verification only");
  }
  verifySignature(material, key, signatureValue) {
    // createPublicKey() refuses a KeyObject that is already public.
    const publicKey =
      key instanceof crypto.KeyObject && key.type === "public" ? key : crypto.createPublicKey(key);
    if (publicKey.asymmetricKeyType !== "dsa") {
      return false;
    }
    return crypto.verify(
      "sha1",
      Buffer.from(material),
      { key: publicKey, dsaEncoding: "ieee-p1363" },
      Buffer.from(signatureValue, "base64"),
    );
  }
  getAlgorithmName() {
    return DSA_SHA1;
  }
}

function check(label, document, register) {
  const sig = new SignedXml({ publicCert: dsaPublicKey });
  if (register) {
    sig.SignatureAlgorithms[DSA_SHA1] = DsaSha1;
  }
  sig.loadSignature(/<Signature[\s\S]*<\/Signature>/.exec(document)[0]);
  let result;
  try {
    result = sig.checkSignature(document);
  } catch (error) {
    result = `throws: ${error.message}`;
  }
  console.log(`${label.padEnd(52)} ${result}`);
}

check("JDK document, default algorithms", xml, false);
check("JDK document, DsaSha1 registered", xml, true);
check("JDK document edited, DsaSha1 registered", xml.replace("data", "date"), true);

// The digest name does not bind the key type: Node takes the algorithm from the key.
const material = Buffer.from("<SignedInfo/>");
const dsa = crypto.generateKeyPairSync("dsa", { modulusLength: 1024, divisorLength: 160 });
const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
const dsaDer = crypto.sign("sha1", material, dsa.privateKey).toString("base64");
const rsaSig = crypto.sign("sha1", material, rsa.privateKey);
const row = (label, result) => console.log(`${label.padEnd(52)} ${result}`);
row(
  "RsaSha1 given a DSA key and a DER DSA signature",
  new RsaSha1().verifySignature(material.toString(), dsa.publicKey, dsaDer),
);
row(
  "DsaSha1 given an RSA key and an RSA-SHA1 signature",
  new DsaSha1().verifySignature(material.toString(), rsa.publicKey, rsaSig.toString("base64")),
);
row(
  "the same without the key-type check",
  crypto.verify("sha1", material, { key: rsa.publicKey, dsaEncoding: "ieee-p1363" }, rsaSig),
);
JDK document, default algorithms                     throws: signature algorithm 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' is not supported
JDK document, DsaSha1 registered                     true
JDK document edited, DsaSha1 registered              false
RsaSha1 given a DSA key and a DER DSA signature      true
DsaSha1 given an RSA key and an RSA-SHA1 signature   false
the same without the key-type check                  true

The first three rows are the gap, and a sketch that closes it. The last three are why the sketch checks the key type: Node takes the algorithm from the key, not from the digest name, so without the check an RSA-SHA1 signature verifies under the dsa-sha1 identifier. The classes here have the same looseness the other way round: RsaSha1 verifies a DER-encoded DSA signature when the key it is given is a DSA key. Neither lets anyone forge a signature, because the key is still the one the caller supplied. The identifier just does not constrain the key.

Proposal

In 6.4, add a DsaSha1 signature algorithm that:

  • Verifies only. getSignature() throws.
  • Reads SignatureValue as r ‖ s, 20 octets each, as §6.4.1 defines it. In Node that is dsaEncoding: "ieee-p1363", which Add ECDSA Support (#530) #531 uses for ECDSA. A DER-encoded signature does not verify.
  • Refuses a key that is not a DSA key, for the reason above.

Open questions

  • Default set, or opt-in? In the default SignatureAlgorithms, every caller gets it, node-saml included, and with the key check nothing changes for a caller whose key is not DSA. rsa-sha1 is already there, so it would not be the first SHA-1 algorithm on by default. Opt-in is what the JDK does, and enableHMAC() is the precedent here, though HMAC is off for a different reason (key confusion).
  • Key size. §6.4.1 ties dsa-sha1 to the (1024, 160) parameters. The sketch also verifies a 64-octet signature made with a (2048, 256) key. Should a key whose q is not 160 bits be refused?
  • dsa-sha256 (http://www.w3.org/2009/xmldsig11#dsa-sha256) is optional in §6.1 and is not part of this.

Tests

  • The JDK-signed document above as a fixture, recorded in test/README.md.
  • Rejections: the document edited, an RSA key with an RSA-SHA1 signature under dsa-sha1, a DER-encoded DSA signature, and getSignature() throwing.

Activity

  1. added this to the v6.4 milestone on Oct 8, 2026
  2. added
    enhancementAdds a feature, option or export without breaking existing callers
    on Oct 8, 2026
  3. modified the milestones: v6.4, v7.0 on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAdds a feature, option or export without breaking existing callers

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions