From 52a0019bc31c3a70baa0a05acb5fe41a972c8d01 Mon Sep 17 00:00:00 2001 From: adam strickland Date: Wed, 28 Jan 2026 12:18:56 +0000 Subject: [PATCH 1/5] ECDSA demo --- src/signature-algorithms.ts | 26 ++++++++++++++++++++++++++ src/signed-xml.ts | 1 + 2 files changed, 27 insertions(+) diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index 52e09280..cd4872a1 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -53,6 +53,32 @@ export class RsaSha256 implements SignatureAlgorithm { }; } +export class EcdsaSha256 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { + const signer = crypto.createSign("SHA256"); + signer.update(signedInfo); + const res = signer.sign(privateKey, "base64"); + + return res; + }, + ); + + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { + const verifier = crypto.createVerify("SHA256"); + verifier.update(material); + const res = verifier.verify(key, signatureValue, "base64"); + + return res; + }, + ); + + getAlgorithmName = () => { + return "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"; + }; +} + export class RsaSha256Mgf1 implements SignatureAlgorithm { getSignature = createOptionalCallbackFunction( (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 663d3d0e..7c27854b 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -117,6 +117,7 @@ export class SignedXml { SignatureAlgorithms: Record SignatureAlgorithm> = { "http://www.w3.org/2000/09/xmldsig#rsa-sha1": signatureAlgorithms.RsaSha1, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": signatureAlgorithms.RsaSha256, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": signatureAlgorithms.EcdsaSha256, "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1": signatureAlgorithms.RsaSha256Mgf1, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": signatureAlgorithms.RsaSha512, // Disabled by default due to key confusion concerns. From ff5d59c763bc09d375c6380a48725888ed9e9bfc Mon Sep 17 00:00:00 2001 From: adam strickland Date: Sat, 31 Jan 2026 11:34:42 +0000 Subject: [PATCH 2/5] Swap to IEEE-P1363, create local_example with required fields --- example/local_example.js | 53 +++++++++++++++++++++++++++++++++++++ src/signature-algorithms.ts | 8 ++++-- src/types.ts | 1 + 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 example/local_example.js diff --git a/example/local_example.js b/example/local_example.js new file mode 100644 index 00000000..96c38016 --- /dev/null +++ b/example/local_example.js @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ + +const select = require("xpath") +const dom = require("@xmldom/xmldom").DOMParser; +const SignedXml = require("../").SignedXml; +const fs = require("fs"); + +function signXml(xml, xpath, key, dest) { + const sig = new SignedXml(); + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#" + sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" + sig.privateKey = fs.readFileSync(key); + sig.addReference({ + xpath, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + sig.computeSignature(xml); + console.log(sig.getSignedXml()) +} + +function validateXml(xml, key) { + const doc = new dom().parseFromString(xml); + const signature = select( + "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + )[0]; + const sig = new SignedXml(); + sig.publicCert = key; + sig.loadSignature(signature.toString()); + const res = sig.checkSignature(xml); + if (!res) { + console.log(sig.validationErrors); + } + return res; +} + +const xml = "" + "" + "Harry Potter" + "" + ""; + +//sign an xml document +signXml(xml, "//*[local-name(.)='book']", __dirname + "/client.pem", __dirname + "/result.xml"); + +console.log("xml signed successfully"); + +const signedXml = fs.readFileSync("result.xml").toString(); +console.log("validating signature..."); + +//validate an xml document +if (validateXml(signedXml, "client_public.pem")) { + console.log("signature is valid"); +} else { + console.log("signature not valid"); +} diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index cd4872a1..e2d2a2d0 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -56,9 +56,12 @@ export class RsaSha256 implements SignatureAlgorithm { export class EcdsaSha256 implements SignatureAlgorithm { getSignature = createOptionalCallbackFunction( (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { + // Maybe the fix for ts-ignore below? + // const parsedPrivateKey = crypto.createPrivateKey(privateKey); const signer = crypto.createSign("SHA256"); signer.update(signedInfo); - const res = signer.sign(privateKey, "base64"); + // @ts-ignore + const res = signer.sign({key: privateKey, dsaEncoding: 'ieee-p1363'}, "base64"); return res; }, @@ -66,9 +69,10 @@ export class EcdsaSha256 implements SignatureAlgorithm { verifySignature = createOptionalCallbackFunction( (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { + const publicKey = crypto.createPublicKey(key); const verifier = crypto.createVerify("SHA256"); verifier.update(material); - const res = verifier.verify(key, signatureValue, "base64"); + const res = verifier.verify({key: publicKey, dsaEncoding: 'ieee-p1363'}, signatureValue, "base64"); return res; }, diff --git a/src/types.ts b/src/types.ts index 89c0b304..1e72861c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,7 @@ export type HashAlgorithmType = export type SignatureAlgorithmType = | "http://www.w3.org/2000/09/xmldsig#rsa-sha1" | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + | "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" | "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" | "http://www.w3.org/2000/09/xmldsig#hmac-sha1" From b9bb7ed06235c6c578cbac31f05dc220be8aa232 Mon Sep 17 00:00:00 2001 From: adam strickland Date: Sat, 31 Jan 2026 16:48:00 +0000 Subject: [PATCH 3/5] Fix example and local example --- example/example.js | 16 ++++++++++++---- example/local_example.js | 21 ++++++++++++--------- example/result.xml | 1 + package.json | 3 ++- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 example/result.xml diff --git a/example/example.js b/example/example.js index 32c9e8b6..d5c24ac5 100644 --- a/example/example.js +++ b/example/example.js @@ -5,10 +5,17 @@ const dom = require("@xmldom/xmldom").DOMParser; const SignedXml = require("xml-crypto").SignedXml; const fs = require("fs"); -function signXml(xml, xpath, key, dest) { +function signXml(xml, xpath, key, dest, cert) { const sig = new SignedXml(); + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#" + sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" sig.privateKey = fs.readFileSync(key); - sig.addReference(xpath); + sig.publicCert = fs.readFileSync(cert); // To populate KeyInfo, as an example + sig.addReference({ + xpath, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); sig.computeSignature(xml); fs.writeFileSync(dest, sig.getSignedXml()); } @@ -20,7 +27,8 @@ function validateXml(xml, key) { doc, )[0]; const sig = new SignedXml(); - sig.publicCert = key; + sig.publicCert = fs.readFileSync(key); // Note since the XML has a KeyInfo, this cert is NOT doing anything! + // Validate the cert in `KeyInfo` on your own if that is your security model. See: sig.loadSignature(signature.toString()); const res = sig.checkSignature(xml); if (!res) { @@ -32,7 +40,7 @@ function validateXml(xml, key) { const xml = "" + "" + "Harry Potter" + "" + ""; //sign an xml document -signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml"); +signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml", "client_public.pem"); console.log("xml signed successfully"); diff --git a/example/local_example.js b/example/local_example.js index 96c38016..4065a7cf 100644 --- a/example/local_example.js +++ b/example/local_example.js @@ -1,22 +1,24 @@ /* eslint-disable no-console */ +// Run with `npm run example`, requires one-time `npm run build` to generate `/lib` code (and re-run if you update `/src`) -const select = require("xpath") +const select = require("xpath").select const dom = require("@xmldom/xmldom").DOMParser; const SignedXml = require("../").SignedXml; const fs = require("fs"); -function signXml(xml, xpath, key, dest) { +function signXml(xml, xpath, key, dest, cert) { const sig = new SignedXml(); sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#" - sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" - sig.privateKey = fs.readFileSync(key); + sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + sig.privateKey = fs.readFileSync(__dirname + "/" + key); + sig.publicCert = fs.readFileSync(__dirname + "/" + cert); // To populate KeyInfo, as an example sig.addReference({ xpath, digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); sig.computeSignature(xml); - console.log(sig.getSignedXml()) + fs.writeFileSync(__dirname + "/" + dest, sig.getSignedXml()); } function validateXml(xml, key) { @@ -26,7 +28,8 @@ function validateXml(xml, key) { doc, )[0]; const sig = new SignedXml(); - sig.publicCert = key; + sig.publicCert = fs.readFileSync(__dirname + "/" + key); // Note since the XML has a KeyInfo, this cert is NOT doing anything! + // Validate the cert in `KeyInfo` on your own if that is your security model. See: sig.loadSignature(signature.toString()); const res = sig.checkSignature(xml); if (!res) { @@ -38,11 +41,11 @@ function validateXml(xml, key) { const xml = "" + "" + "Harry Potter" + "" + ""; //sign an xml document -signXml(xml, "//*[local-name(.)='book']", __dirname + "/client.pem", __dirname + "/result.xml"); +signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml", "client_public.pem"); console.log("xml signed successfully"); -const signedXml = fs.readFileSync("result.xml").toString(); +const signedXml = fs.readFileSync(__dirname + "/" + "result.xml").toString(); console.log("validating signature..."); //validate an xml document @@ -50,4 +53,4 @@ if (validateXml(signedXml, "client_public.pem")) { console.log("signature is valid"); } else { console.log("signature not valid"); -} +} \ No newline at end of file diff --git a/example/result.xml b/example/result.xml new file mode 100644 index 00000000..35eb3086 --- /dev/null +++ b/example/result.xml @@ -0,0 +1 @@ +Harry Potter9d/ciWlVZkaJnJ3KBB5WY1H2Y8WRXPB2DquM0goT8jY=uxmxGw2O3B6ylkhEXOaZd1Iupgy3sHtCoBTgbmSMSnHYOitiHXRdHjJGJdMG4EMSgItsB6k5gxrKeyQ/5LkwvMqSc0VRPXd9vavt0pYatqwWDO/r6WITLb0jzymJfNDJ4lr4OcqH4zBKX8Deb6EpS9L7S6OXNqd1vOZ0STMSSaM=MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl \ No newline at end of file diff --git a/package.json b/package.json index 8bd4caa0..5d96b443 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "prettier-format": "prettier --config .prettierrc.json --write .", "prerelease": "git clean -xfd && npm ci && npm test", "release": "release-it", - "test": "nyc mocha" + "test": "nyc mocha", + "example": "node ./example/local_example.js" }, "dependencies": { "@xmldom/is-dom-node": "^1.0.1", From 575f7831268b003243f40ab980e1cb3bda362b2f Mon Sep 17 00:00:00 2001 From: adam strickland Date: Sat, 31 Jan 2026 16:48:29 +0000 Subject: [PATCH 4/5] Add ECDSA unit tests --- test/signature-unit-tests.spec.ts | 98 +++++++++++++++++++++++++++ test/static/client_ecdsa.pem | 5 ++ test/static/client_public_ecdsa.pem | 13 ++++ test/static/ecdsa_external.pem | 8 +++ test/static/valid_signature_ecdsa.xml | 1 + 5 files changed, 125 insertions(+) create mode 100644 test/static/client_ecdsa.pem create mode 100644 test/static/client_public_ecdsa.pem create mode 100644 test/static/ecdsa_external.pem create mode 100644 test/static/valid_signature_ecdsa.xml diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index c0dcf136..4668d811 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -12,6 +12,7 @@ const signatureAlgorithms = [ "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", ]; +const ecdsaSignatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"; describe("Signature unit tests", function () { describe("sign and verify", function () { @@ -75,6 +76,76 @@ describe("Signature unit tests", function () { }); }); + describe("sign and verify - ecdsa", function () { + const signatureAlgorithm = ecdsaSignatureAlgorithm; + function signWith(signatureAlgorithm: string): string { + const xml = ''; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client_ecdsa.pem"); + + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = signatureAlgorithm; + sig.computeSignature(xml); + return sig.getSignedXml(); + } + + function loadSignature(xml: string): SignedXml { + const doc = new xmldom.DOMParser().parseFromString(xml); + const node = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(node); + const sig = new SignedXml(); + sig.publicCert = fs.readFileSync("./test/static/client_public_ecdsa.pem"); + sig.loadSignature(node); + return sig; + } + + it(`should verify signed xml with ${signatureAlgorithm}`, function () { + const xml = signWith(signatureAlgorithm); + const sig = loadSignature(xml); + const res = sig.checkSignature(xml); + expect( + res, + `expected all signatures with ${signatureAlgorithm} to be valid, but some reported invalid`, + ).to.be.true; + }); + + it(`should verify signed xml with ${ecdsaSignatureAlgorithm}`, function () { + const xml = signWith(ecdsaSignatureAlgorithm); + const sig = loadSignature(xml); + const res = sig.checkSignature(xml); + expect( + res, + `expected all signatures with ${ecdsaSignatureAlgorithm} to be valid, but some reported invalid`, + ).to.be.true; + }); + + it(`should fail verification of signed xml with ${signatureAlgorithm} after manipulation`, function () { + const xml = signWith(signatureAlgorithm); + const doc = new xmldom.DOMParser().parseFromString(xml); + const node = xpath.select1("//*[local-name(.)='x']", doc); + isDomNode.assertIsElementNode(node); + const targetElement = node as Element; + targetElement.setAttribute("attr", "manipulatedValue"); + const manipulatedXml = new xmldom.XMLSerializer().serializeToString(doc); + + const sig = loadSignature(manipulatedXml); + const res = sig.checkSignature(manipulatedXml); + expect( + res, + `expected all signatures with ${signatureAlgorithm} to be invalid, but some reported valid`, + ).to.be.false; + }); + }); + describe("verify adds ID", function () { function nodeExists(doc, xpathArg) { if (!doc && !xpathArg) { @@ -943,6 +1014,20 @@ describe("Signature unit tests", function () { return sig; } + function loadEcdsaSignature(xml: string) { + const doc = new xmldom.DOMParser().parseFromString(xml); + const node = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(node); + const sig = new SignedXml(); + sig.publicCert = fs.readFileSync("./test/static/ecdsa_external.pem"); + sig.loadSignature(node); + + return sig; + } + function passValidSignature(file: string, mode?: "wssecurity") { const xml = fs.readFileSync(file, "utf8"); const sig = loadSignature(xml, mode); @@ -952,6 +1037,15 @@ describe("Signature unit tests", function () { expect(sig.getSignedReferences().length).to.equal(sig.getReferences().length); } + function passValidEcdsaSignature(file: string) { + const xml = fs.readFileSync(file, "utf8"); + const sig = loadEcdsaSignature(xml); + const res = sig.checkSignature(xml); + expect(res, "expected all signatures to be valid, but some reported invalid").to.be.true; + /* eslint-disable-next-line deprecation/deprecation */ + expect(sig.getSignedReferences().length).to.equal(sig.getReferences().length); + } + function failInvalidSignature(file: string, idMode?: "wssecurity") { const xml = fs.readFileSync(file).toString(); const sig = loadSignature(xml, idMode); @@ -973,6 +1067,10 @@ describe("Signature unit tests", function () { it("verifies valid signature", function () { passValidSignature("./test/static/valid_signature.xml"); }); + + it("verifies valid ecdsa signature", function () { + passValidEcdsaSignature("./test/static/valid_signature_ecdsa.xml"); + }); it("verifies valid signature with lowercase id attribute", function () { passValidSignature("./test/static/valid_signature_with_lowercase_id_attribute.xml"); diff --git a/test/static/client_ecdsa.pem b/test/static/client_ecdsa.pem new file mode 100644 index 00000000..ab921e3c --- /dev/null +++ b/test/static/client_ecdsa.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOg1FiE/iu8uoRXX3UvBs53JIEsjkcf9IbMpJsfkvG30oAoGCCqGSM49 +AwEHoUQDQgAE69ImJGeiClnYW20zXK3L+w5q463+PN302fpmEDE/6xTEbG/KIxcA +d77nrzo5Iq4ve2SqL0Bk1Yxk2V/1f8t52g== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/test/static/client_public_ecdsa.pem b/test/static/client_public_ecdsa.pem new file mode 100644 index 00000000..eb492ca0 --- /dev/null +++ b/test/static/client_public_ecdsa.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB3zCCAYWgAwIBAgIUYjTFVRq9oJ9JsEdzs9GEp+Ro2nYwCgYIKoZIzj0EAwIw +RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu +dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNjAxMjcyMDA0MjhaFw0yNzAxMjIy +MDA0MjhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD +VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAATr0iYkZ6IKWdhbbTNcrcv7Dmrjrf483fTZ+mYQMT/rFMRsb8ojFwB3 +vuevOjkiri97ZKovQGTVjGTZX/V/y3nao1MwUTAdBgNVHQ4EFgQUKdQQ4ogzLU06 +Gypz35quxaLJr50wHwYDVR0jBBgwFoAUKdQQ4ogzLU06Gypz35quxaLJr50wDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiADI3VXNdnYMIIFlVLS6Ss2 +E+tamOigyNvruaKT+0YiGQIhALYU9Dyu2fRRvULX7sBpv7Dxk/4ynUCcCTJ1L9SK +O9bJ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/static/ecdsa_external.pem b/test/static/ecdsa_external.pem new file mode 100644 index 00000000..4be53cb4 --- /dev/null +++ b/test/static/ecdsa_external.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE----- +MIIBEDCBuKADAgECAggVM8YfT8EdfTAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwR0 +ZXN0MB4XDTIxMDczMDA3NTU1NVoXDTIxMDczMDA3NTU1NVowDzENMAsGA1UEAxME +dGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpb/uly4GB+9G2BKgToi57/ +XzqapEdo2Pys48RMtj8tc6WE2BO0TJoR1KJZ1Bu05OQ0aOwyFGo1QY65V6sgONIw +CgYIKoZIzj0EAwIDRwAwRAIgV50ULGELC8aTSxOmTPptqHjOgKlbKLlQ+CuErOUB +CucCIBvn/IWSLPVqwoQNzP7VnRgk9mZvUuTW0MaIf/4lhOc7 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/static/valid_signature_ecdsa.xml b/test/static/valid_signature_ecdsa.xml new file mode 100644 index 00000000..24321a33 --- /dev/null +++ b/test/static/valid_signature_ecdsa.xml @@ -0,0 +1 @@ +Just remember ALL CAPS when you spell the man name9A4u9FDDvQS0fcqS76EbS5Ir95wh3JOu2QldyyfWrHs=A1Vz+93PgSq3auxwqW087exDtOYgSazYTSgYlXZgWVZI6tKXwrZZ9O4SdQiHHI4Y2Ro8Ho5zgf+HjpN/ushvPw==MIIBEDCBuKADAgECAggVM8YfT8EdfTAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwR0ZXN0MB4XDTIxMDczMDA3NTU1NVoXDTIxMDczMDA3NTU1NVowDzENMAsGA1UEAxMEdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpb/uly4GB+9G2BKgToi57/XzqapEdo2Pys48RMtj8tc6WE2BO0TJoR1KJZ1Bu05OQ0aOwyFGo1QY65V6sgONIwCgYIKoZIzj0EAwIDRwAwRAIgV50ULGELC8aTSxOmTPptqHjOgKlbKLlQ+CuErOUBCucCIBvn/IWSLPVqwoQNzP7VnRgk9mZvUuTW0MaIf/4lhOc7 \ No newline at end of file From 5a833c0cec6ccc00d98af891248e0c5f17aac9f8 Mon Sep 17 00:00:00 2001 From: adam strickland Date: Sat, 31 Jan 2026 17:38:39 +0000 Subject: [PATCH 5/5] Add ecdsa-sha512 --- README.md | 2 + src/signature-algorithms.ts | 34 ++++++++- src/signed-xml.ts | 1 + src/types.ts | 1 + test/signature-unit-tests.spec.ts | 116 ++++++++++++++---------------- 5 files changed, 91 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index dfe164f2..0d64f0f7 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ This will help prevent future XML signature wrapping attacks. - RSA-SHA256 - RSA-SHA256 with MGF1 - RSA-SHA512 +- ECDSA-SHA256 +- ECDSA-SHA512 HMAC-SHA1 is also available but it is disabled by default diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index e2d2a2d0..7faff756 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -61,7 +61,7 @@ export class EcdsaSha256 implements SignatureAlgorithm { const signer = crypto.createSign("SHA256"); signer.update(signedInfo); // @ts-ignore - const res = signer.sign({key: privateKey, dsaEncoding: 'ieee-p1363'}, "base64"); + const res = signer.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' }, "base64"); return res; }, @@ -72,7 +72,7 @@ export class EcdsaSha256 implements SignatureAlgorithm { const publicKey = crypto.createPublicKey(key); const verifier = crypto.createVerify("SHA256"); verifier.update(material); - const res = verifier.verify({key: publicKey, dsaEncoding: 'ieee-p1363'}, signatureValue, "base64"); + const res = verifier.verify({ key: publicKey, dsaEncoding: 'ieee-p1363' }, signatureValue, "base64"); return res; }, @@ -156,6 +156,36 @@ export class RsaSha512 implements SignatureAlgorithm { }; } +export class EcdsaSha512 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { + // Maybe the fix for ts-ignore below? + // const parsedPrivateKey = crypto.createPrivateKey(privateKey); + const signer = crypto.createSign("SHA512"); + signer.update(signedInfo); + // @ts-ignore + const res = signer.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' }, "base64"); + + return res; + }, + ); + + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { + const publicKey = crypto.createPublicKey(key); + const verifier = crypto.createVerify("SHA512"); + verifier.update(material); + const res = verifier.verify({ key: publicKey, dsaEncoding: 'ieee-p1363' }, signatureValue, "base64"); + + return res; + }, + ); + + getAlgorithmName = () => { + return "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"; + }; +} + export class HmacSha1 implements SignatureAlgorithm { getSignature = createOptionalCallbackFunction( (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 7c27854b..2626778a 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -120,6 +120,7 @@ export class SignedXml { "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": signatureAlgorithms.EcdsaSha256, "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1": signatureAlgorithms.RsaSha256Mgf1, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": signatureAlgorithms.RsaSha512, + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512": signatureAlgorithms.EcdsaSha512, // Disabled by default due to key confusion concerns. // 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': SignatureAlgorithms.HmacSha1 }; diff --git a/src/types.ts b/src/types.ts index 1e72861c..af92e203 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,7 @@ export type SignatureAlgorithmType = | "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" | "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" + | "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" | "http://www.w3.org/2000/09/xmldsig#hmac-sha1" | string; diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 4668d811..e17d8d8b 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -12,7 +12,10 @@ const signatureAlgorithms = [ "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", ]; -const ecdsaSignatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"; +const ecdsaSignatureAlgorithms = [ + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", +]; describe("Signature unit tests", function () { describe("sign and verify", function () { @@ -77,72 +80,63 @@ describe("Signature unit tests", function () { }); describe("sign and verify - ecdsa", function () { - const signatureAlgorithm = ecdsaSignatureAlgorithm; - function signWith(signatureAlgorithm: string): string { - const xml = ''; - const sig = new SignedXml(); - sig.privateKey = fs.readFileSync("./test/static/client_ecdsa.pem"); + ecdsaSignatureAlgorithms.forEach((signatureAlgorithm) => { + function signWith(signatureAlgorithm: string): string { + const xml = ''; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client_ecdsa.pem"); - sig.addReference({ - xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = signatureAlgorithm; - sig.computeSignature(xml); - return sig.getSignedXml(); - } + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = signatureAlgorithm; + sig.computeSignature(xml); + return sig.getSignedXml(); + } - function loadSignature(xml: string): SignedXml { - const doc = new xmldom.DOMParser().parseFromString(xml); - const node = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(node); - const sig = new SignedXml(); - sig.publicCert = fs.readFileSync("./test/static/client_public_ecdsa.pem"); - sig.loadSignature(node); - return sig; - } + function loadSignature(xml: string): SignedXml { + const doc = new xmldom.DOMParser().parseFromString(xml); + const node = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(node); + const sig = new SignedXml(); + sig.publicCert = fs.readFileSync("./test/static/client_public_ecdsa.pem"); + sig.loadSignature(node); + return sig; + } - it(`should verify signed xml with ${signatureAlgorithm}`, function () { - const xml = signWith(signatureAlgorithm); - const sig = loadSignature(xml); - const res = sig.checkSignature(xml); - expect( - res, - `expected all signatures with ${signatureAlgorithm} to be valid, but some reported invalid`, - ).to.be.true; - }); + it(`should verify signed xml with ${signatureAlgorithm}`, function () { + const xml = signWith(signatureAlgorithm); + const sig = loadSignature(xml); + const res = sig.checkSignature(xml); + expect( + res, + `expected all signatures with ${signatureAlgorithm} to be valid, but some reported invalid`, + ).to.be.true; + }); - it(`should verify signed xml with ${ecdsaSignatureAlgorithm}`, function () { - const xml = signWith(ecdsaSignatureAlgorithm); - const sig = loadSignature(xml); - const res = sig.checkSignature(xml); - expect( - res, - `expected all signatures with ${ecdsaSignatureAlgorithm} to be valid, but some reported invalid`, - ).to.be.true; - }); + it(`should fail verification of signed xml with ${signatureAlgorithm} after manipulation`, function () { + const xml = signWith(signatureAlgorithm); + const doc = new xmldom.DOMParser().parseFromString(xml); + const node = xpath.select1("//*[local-name(.)='x']", doc); + isDomNode.assertIsElementNode(node); + const targetElement = node as Element; + targetElement.setAttribute("attr", "manipulatedValue"); + const manipulatedXml = new xmldom.XMLSerializer().serializeToString(doc); - it(`should fail verification of signed xml with ${signatureAlgorithm} after manipulation`, function () { - const xml = signWith(signatureAlgorithm); - const doc = new xmldom.DOMParser().parseFromString(xml); - const node = xpath.select1("//*[local-name(.)='x']", doc); - isDomNode.assertIsElementNode(node); - const targetElement = node as Element; - targetElement.setAttribute("attr", "manipulatedValue"); - const manipulatedXml = new xmldom.XMLSerializer().serializeToString(doc); - - const sig = loadSignature(manipulatedXml); - const res = sig.checkSignature(manipulatedXml); - expect( - res, - `expected all signatures with ${signatureAlgorithm} to be invalid, but some reported valid`, - ).to.be.false; + const sig = loadSignature(manipulatedXml); + const res = sig.checkSignature(manipulatedXml); + expect( + res, + `expected all signatures with ${signatureAlgorithm} to be invalid, but some reported valid`, + ).to.be.false; + }); }); });