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/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 new file mode 100644 index 00000000..4065a7cf --- /dev/null +++ b/example/local_example.js @@ -0,0 +1,56 @@ +/* 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").select +const dom = require("@xmldom/xmldom").DOMParser; +const SignedXml = require("../").SignedXml; +const fs = require("fs"); + +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(__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); + fs.writeFileSync(__dirname + "/" + dest, 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 = 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) { + console.log(sig.validationErrors); + } + return res; +} + +const xml = "" + "" + "Harry Potter" + "" + ""; + +//sign an xml document +signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml", "client_public.pem"); + +console.log("xml signed successfully"); + +const signedXml = fs.readFileSync(__dirname + "/" + "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"); +} \ 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", diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index 52e09280..7faff756 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -53,6 +53,36 @@ 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); + // @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("SHA256"); + 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-sha256"; + }; +} + export class RsaSha256Mgf1 implements SignatureAlgorithm { getSignature = createOptionalCallbackFunction( (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { @@ -126,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 663d3d0e..2626778a 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -117,8 +117,10 @@ 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, + "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 89c0b304..af92e203 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,8 +30,10 @@ 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/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 c0dcf136..e17d8d8b 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -12,6 +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 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 () { @@ -75,6 +79,67 @@ describe("Signature unit tests", function () { }); }); + describe("sign and verify - ecdsa", function () { + 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.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 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 +1008,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 +1031,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 +1061,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