diff --git a/README.md b/README.md index 60403d22..f5357df1 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,20 @@ If the verification process fails `sig.validationErrors` will contain the errors In order to protect from some attacks we must check the content we want to use is the one that has been signed: ```javascript -var elem = select(doc, "/xpath_to_interesting_element"); +// Roll your own +var elem = xpath.select("/xpath_to_interesting_element", doc); var uri = sig.references[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"); + +// Use the built-in method +let elem = xpath.select("/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"); +} ``` Note: diff --git a/src/signed-xml.ts b/src/signed-xml.ts index b5d7da13..797fb26d 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -33,7 +33,8 @@ export class SignedXml { privateKey?: crypto.KeyLike; publicCert?: crypto.KeyLike; /** - * One of the supported signature algorithms. See {@link SignatureAlgorithmType} + * One of the supported signature algorithms. + * @see {@link SignatureAlgorithmType} */ signatureAlgorithm: SignatureAlgorithmType = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; /** @@ -56,21 +57,24 @@ export class SignedXml { getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; // Internal state - /** - * Specifies the data to be signed within an XML document. See {@link Reference} - */ - private references: Reference[] = []; private id = 0; private signedXml = ""; private signatureXml = ""; private signatureNode: Node | null = null; private signatureValue = ""; private originalXmlWithIds = ""; + private keyInfo: Node | null = null; + + /** + * Contains the references that were signed. + * @see {@link Reference} + */ + references: Reference[] = []; + /** * Contains validation errors (if any) after {@link checkSignature} method is called */ validationErrors: string[] = []; - private keyInfo: Node | null = null; /** * To add a new transformation algorithm create a new class that implements the {@link TransformationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} @@ -205,7 +209,7 @@ export class SignedXml { * Returns the value of the signing certificate based on the contents of the * specified KeyInfo. * - * @param keyInfo KeyInfo element (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data) + * @param keyInfo KeyInfo element (@see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data) * @return the signing certificate as a string in PEM format */ static getCertFromKeyInfo(keyInfo?: Node | null): string | null { @@ -389,6 +393,37 @@ export class SignedXml { } } + validateElementAgainstReferences(elem: Element, doc: Document): Reference { + for (const ref of this.references) { + const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; + let targetElem: xpath.SelectSingleReturnType; + + for (const attr of this.idAttributes) { + const elemId = elem.getAttribute(attr); + if (uri === elemId) { + targetElem = elem; + ref.xpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; + break; // found the correct element, no need to check further + } + } + + // @ts-expect-error This is a problem with the types on `xpath` + if (!xpath.isNodeLike(targetElem)) { + continue; + } + + const canonXml = this.getCanonReferenceXml(doc, ref, targetElem); + const hash = this.findHashAlgorithm(ref.digestAlgorithm); + const digest = hash.getHash(canonXml); + + if (utils.validateDigestValue(digest, ref.digestValue)) { + return ref; + } + } + + throw new Error("No references passed validation"); + } + validateReferences(doc) { for (const ref of this.references) { let elemXpath; @@ -573,7 +608,7 @@ export class SignedXml { * DigestMethods take an octet stream rather than a node set. If the output of the last transform is a node set, we * need to canonicalize the node set to an octet stream using non-exclusive canonicalization. If there are no * transforms, we need to canonicalize because URI dereferencing for a same-document reference will return a node-set. - * See: + * @see: * https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod * https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel * https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document diff --git a/src/types.ts b/src/types.ts index 1a2193b5..6cacddf6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,7 +35,7 @@ export type SignatureAlgorithmType = | string; /** - * @param cert the certificate as a string or array of strings (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data) + * @param cert the certificate as a string or array of strings (@see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data) * @param prefix an optional namespace alias to be used for the generated XML */ export interface GetKeyInfoContentArgs { diff --git a/src/utils.ts b/src/utils.ts index f1226a26..8896447c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,18 +69,22 @@ const xml_special_to_encoded_text = { export function encodeSpecialCharactersInAttribute(attributeValue) { return attributeValue.replace(/([&<"\r\n\t])/g, function (str, item) { - // Special character normalization. See: - // - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes) - // - https://www.w3.org/TR/xml-c14n#Example-Chars + /** Special character normalization. + * @see: + * - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes) + * - https://www.w3.org/TR/xml-c14n#Example-Chars + */ return xml_special_to_encoded_attribute[item]; }); } export function encodeSpecialCharactersInText(text: string): string { return text.replace(/([&<>\r])/g, function (str, item) { - // Special character normalization. See: - // - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes) - // - https://www.w3.org/TR/xml-c14n#Example-Chars + /** Special character normalization. + * @see: + * - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes) + * - https://www.w3.org/TR/xml-c14n#Example-Chars + */ return xml_special_to_encoded_text[item]; }); } diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index a548424e..995405dd 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -77,10 +77,12 @@ describe("Signature integration tests", function () { it("add canonicalization if output of transforms will be a node-set rather than an octet stream", function () { let xml = fs.readFileSync("./test/static/windows_store_signature.xml", "utf-8"); - // Make sure that whitespace in the source document is removed -- see xml-crypto issue #23 and post at - // http://webservices20.blogspot.co.il/2013/06/validating-windows-mobile-app-store.html - // This regex is naive but works for this test case; for a more general solution consider - // the xmldom-fork-fixed library which can pass {ignoreWhiteSpace: true} into the Dom constructor. + /** Make sure that whitespace in the source document is removed -- + * @see xml-crypto issue #23 and post at + * http://webservices20.blogspot.co.il/2013/06/validating-windows-mobile-app-store.html + * This regex is naive but works for this test case; for a more general solution consider + * the xmldom-fork-fixed library which can pass {ignoreWhiteSpace: true} into the Dom constructor. + */ xml = xml.replace(/>\s*<"); const doc = new xmldom.DOMParser().parseFromString(xml); diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index f7e558db..f1cbe3f4 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -82,7 +82,6 @@ describe("Signature unit tests", function () { const checkedSignature = sig.checkSignature(xml); expect(checkedSignature).to.be.true; - // @ts-expect-error FIXME expect(sig.references.length).to.equal(3); const digests = [ @@ -91,9 +90,17 @@ describe("Signature unit tests", function () { "sH1gxKve8wlU8LlFVa2l6w3HMJ0=", ]; + const firstGrandchild = doc.firstChild?.firstChild; + // @ts-expect-error FIXME - for (let i = 0; i < sig.references.length; i++) { + if (xpath.isElement(firstGrandchild)) { + expect(() => sig.validateElementAgainstReferences(firstGrandchild, doc)).to.not.throw; + } else { // @ts-expect-error FIXME + expect(xpath.isElement(firstGrandchild)).to.be.true; + } + + for (let i = 0; i < sig.references.length; i++) { const ref = sig.references[i]; const expectedUri = `#_${i}`; expect(