Repository navigation
Description
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
SignatureValueasr ‖ s, 20 octets each, as §6.4.1 defines it. In Node that isdsaEncoding: "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-sha1is already there, so it would not be the first SHA-1 algorithm on by default. Opt-in is what the JDK does, andenableHMAC()is the precedent here, though HMAC is off for a different reason (key confusion). - Key size. §6.4.1 ties
dsa-sha1to the (1024, 160) parameters. The sketch also verifies a 64-octet signature made with a (2048, 256) key. Should a key whoseqis 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, andgetSignature()throwing.
Activity