From 2aa2d133209b234be1c00ee31f990ce0017512d3 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 7 Oct 2023 07:24:11 -0500 Subject: [PATCH] Add support for directly querying a node to see if it has passed validation (#389) --- README.md | 71 +++++++++++++++++++++++-------------- src/signed-xml.ts | 14 ++++++++ src/types.ts | 2 ++ test/document-tests.spec.ts | 71 +++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c2308525..6a49f1c6 100644 --- a/README.md +++ b/README.md @@ -21,27 +21,27 @@ A pre requisite it to have [openssl](http://www.openssl.org/) installed and its ### Canonicalization and Transformation Algorithms -- Canonicalization http://www.w3.org/TR/2001/REC-xml-c14n-20010315 -- Canonicalization with comments http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments -- Exclusive Canonicalization http://www.w3.org/2001/10/xml-exc-c14n# -- Exclusive Canonicalization with comments http://www.w3.org/2001/10/xml-exc-c14n#WithComments -- Enveloped Signature transform http://www.w3.org/2000/09/xmldsig#enveloped-signature +- Canonicalization +- Canonicalization with comments +- Exclusive Canonicalization +- Exclusive Canonicalization with comments +- Enveloped Signature transform ### Hashing Algorithms -- SHA1 digests http://www.w3.org/2000/09/xmldsig#sha1 -- SHA256 digests http://www.w3.org/2001/04/xmlenc#sha256 -- SHA512 digests http://www.w3.org/2001/04/xmlenc#sha512 +- SHA1 digests +- SHA256 digests +- SHA512 digests ### Signature Algorithms -- RSA-SHA1 http://www.w3.org/2000/09/xmldsig#rsa-sha1 -- RSA-SHA256 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 -- RSA-SHA512 http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 +- RSA-SHA1 +- RSA-SHA256 +- RSA-SHA512 HMAC-SHA1 is also available but it is disabled by default -- HMAC-SHA1 http://www.w3.org/2000/09/xmldsig#hmac-sha1 +- HMAC-SHA1 to enable HMAC-SHA1, call `enableHMAC()` on your instance of `SignedXml`. @@ -51,11 +51,11 @@ signature algorithms enabled at same time. by default the following algorithms are used: -_Canonicalization/Transformation Algorithm:_ Exclusive Canonicalization http://www.w3.org/2001/10/xml-exc-c14n# +_Canonicalization/Transformation Algorithm:_ Exclusive Canonicalization -_Hashing Algorithm:_ SHA1 digest http://www.w3.org/2000/09/xmldsig#sha1 +_Hashing Algorithm:_ SHA1 digest -_Signature Algorithm:_ RSA-SHA1 http://www.w3.org/2000/09/xmldsig#rsa-sha1 +_Signature Algorithm:_ RSA-SHA1 [You are able to extend xml-crypto with custom algorithms.](#customizing-algorithms) @@ -154,18 +154,37 @@ In order to protect from some attacks we must check the content we want to use i ```javascript // Roll your own -var elem = xpath.select("/xpath_to_interesting_element", doc); -var uri = sig.getReferences()[0].uri; // might not be 0 - depending on the document you verify -var id = uri[0] === "#" ? uri.substring(1) : uri; -if (elem.getAttribute("ID") != id && elem.getAttribute("Id") != id && elem.getAttribute("id") != id) - throw new Error("the interesting element was not the one verified by the signature"); +const elem = xpath.select("/xpath_to_interesting_element", doc); +const uri = sig.getReferences()[0].uri; // might not be 0; it depends on the document +const id = uri[0] === "#" ? uri.substring(1) : uri; +if ( + elem.getAttribute("ID") != id && + elem.getAttribute("Id") != id && + elem.getAttribute("id") != id +) { + throw new Error("The interesting element was not the one verified by the signature"); +} + +// Get the validated element directly from a reference +const elem = sig.references[0].getValidatedElement(); // might not be 0; it depends on the document +const matchingReference = xpath.select1("/xpath_to_interesting_element", elem); +if (!isDomNode.isNodeLike(matchingReference)) { + throw new Error("The interesting element was not the one verified by the signature"); +} // Use the built-in method -let elem = xpath.select("/xpath_to_interesting_element", doc); +const elem = xpath.select1("/xpath_to_interesting_element", doc); try { const matchingReference = sig.validateElementAgainstReferences(elem, doc); } catch { - throw new Error("the interesting element was not the one verified by the signature"); + throw new Error("The interesting element was not the one verified by the signature"); +} + +// Use the built-in method with a an xpath expression +try { + const matchingReference = sig.validateReferenceWithXPath("/xpath_to_interesting_element", doc); +} catch { + throw new Error("The interesting element was not the one verified by the signature"); } ``` @@ -195,10 +214,10 @@ var res = sig.checkSignature(xml); You might find it difficult to guess such transforms, but there are typical transforms you can try. -- http://www.w3.org/TR/2001/REC-xml-c14n-20010315 -- http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments -- http://www.w3.org/2001/10/xml-exc-c14n# -- http://www.w3.org/2001/10/xml-exc-c14n#WithComments +- +- +- +- ## API diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 51997e35..53ef3ec7 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -430,6 +430,15 @@ export class SignedXml { } } + ref.getValidatedNode = (xpathSelector?: string) => { + xpathSelector = xpathSelector || ref.xpath; + if (typeof xpathSelector !== "string" || ref.validationError != null) { + return null; + } + const selectedValue = xpath.select1(xpathSelector, doc); + return isDomNode.isNodeLike(selectedValue) ? selectedValue : null; + }; + if (!isDomNode.isNodeLike(elem)) { const validationError = new Error( `invalid signature: the signature references an element with uri ${ref.uri} but could not find such element in the xml`, @@ -641,6 +650,11 @@ export class SignedXml { digestValue, inclusiveNamespacesPrefixList, isEmptyUri, + getValidatedNode: () => { + throw new Error( + "Reference has not been validated yet; Did you call `sig.checkSignature()`?", + ); + }, }); } diff --git a/src/types.ts b/src/types.ts index d7c9e5f8..97aae1ba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -131,6 +131,8 @@ export interface Reference { ancestorNamespaces?: NamespacePrefix[]; validationError?: Error; + + getValidatedNode(xpathSelector?: string): Node | null; } /** Implement this to create a new CanonicalizationOrTransformationAlgorithm */ diff --git a/test/document-tests.spec.ts b/test/document-tests.spec.ts index 5658bea1..37afb212 100644 --- a/test/document-tests.spec.ts +++ b/test/document-tests.spec.ts @@ -41,3 +41,74 @@ describe("Document tests", function () { expect(result).to.be.true; }); }); + +describe("Validated node references tests", function () { + it("should return references if the document is validly signed", function () { + const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + sig.loadSignature(sig.findSignatures(doc)[0]); + const validSignature = sig.checkSignature(xml); + expect(validSignature).to.be.true; + + const ref = sig.getReferences()[0]; + const result = ref.getValidatedNode(); + expect(result?.toString()).to.equal(doc.toString()); + }); + + it("should not return references if the document is not validly signed", function () { + const xml = fs.readFileSync("./test/static/invalid_signature - changed content.xml", "utf-8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + sig.loadSignature(sig.findSignatures(doc)[0]); + const validSignature = sig.checkSignature(xml); + expect(validSignature).to.be.false; + + const ref = sig.getReferences()[1]; + const result = ref.getValidatedNode(); + expect(result).to.be.null; + }); + + it("should return `null` if the selected node isn't found", function () { + const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + sig.loadSignature(sig.findSignatures(doc)[0]); + const validSignature = sig.checkSignature(xml); + expect(validSignature).to.be.true; + + const ref = sig.getReferences()[0]; + const result = ref.getValidatedNode("/non-existent-node"); + expect(result).to.be.null; + }); + + it("should return the selected node if it is validly signed", function () { + const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + sig.loadSignature(sig.findSignatures(doc)[0]); + const validSignature = sig.checkSignature(xml); + expect(validSignature).to.be.true; + + const ref = sig.getReferences()[0]; + const result = ref.getValidatedNode( + "//*[local-name()='Attribute' and @Name='mail']/*[local-name()='AttributeValue']/text()", + ); + expect(result?.nodeValue).to.equal("henri.bergius@nemein.com"); + }); + + it("should return `null` if the selected node isn't validly signed", function () { + const xml = fs.readFileSync("./test/static/invalid_signature - changed content.xml", "utf-8"); + const doc = new xmldom.DOMParser().parseFromString(xml); + const sig = new SignedXml(); + sig.loadSignature(sig.findSignatures(doc)[0]); + const validSignature = sig.checkSignature(xml); + expect(validSignature).to.be.false; + + const ref = sig.getReferences()[0]; + const result = ref.getValidatedNode( + "//*[local-name()='Attribute' and @Name='mail']/*[local-name()='AttributeValue']/text()", + ); + expect(result).to.be.null; + }); +});