diff --git a/src/signed-xml.ts b/src/signed-xml.ts index e5d80af..322e2d0 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -637,6 +637,28 @@ export class SignedXml { }); } + addReferenceToAllRootChildren( + doc: Document, + transforms: CanonicalizationOrTransformAlgorithmType[], + ) { + const root = doc.documentElement; + if (root == null) { + throw new Error("Document has no root element"); + } + + const children = root.childNodes; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (isDomNode.isElementNode(child)) { + this.addReference({ + xpath: `./*/*[local-name(.)='${child.localName}']`, + transforms, + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + }); + } + } + } + /** * Adds a reference to the signature. * @@ -798,10 +820,19 @@ export class SignedXml { // add the xml namespace attribute signatureAttrs.push(`${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#"`); + const keyInfo = this.getKeyInfo(prefix); + if (keyInfo != null && keyInfo.length > 0) { + // this.addReference({ + // xpath: ".//*[local-name(.)='KeyInfo']", + // transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + // digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + // }); + } + let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`; - signatureXml += this.createSignedInfo(doc, prefix); - signatureXml += this.getKeyInfo(prefix); + // signatureXml += this.createSignedInfo(doc, prefix); + signatureXml += keyInfo; signatureXml += ``; this.originalXmlWithIds = doc.toString(); @@ -854,6 +885,12 @@ export class SignedXml { referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling); } + // Now that we've inserted the Signature node into the document + // we need to calculate the SignedInfo and insert it into the Signature element + const signedInfoXml = this.createSignedInfo(doc, prefix); + const signedInfoElement = new xmldom.DOMParser().parseFromString(signedInfoXml).documentElement; + signatureDoc.insertBefore(signedInfoElement, signatureDoc.firstChild); + this.signatureNode = signatureDoc; const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo"); if (signedInfoNodes.length === 0) { @@ -865,7 +902,6 @@ export class SignedXml { return; } } - const signedInfoNode = signedInfoNodes[0]; if (typeof callback === "function") { // Asynchronous flow @@ -874,7 +910,7 @@ export class SignedXml { callback(err); } else { this.signatureValue = signature || ""; - signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); + signatureDoc.insertBefore(this.createSignature(prefix), signedInfoElement.nextSibling); this.signatureXml = signatureDoc.toString(); this.signedXml = doc.toString(); callback(null, this); @@ -883,7 +919,7 @@ export class SignedXml { } else { // Synchronous flow this.calculateSignatureValue(doc); - signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); + signatureDoc.insertBefore(this.createSignature(prefix), signedInfoElement.nextSibling); this.signatureXml = signatureDoc.toString(); this.signedXml = doc.toString(); } @@ -901,7 +937,14 @@ export class SignedXml { const keyInfoContent = this.getKeyInfoContent({ publicCert: this.publicCert, prefix }); if (keyInfoAttrs || keyInfoContent) { - return `<${currentPrefix}KeyInfo${keyInfoAttrs}>${keyInfoContent}`; + const keyInfoXml = `<${currentPrefix}KeyInfo${keyInfoAttrs}>${keyInfoContent}2`; + + // The following is if we want to always include an `Id` attribute in the `KeyInfo` element + // const keyInfoDoc = new xmldom.DOMParser().parseFromString(keyInfoXml); + // this.ensureHasId(keyInfoDoc.documentElement); + // return keyInfoDoc.toString(); + + return keyInfoXml; } return ""; @@ -920,7 +963,7 @@ export class SignedXml { for (const ref of this.getReferences()) { const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); - if (!utils.isArrayHasLength(nodes)) { + if (!utils.isArrayHasLength(nodes) || nodes.some((node) => !isDomNode.isElementNode(node))) { throw new Error( `the following xpath cannot be signed because it was not found: ${ref.xpath}`, ); @@ -930,6 +973,7 @@ export class SignedXml { if (ref.isEmptyUri) { res += `<${prefix}Reference URI="">`; } else { + isDomNode.assertIsElementNode(node) const id = this.ensureHasId(node); ref.uri = id; res += `<${prefix}Reference URI="#${id}">`; @@ -996,18 +1040,18 @@ export class SignedXml { * Ensure an element has Id attribute. If not create it with unique value. * Work with both normal and wssecurity Id flavour */ - private ensureHasId(node) { + private ensureHasId(elem: Element) { let attr; if (this.idMode === "wssecurity") { attr = utils.findAttr( - node, + elem, "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", ); } else { this.idAttributes.some((idAttribute) => { - attr = utils.findAttr(node, idAttribute); + attr = utils.findAttr(elem, idAttribute); return !!attr; // This will break the loop as soon as a truthy attr is found. }); } @@ -1020,18 +1064,18 @@ export class SignedXml { const id = `_${this.id++}`; if (this.idMode === "wssecurity") { - node.setAttributeNS( + elem.setAttributeNS( "http://www.w3.org/2000/xmlns/", "xmlns:wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", ); - node.setAttributeNS( + elem.setAttributeNS( "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "wsu:Id", id, ); } else { - node.setAttribute("Id", id); + elem.setAttribute("Id", id); } return id; diff --git a/src/utils.ts b/src/utils.ts index 6098286..d22b56c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,7 +18,7 @@ function attrEqualsImplicitly(attr: Attr, localName: string, namespace?: string, } export function findAttr(element: Element, localName: string, namespace?: string) { - for (let i = 0; i < element.attributes.length; i++) { + for (let i = 0; i < element.attributes?.length; i++) { const attr = element.attributes[i]; if ( diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 0db89a4..bd6e85a 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -187,6 +187,64 @@ describe("Signature unit tests", function () { ).to.equal("Signature"); }); + it("signer appends signature to all root node children when helper function is used", function () { + const xml = "xml-cryptogithub"; + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + sig.publicCert = fs.readFileSync("./test/static/client_public.pem"); + // sig.addReferenceToAllRootChildren(doc, ["http://www.w3.org/2001/10/xml-exc-c14n#"]); + sig.addReference({ + xpath: "/*", + 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 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.computeSignature(xml); + + const signatureElement = new xmldom.DOMParser().parseFromString( + sig.getSignatureXml(), + ).documentElement; + const signatureValueNode = xpath.select1( + ".//*[local-name(.)='SignatureValue']/text()", + signatureElement, + ); + isDomNode.assertIsTextNode(signatureValueNode); + const signatureValue = signatureValueNode.textContent; + expect(signatureValue).to.equal( + "GPMBtom1Qos8OXyAK2VdUadRaipPTjJpyOpO+6msrQM54L4OullXbpoEf/n7BCrRLzXUarw5xpxiBYOR4gFtTOOMD0LUN2zIQEdO87mjxhqoo8iVEHjuluILj2lEmhNYDsyQZfd7uUD2k5OgpBhpi9fgs+7w0N43iTfTiJ+4qC4=", + ); + expect(sig.getSignedXml()).to.equal( + 'xml-cryptogithubq2ONs+Sgbm1r4eFxQPyMGHWGay8=SoATUdmNo4CzkYtBMqktRZYdw/0=L2ghNuRUydddUrpL6OynnGW9JpNxmGANDXL90w4jXGiVPukTM6kn5dzQoTVlKokm5bzEtxibWRLJpeqLYoBlH7g2foIEoKpAIqa3I1an78BRrR/VjRzqT/QKrGtivgHgRID9FWCuZdMmP4h+RMA4t753iH/gsxts4OhrymxJayI=MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl', + ); + expect(sig.getSignedXml()).to.not.equal( + 'xml-cryptogithubq2ONs+Sgbm1r4eFxQPyMGHWGay8=1vawhQI+FPhqoGY3rXSh1pwtuyI=GlYi55Sr0zYtef64F6Wy2MKsCYw=n3yP4E3+0qyW/siPxzKU0RTetibusDU5n4OZ1Y0kV+N+qEh57Bj4Jk87RlvHFH0CNq0IJ8fJ+yyfW0d//WNSwHU1DIZkHFdl41M3S1pZWOsPPPCV+4ByCvJBn20enE/zY4okIyeU84PP081oSZMT5RtFsIbjJ6WtXEYZZq31dGs=MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl', + ); + }); + + it("signer adds a reference to KeyInfo when it is included", function () { + const xml = "xml-cryptogithub"; + const sig = new SignedXml(); + + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + sig.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.computeSignature(xml); + + const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const keyInfoNode = xpath.select1("//KeyInfo", doc); + + isDomNode.assertIsNodeLike(keyInfoNode); + const x509DataNode = xpath.select1("//X509Data", keyInfoNode); + + isDomNode.assertIsNodeLike(x509DataNode); + }); + it("signer appends signature to a reference node", function () { const xml = "xml-cryptogithub"; const sig = new SignedXml(); @@ -856,6 +914,16 @@ describe("Signature unit tests", function () { }); }); + describe("fail loading signatures", function () { + it("should be unable to load a signature if the CanonicalizationMethod is not missing", function () { + const xml = fs.readFileSync("./test/static/valid_signature.xml", "utf8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + + const sig = new SignedXml(); + expect(sig.loadSignature(sig.findSignatures(doc)[0])).to.throw; + }); + }); + describe("pass verify signature", function () { function verifySignature(xml: string, idMode?: "wssecurity") { const doc = new xmldom.DOMParser().parseFromString(xml); @@ -965,6 +1033,30 @@ describe("Signature unit tests", function () { failInvalidSignature("./test/static/invalid_signature_without_transforms_element.xml"); }); }); + + describe("pass check known signature", function () { + it("should check to make sure that signature is in list of expected signature", function () { + const xml = fs.readFileSync("./test/static/valid_signature.xml", "utf8"); + 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(); + const publicCert = fs.readFileSync("./test/static/client_public.pem"); + sig.publicCert = publicCert; + sig.loadSignature(node); + try { + const res = sig.checkSignature(xml); + const foundCert = sig.getCertFromKeyInfo(); + expect(foundCert).to.equal(sig.publicCert); + return res; + } catch (e) { + return false; + } + }); + }); }); it("allow empty reference uri when signing", function () {