From 5ba1d048723cec6ce0294fa7b853f0ea451664f6 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 18 Jul 2023 10:50:03 -0400 Subject: [PATCH] Convert this project to TypeScript (#325) --- src/c14n-canonicalization.ts | 79 ++- src/enveloped-signature.ts | 66 +- src/exclusive-canonicalization.ts | 86 ++- src/hash-algorithms.ts | 84 +-- src/index.ts | 18 +- src/signature-algorithms.ts | 164 ++--- src/signed-xml.ts | 713 +++++++++++++-------- src/utils.ts | 178 ++--- test/c14n-non-exclusive-unit-tests.spec.ts | 17 +- test/c14nWithComments-unit-tests.spec.ts | 33 +- test/canonicalization-unit-tests.spec.ts | 45 +- test/document-tests.spec.ts | 18 +- test/hmac-tests.spec.ts | 37 +- test/key-info-tests.spec.ts | 24 +- test/saml-response-tests.spec.ts | 51 +- test/signature-integration-tests.spec.ts | 50 +- test/signature-unit-tests.spec.ts | 448 ++++++++----- test/utils-tests.spec.ts | 10 +- test/wsfed-metadata-tests.spec.ts | 19 +- 19 files changed, 1233 insertions(+), 907 deletions(-) diff --git a/src/c14n-canonicalization.ts b/src/c14n-canonicalization.ts index 373dffa5..03406145 100644 --- a/src/c14n-canonicalization.ts +++ b/src/c14n-canonicalization.ts @@ -1,12 +1,14 @@ -const utils = require("./utils"); - -/** - * @type { import("../index.d.ts").CanonicalizationOrTransformationAlgorithm} - */ -class C14nCanonicalization { - constructor() { - this.includeComments = false; - } +import type { + CanonicalizationOrTransformationAlgorithm, + CanonicalizationOrTransformationAlgorithmProcessOptions, + NamespacePrefix, + RenderedNamespace, +} from "./types"; +import * as utils from "./utils"; +import * as xpath from "xpath"; + +export class C14nCanonicalization implements CanonicalizationOrTransformationAlgorithm { + includeComments = false; attrCompare(a, b) { if (!a.namespaceURI && b.namespaceURI) { @@ -40,9 +42,9 @@ class C14nCanonicalization { renderAttrs(node) { let i; let attr; - const attrListToRender = []; + const attrListToRender: Attr[] = []; - if (node.nodeType === 8) { + if (xpath.isComment(node)) { return this.renderComment(node); } @@ -69,21 +71,25 @@ class C14nCanonicalization { /** * Create the string of all namespace declarations that should appear on this element * - * @param {Node} node. The node we now render - * @param {Array} prefixesInScope. The prefixes defined on this node - * parents which are a part of the output set - * @param {String} defaultNs. The current default namespace - * @param {String} defaultNsForPrefix. - * @param {String} ancestorNamespaces - Import ancestor namespaces if it is specified - * @return {String} + * @param node The node we now render + * @param prefixesInScope The prefixes defined on this node parents which are a part of the output set + * @param defaultNs The current default namespace + * @param defaultNsForPrefix + * @param ancestorNamespaces Import ancestor namespaces if it is specified * @api private */ - renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) { + renderNs( + node: Element, + prefixesInScope: string[], + defaultNs: string, + defaultNsForPrefix: string, + ancestorNamespaces: NamespacePrefix[] + ): RenderedNamespace { let i; let attr; - const res = []; + const res: string[] = []; let newDefaultNs = defaultNs; - const nsListToRender = []; + const nsListToRender: { prefix: string; namespaceURI: string }[] = []; const currNs = node.namespaceURI || ""; //handle the namespace of the node itself @@ -97,7 +103,7 @@ class C14nCanonicalization { } } else if (defaultNs !== currNs) { //new default ns - newDefaultNs = node.namespaceURI; + newDefaultNs = node.namespaceURI || ""; res.push(' xmlns="', newDefaultNs, '"'); } @@ -127,7 +133,7 @@ class C14nCanonicalization { } } - if (Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0) { + if (utils.isArrayHasLength(ancestorNamespaces)) { // Remove namespaces which are already present in nsListToRender for (const ancestorNamespace of ancestorNamespaces) { let alreadyListed = false; @@ -158,11 +164,11 @@ class C14nCanonicalization { }) ); - return { rendered: res.join(""), newDefaultNs: newDefaultNs }; + return { rendered: res.join(""), newDefaultNs }; } processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) { - if (node.nodeType === 8) { + if (xpath.isComment(node)) { return this.renderComment(node); } if (node.data) { @@ -192,18 +198,18 @@ class C14nCanonicalization { } // Thanks to deoxxa/xml-c14n for comment renderer - renderComment(node) { + renderComment(node: Comment) { if (!this.includeComments) { return ""; } const isOutsideDocument = node.ownerDocument === node.parentNode; - let isBeforeDocument = null; - let isAfterDocument = null; + let isBeforeDocument = false; + let isAfterDocument = false; if (isOutsideDocument) { - let nextNode = node; - let previousNode = node; + let nextNode: ChildNode | null = node; + let previousNode: ChildNode | null = node; while (nextNode !== null) { if (nextNode === node.ownerDocument.documentElement) { @@ -240,13 +246,13 @@ class C14nCanonicalization { * @return {String} * @api public */ - process(node, options) { + process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions) { options = options || {}; const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; const ancestorNamespaces = options.ancestorNamespaces || []; - const prefixesInScope = []; + const prefixesInScope: string[] = []; for (let i = 0; i < ancestorNamespaces.length; i++) { prefixesInScope.push(ancestorNamespaces[i].prefix); } @@ -268,10 +274,8 @@ class C14nCanonicalization { /** * Add c14n#WithComments here (very simple subclass) - * - * @type { import("../index.d.ts").CanonicalizationOrTransformationAlgorithm} */ -class C14nCanonicalizationWithComments extends C14nCanonicalization { +export class C14nCanonicalizationWithComments extends C14nCanonicalization { constructor() { super(); this.includeComments = true; @@ -281,8 +285,3 @@ class C14nCanonicalizationWithComments extends C14nCanonicalization { return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"; } } - -module.exports = { - C14nCanonicalization, - C14nCanonicalizationWithComments, -}; diff --git a/src/enveloped-signature.ts b/src/enveloped-signature.ts index db6e8af0..80a0a553 100644 --- a/src/enveloped-signature.ts +++ b/src/enveloped-signature.ts @@ -1,47 +1,55 @@ -const xpath = require("xpath"); -const utils = require("./utils"); +import * as xpath from "xpath"; -/** - * @type { import("../index.d.ts").CanonicalizationOrTransformationAlgorithm} - */ -class EnvelopedSignature { - process(node, options) { +import type { + CanonicalizationOrTransformationAlgorithm, + CanonicalizationOrTransformationAlgorithmProcessOptions, + CanonicalizationOrTransformAlgorithmType, +} from "./types"; + +export class EnvelopedSignature implements CanonicalizationOrTransformationAlgorithm { + includeComments = false; + process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions) { if (null == options.signatureNode) { - const signature = xpath.select( + const signature = xpath.select1( "./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", node - )[0]; - if (signature) { + ); + if (xpath.isNodeLike(signature) && signature.parentNode) { signature.parentNode.removeChild(signature); } return node; } const signatureNode = options.signatureNode; - const expectedSignatureValue = utils.findFirst( - signatureNode, - ".//*[local-name(.)='SignatureValue']/text()" - ).data; - const signatures = xpath.select( - ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - node + const expectedSignatureValue = xpath.select1( + ".//*[local-name(.)='SignatureValue']/text()", + signatureNode ); - for (const nodeSignature of signatures) { - const signatureValue = utils.findFirst( - nodeSignature, - ".//*[local-name(.)='SignatureValue']/text()" - ).data; - if (expectedSignatureValue === signatureValue) { - nodeSignature.parentNode.removeChild(nodeSignature); + if (xpath.isTextNode(expectedSignatureValue)) { + const expectedSignatureValueData = expectedSignatureValue.data; + + const signatures = xpath.select( + ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + node + ); + for (const nodeSignature of Array.isArray(signatures) ? signatures : []) { + const signatureValue = xpath.select1( + ".//*[local-name(.)='SignatureValue']/text()", + nodeSignature + ); + if (xpath.isTextNode(signatureValue)) { + const signatureValueData = signatureValue.data; + if (expectedSignatureValueData === signatureValueData) { + if (nodeSignature.parentNode) { + nodeSignature.parentNode.removeChild(nodeSignature); + } + } + } } } return node; } - getAlgorithmName() { + getAlgorithmName(): CanonicalizationOrTransformAlgorithmType { return "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; } } - -module.exports = { - EnvelopedSignature, -}; \ No newline at end of file diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 961da497..4f76c83a 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -1,4 +1,10 @@ -const utils = require("./utils"); +import type { + CanonicalizationOrTransformationAlgorithm, + CanonicalizationOrTransformationAlgorithmProcessOptions, + NamespacePrefix, +} from "./types"; +import * as utils from "./utils"; +import * as xpath from "xpath"; function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { let ret = false; @@ -11,13 +17,8 @@ function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { return ret; } -/** - * @type { import("../index.d.ts").CanonicalizationOrTransformationAlgorithm} - */ -class ExclusiveCanonicalization { - constructor() { - this.includeComments = false; - } +export class ExclusiveCanonicalization implements CanonicalizationOrTransformationAlgorithm { + includeComments = false; attrCompare(a, b) { if (!a.namespaceURI && b.namespaceURI) { @@ -51,10 +52,10 @@ class ExclusiveCanonicalization { renderAttrs(node) { let i; let attr; - const res = []; - const attrListToRender = []; + const res: string[] = []; + const attrListToRender: Attr[] = []; - if (node.nodeType === 8) { + if (xpath.isComment(node)) { return this.renderComment(node); } @@ -88,12 +89,18 @@ class ExclusiveCanonicalization { * @return {String} * @api private */ - renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList) { + renderNs( + node, + prefixesInScope, + defaultNs, + defaultNsForPrefix, + inclusiveNamespacesPrefixList: string[] + ) { let i; let attr; - const res = []; + const res: string[] = []; let newDefaultNs = defaultNs; - const nsListToRender = []; + const nsListToRender: NamespacePrefix[] = []; const currNs = node.namespaceURI || ""; //handle the namespaceof the node itself @@ -165,9 +172,9 @@ class ExclusiveCanonicalization { prefixesInScope, defaultNs, defaultNsForPrefix, - inclusiveNamespacesPrefixList + inclusiveNamespacesPrefixList: string[] ) { - if (node.nodeType === 8) { + if (xpath.isComment(node)) { return this.renderComment(node); } if (node.data) { @@ -203,20 +210,20 @@ class ExclusiveCanonicalization { } // Thanks to deoxxa/xml-c14n for comment renderer - renderComment(node) { + renderComment(node: Comment) { if (!this.includeComments) { return ""; } const isOutsideDocument = node.ownerDocument === node.parentNode; - let isBeforeDocument = null; - let isAfterDocument = null; + let isBeforeDocument = false; + let isAfterDocument = false; if (isOutsideDocument) { - let nextNode = node; - let previousNode = node; + let nextNode: ChildNode | null = node; + let previousNode: ChildNode | null = node; - while (nextNode !== null) { + while (nextNode != null) { if (nextNode === node.ownerDocument.documentElement) { isBeforeDocument = true; break; @@ -225,7 +232,7 @@ class ExclusiveCanonicalization { nextNode = nextNode.nextSibling; } - while (previousNode !== null) { + while (previousNode != null) { if (previousNode === node.ownerDocument.documentElement) { isAfterDocument = true; break; @@ -251,21 +258,17 @@ class ExclusiveCanonicalization { * @return {String} * @api public */ - process(node, options) { + process(node, options: CanonicalizationOrTransformationAlgorithmProcessOptions) { options = options || {}; let inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; - if (!(inclusiveNamespacesPrefixList instanceof Array)) { - inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList.split(" "); - } - const ancestorNamespaces = options.ancestorNamespaces || []; /** * If the inclusiveNamespacesPrefixList has not been explicitly provided then look it up in CanonicalizationMethod/InclusiveNamespaces */ - if (inclusiveNamespacesPrefixList.length === 0) { + if (!utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { const CanonicalizationMethod = utils.findChilds(node, "CanonicalizationMethod"); if (CanonicalizationMethod.length !== 0) { const inclusiveNamespaces = utils.findChilds( @@ -273,9 +276,9 @@ class ExclusiveCanonicalization { "InclusiveNamespaces" ); if (inclusiveNamespaces.length !== 0) { - inclusiveNamespacesPrefixList = inclusiveNamespaces[0] - .getAttribute("PrefixList") - .split(" "); + inclusiveNamespacesPrefixList = ( + inclusiveNamespaces[0].getAttribute("PrefixList") || "" + ).split(" "); } } } @@ -283,12 +286,8 @@ class ExclusiveCanonicalization { /** * If you have a PrefixList then use it and the ancestors to add the necessary namespaces */ - if (inclusiveNamespacesPrefixList) { - const prefixList = - inclusiveNamespacesPrefixList instanceof Array - ? inclusiveNamespacesPrefixList - : inclusiveNamespacesPrefixList.split(" "); - prefixList.forEach(function (prefix) { + if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { + inclusiveNamespacesPrefixList.forEach(function (prefix) { if (ancestorNamespaces) { ancestorNamespaces.forEach(function (ancestorNamespace) { if (prefix === ancestorNamespace.prefix) { @@ -318,11 +317,7 @@ class ExclusiveCanonicalization { } } -/** - * Add c14n#WithComments here (very simple subclass) - * @type { import("../index.d.ts").CanonicalizationOrTransformationAlgorithm} - */ -class ExclusiveCanonicalizationWithComments extends ExclusiveCanonicalization { +export class ExclusiveCanonicalizationWithComments extends ExclusiveCanonicalization { constructor() { super(); this.includeComments = true; @@ -332,8 +327,3 @@ class ExclusiveCanonicalizationWithComments extends ExclusiveCanonicalization { return "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; } } - -module.exports = { - ExclusiveCanonicalization, - ExclusiveCanonicalizationWithComments, -}; diff --git a/src/hash-algorithms.ts b/src/hash-algorithms.ts index 0cf3619d..eeeb8b27 100644 --- a/src/hash-algorithms.ts +++ b/src/hash-algorithms.ts @@ -1,61 +1,41 @@ -const crypto = require("crypto"); +import * as crypto from "crypto"; +import type { HashAlgorithm } from "./types"; -/** - * @type { import("../index.d.ts").HashAlgorithm} - */ -class Sha1 { - constructor() { - this.getHash = function (xml) { - const shasum = crypto.createHash("sha1"); - shasum.update(xml, "utf8"); - const res = shasum.digest("base64"); - return res; - }; +export class Sha1 implements HashAlgorithm { + getHash = function (xml) { + const shasum = crypto.createHash("sha1"); + shasum.update(xml, "utf8"); + const res = shasum.digest("base64"); + return res; + }; - this.getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#sha1"; - }; - } + getAlgorithmName = function () { + return "http://www.w3.org/2000/09/xmldsig#sha1"; + }; } -/** - * @type { import("../index.d.ts").HashAlgorithm} - */ -class Sha256 { - constructor() { - this.getHash = function (xml) { - const shasum = crypto.createHash("sha256"); - shasum.update(xml, "utf8"); - const res = shasum.digest("base64"); - return res; - }; +export class Sha256 implements HashAlgorithm { + getHash = function (xml) { + const shasum = crypto.createHash("sha256"); + shasum.update(xml, "utf8"); + const res = shasum.digest("base64"); + return res; + }; - this.getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmlenc#sha256"; - }; - } + getAlgorithmName = function () { + return "http://www.w3.org/2001/04/xmlenc#sha256"; + }; } -/** - * @type { import("../index.d.ts").HashAlgorithm} - */ -class Sha512 { - constructor() { - this.getHash = function (xml) { - const shasum = crypto.createHash("sha512"); - shasum.update(xml, "utf8"); - const res = shasum.digest("base64"); - return res; - }; +export class Sha512 implements HashAlgorithm { + getHash = function (xml) { + const shasum = crypto.createHash("sha512"); + shasum.update(xml, "utf8"); + const res = shasum.digest("base64"); + return res; + }; - this.getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmlenc#sha512"; - }; - } + getAlgorithmName = function () { + return "http://www.w3.org/2001/04/xmlenc#sha512"; + }; } - -module.exports = { - Sha1, - Sha256, - Sha512, -}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 719120ff..3e4a8a4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,3 @@ -const select = require("xpath").select; -const utils = require("./lib/utils"); - -module.exports = require("./lib/signed-xml"); -module.exports.C14nCanonicalization = require("./lib/c14n-canonicalization").C14nCanonicalization; -module.exports.C14nCanonicalizationWithComments = - require("./lib/c14n-canonicalization").C14nCanonicalizationWithComments; -module.exports.ExclusiveCanonicalization = - require("./lib/exclusive-canonicalization").ExclusiveCanonicalization; -module.exports.ExclusiveCanonicalizationWithComments = - require("./lib/exclusive-canonicalization").ExclusiveCanonicalizationWithComments; -module.exports.xpath = function (node, xpath) { - return select(xpath, node); -}; -module.exports.utils = utils; +export { SignedXml } from "./signed-xml"; +export * from "./utils"; +export * from "./types"; diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index 2c1a66f8..46abe0eb 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -1,134 +1,106 @@ -const crypto = require("crypto"); - -/** - * @type { import("../index.d.ts").SignatureAlgorithm} - */ -class RsaSha1 { - constructor() { - /** - * Sign the given string using the given key - * - */ - this.getSignature = function (signedInfo, privateKey, callback) { +import * as crypto from "crypto"; +import { type SignatureAlgorithm, createOptionalCallbackFunction } from "./types"; + +export class RsaSha1 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { const signer = crypto.createSign("RSA-SHA1"); signer.update(signedInfo); const res = signer.sign(privateKey, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - /** - * Verify the given signature of the given string using key - * - */ - this.verifySignature = function (str, key, signatureValue, callback) { + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { const verifier = crypto.createVerify("RSA-SHA1"); - verifier.update(str); + verifier.update(material); const res = verifier.verify(key, signatureValue, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - this.getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - }; - } + getAlgorithmName = () => { + return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + }; } -/** - * @type { import("../index.d.ts").SignatureAlgorithm} SignatureAlgorithm - */ -class RsaSha256 { - constructor() { - this.getSignature = function (signedInfo, privateKey, callback) { +export class RsaSha256 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { const signer = crypto.createSign("RSA-SHA256"); signer.update(signedInfo); const res = signer.sign(privateKey, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - this.verifySignature = function (str, key, signatureValue, callback) { + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { const verifier = crypto.createVerify("RSA-SHA256"); - verifier.update(str); + verifier.update(material); const res = verifier.verify(key, signatureValue, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - this.getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; - }; - } + getAlgorithmName = () => { + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + }; } -/** - * @type { import("../index.d.ts").SignatureAlgorithm} - */ -class RsaSha512 { - constructor() { - this.getSignature = function (signedInfo, privateKey, callback) { +export class RsaSha512 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { const signer = crypto.createSign("RSA-SHA512"); signer.update(signedInfo); const res = signer.sign(privateKey, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - this.verifySignature = function (str, key, signatureValue, callback) { + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { const verifier = crypto.createVerify("RSA-SHA512"); - verifier.update(str); + verifier.update(material); const res = verifier.verify(key, signatureValue, "base64"); - if (callback) { - callback(null, res); - } + return res; - }; + } + ); - this.getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; - }; - } + getAlgorithmName = () => { + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; + }; } -/** - * @type { import("../index.d.ts").SignatureAlgorithm} - */ -class HmacSha1 { - constructor() { - this.verifySignature = function (str, key, signatureValue) { +export class HmacSha1 implements SignatureAlgorithm { + getSignature = createOptionalCallbackFunction( + (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => { + const signer = crypto.createHmac("SHA1", privateKey); + signer.update(signedInfo); + const res = signer.digest("base64"); + + return res; + } + ); + + verifySignature = createOptionalCallbackFunction( + (material: string, key: crypto.KeyLike, signatureValue: string): boolean => { const verifier = crypto.createHmac("SHA1", key); - verifier.update(str); + verifier.update(material); const res = verifier.digest("base64"); - return res === signatureValue; - }; - this.getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; - }; + return res === signatureValue; + } + ); - this.getSignature = function (signedInfo, privateKey) { - const verifier = crypto.createHmac("SHA1", privateKey); - verifier.update(signedInfo); - const res = verifier.digest("base64"); - return res; - }; - } + getAlgorithmName = () => { + return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; + }; } - -module.exports = { - RsaSha1, - RsaSha256, - RsaSha512, - HmacSha1, -}; diff --git a/src/signed-xml.ts b/src/signed-xml.ts index da2949c6..072201ff 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1,18 +1,122 @@ -const xpath = require("xpath"); -const Dom = require("@xmldom/xmldom").DOMParser; -const utils = require("./utils"); -const c14n = require("./c14n-canonicalization"); -const execC14n = require("./exclusive-canonicalization"); -const EnvelopedSignature = require("./enveloped-signature").EnvelopedSignature; -const hashAlgorithms = require("./hash-algorithms"); -const signatureAlgorithms = require("./signature-algorithms"); - -/** - * @type {import ("../index.d.ts").SignedXml} - */ -class SignedXml { - /** @param {import("../index.d.ts").SignedXmlOptions} [options={}] */ - constructor(options = {}) { +import type { + CanonicalizationAlgorithmType, + CanonicalizationOrTransformationAlgorithm, + ComputeSignatureOptions, + GetKeyInfoContentArgs, + HashAlgorithm, + HashAlgorithmType, + Reference, + SignatureAlgorithm, + SignatureAlgorithmType, + SignedXmlOptions, + CanonicalizationOrTransformAlgorithmType, + ErrorFirstCallback, + CanonicalizationOrTransformationAlgorithmProcessOptions, +} from "./types"; + +import * as xpath from "xpath"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import * as utils from "./utils"; +import * as c14n from "./c14n-canonicalization"; +import * as execC14n from "./exclusive-canonicalization"; +import * as envelopedSignatures from "./enveloped-signature"; +import * as hashAlgorithms from "./hash-algorithms"; +import * as signatureAlgorithms from "./signature-algorithms"; +import * as crypto from "crypto"; + +export class SignedXml { + idMode?: "wssecurity"; + idAttributes: string[]; + /** + * A {@link Buffer} or pem encoded {@link String} containing your private key + */ + privateKey?: crypto.KeyLike; + publicCert?: crypto.KeyLike; + /** + * One of the supported signature algorithms. See {@link SignatureAlgorithmType} + */ + signatureAlgorithm: SignatureAlgorithmType = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + /** + * Rules used to convert an XML document into its canonical form. + */ + canonicalizationAlgorithm: CanonicalizationAlgorithmType = + "http://www.w3.org/2001/10/xml-exc-c14n#"; + /** + * It specifies a list of namespace prefixes that should be considered "inclusive" during the canonicalization process. + */ + inclusiveNamespacesPrefixList: string[] = []; + namespaceResolver: XPathNSResolver = { + lookupNamespaceURI: function (prefix) { + throw new Error("Not implemented"); + }, + }; + implicitTransforms: ReadonlyArray = []; + keyInfoAttributes: { [attrName: string]: string } = {}; + getKeyInfoContent = SignedXml.getKeyInfoContent; + 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 = ""; + /** + * Contains validation errors (if any) after {@link checkSignature} method is called + */ + private 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} + */ + CanonicalizationAlgorithms: Record< + CanonicalizationOrTransformAlgorithmType, + new () => CanonicalizationOrTransformationAlgorithm + > = { + "http://www.w3.org/TR/2001/REC-xml-c14n-20010315": c14n.C14nCanonicalization, + "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments": + c14n.C14nCanonicalizationWithComments, + "http://www.w3.org/2001/10/xml-exc-c14n#": execC14n.ExclusiveCanonicalization, + "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": + execC14n.ExclusiveCanonicalizationWithComments, + "http://www.w3.org/2000/09/xmldsig#enveloped-signature": envelopedSignatures.EnvelopedSignature, + }; + + /** + * To add a new hash algorithm create a new class that implements the {@link HashAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + */ + HashAlgorithms: Record HashAlgorithm> = { + "http://www.w3.org/2000/09/xmldsig#sha1": hashAlgorithms.Sha1, + "http://www.w3.org/2001/04/xmlenc#sha256": hashAlgorithms.Sha256, + "http://www.w3.org/2001/04/xmlenc#sha512": hashAlgorithms.Sha512, + }; + + /** + * To add a new signature algorithm create a new class that implements the {@link SignatureAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + */ + 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#rsa-sha512": signatureAlgorithms.RsaSha512, + // Disabled by default due to key confusion concerns. + // 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': SignatureAlgorithms.HmacSha1 + }; + + static defaultNsForPrefix = { + ds: "http://www.w3.org/2000/09/xmldsig#", + }; + + /** + * The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using + * @param options {@link SignedXmlOptions} + */ + constructor(options: SignedXmlOptions = {}) { const { idMode, idAttribute, @@ -28,71 +132,50 @@ class SignedXml { } = options; // Options - this.idMode = idMode || null; + this.idMode = idMode; this.idAttributes = ["Id", "ID", "id"]; if (idAttribute) { this.idAttributes.unshift(idAttribute); } - this.privateKey = privateKey || null; - this.publicCert = publicCert || null; - this.signatureAlgorithm = signatureAlgorithm || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - this.canonicalizationAlgorithm = - canonicalizationAlgorithm || "http://www.w3.org/2001/10/xml-exc-c14n#"; - this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList || ""; - this.implicitTransforms = implicitTransforms || []; - this.keyInfoAttributes = keyInfoAttributes || {}; - this.getKeyInfoContent = getKeyInfoContent || SignedXml.getKeyInfoContent; - this.getCertFromKeyInfo = getCertFromKeyInfo || SignedXml.getCertFromKeyInfo; - - // Internal state - this.references = []; - this.id = 0; - this.signedXml = ""; - this.signatureXml = ""; - this.signatureNode = null; - this.signatureValue = ""; - this.originalXmlWithIds = ""; - this.validationErrors = []; - this.keyInfo = null; - - this.CanonicalizationAlgorithms = { - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315": c14n.C14nCanonicalization, - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments": - c14n.C14nCanonicalizationWithComments, - "http://www.w3.org/2001/10/xml-exc-c14n#": execC14n.ExclusiveCanonicalization, - "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": - execC14n.ExclusiveCanonicalizationWithComments, - "http://www.w3.org/2000/09/xmldsig#enveloped-signature": EnvelopedSignature, - }; - - this.HashAlgorithms = { - "http://www.w3.org/2000/09/xmldsig#sha1": hashAlgorithms.Sha1, - "http://www.w3.org/2001/04/xmlenc#sha256": hashAlgorithms.Sha256, - "http://www.w3.org/2001/04/xmlenc#sha512": hashAlgorithms.Sha512, - }; - - this.SignatureAlgorithms = { - "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#rsa-sha512": signatureAlgorithms.RsaSha512, - // Disabled by default due to key confusion concerns. - // 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': SignatureAlgorithms.HmacSha1 - }; + this.privateKey = privateKey; + this.publicCert = publicCert; + this.signatureAlgorithm = signatureAlgorithm ?? this.signatureAlgorithm; + this.canonicalizationAlgorithm = canonicalizationAlgorithm ?? this.canonicalizationAlgorithm; + if (typeof inclusiveNamespacesPrefixList === "string") { + this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList.split(" "); + } else if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { + this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList; + } + this.implicitTransforms = implicitTransforms ?? this.implicitTransforms; + this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes; + this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent; + this.getCertFromKeyInfo = getCertFromKeyInfo ?? this.getCertFromKeyInfo; + this.CanonicalizationAlgorithms; + this.HashAlgorithms; + this.SignatureAlgorithms; } /** * Due to key-confusion issues, it's risky to have both hmac - * and digital signature algos enabled at the same time. - * This enables HMAC and disables other signing algos. + * and digital signature algorithms enabled at the same time. + * This enables HMAC and disables other signing algorithms. */ - enableHMAC() { + enableHMAC(): void { this.SignatureAlgorithms = { "http://www.w3.org/2000/09/xmldsig#hmac-sha1": signatureAlgorithms.HmacSha1, }; this.getKeyInfoContent = () => null; } - static getKeyInfoContent({ publicCert = null, prefix = null } = {}) { + /** + * Builds the contents of a KeyInfo element as an XML string. + * + * For example, if the value of the prefix argument is 'foo', then + * the resultant XML string will be "" + * + * @return an XML string representation of the contents of a KeyInfo element, or `null` if no `KeyInfo` element should be included + */ + static getKeyInfoContent({ publicCert, prefix }: GetKeyInfoContentArgs): string | null { if (publicCert == null) { return null; } @@ -104,12 +187,13 @@ class SignedXml { publicCert = publicCert.toString("latin1"); } + let publicCertMatches: string[] = []; if (typeof publicCert === "string") { - publicCert = publicCert.match(utils.EXTRACT_X509_CERTS); + publicCertMatches = publicCert.match(utils.EXTRACT_X509_CERTS) || []; } - if (Array.isArray(publicCert)) { - x509Certs = publicCert + if (publicCertMatches.length > 0) { + x509Certs = publicCertMatches .map((c) => `${utils.pemToDer(c)}`) .join(""); } @@ -117,18 +201,44 @@ class SignedXml { return `<${prefix}X509Data>${x509Certs}`; } - static getCertFromKeyInfo(keyInfo) { - if (keyInfo != null && keyInfo.length > 0) { - const certs = xpath.select(".//*[local-name(.)='X509Certificate']", keyInfo[0]); - if (certs.length > 0) { - return utils.derToPem(certs[0].textContent.trim(), "CERTIFICATE"); + /** + * 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) + * @return the signing certificate as a string in PEM format + */ + static getCertFromKeyInfo(keyInfo?: Node | null): string | null { + if (keyInfo != null) { + const certs = xpath.select1(".//*[local-name(.)='X509Certificate']", keyInfo); + if (xpath.isNodeLike(certs)) { + return utils.derToPem(certs.textContent || "", "CERTIFICATE"); } } return null; } - checkSignature(xml, callback) { + /** + * Validates the signature of the provided XML document synchronously using the configured key info provider. + * + * @param xml The XML document containing the signature to be validated. + * @returns `true` if the signature is valid + * @throws Error if no key info resolver is provided. + */ + checkSignature(xml: string): boolean; + /** + * Validates the signature of the provided XML document synchronously using the configured key info provider. + * + * @param xml The XML document containing the signature to be validated. + * @param callback Callback function to handle the validation result asynchronously. + * @throws Error if the last parameter is provided and is not a function, or if no key info resolver is provided. + */ + checkSignature(xml: string, callback: (error: Error | null, isValid?: boolean) => void): void; + checkSignature( + xml: string, + callback?: (error: Error | null, isValid?: boolean) => void + ): unknown { if (callback != null && typeof callback !== "function") { throw new Error("Last parameter must be a callback function"); } @@ -155,7 +265,7 @@ class SignedXml { return true; } else { // Asynchronous flow - this.validateSignatureValue(doc, function (err, isValidSignature) { + this.validateSignatureValue(doc, (err: Error | null, isValidSignature?: boolean) => { if (err) { this.validationErrors.push( "invalid signature: the signature value " + this.signatureValue + " is incorrect" @@ -168,7 +278,11 @@ class SignedXml { } } - getCanonSignedInfoXml(doc) { + getCanonSignedInfoXml(doc: Document) { + if (this.signatureNode == null) { + throw new Error("No signature found."); + } + const signedInfo = utils.findChilds(this.signatureNode, "SignedInfo"); if (signedInfo.length === 0) { throw new Error("could not find SignedInfo element in the message"); @@ -189,8 +303,7 @@ class SignedXml { /** * Search for ancestor namespaces before canonicalization. */ - let ancestorNamespaces = []; - ancestorNamespaces = utils.findAncestorNs(doc, "//*[local-name()='SignedInfo']"); + const ancestorNamespaces = utils.findAncestorNs(doc, "//*[local-name()='SignedInfo']"); const c14nOptions = { ancestorNamespaces: ancestorNamespaces, @@ -214,30 +327,42 @@ class SignedXml { return this.getCanonXml(ref.transforms, node, c14nOptions); } - validateSignatureValue(doc, callback) { + validateSignatureValue(doc: Document): boolean; + validateSignatureValue(doc: Document, callback: ErrorFirstCallback): void; + validateSignatureValue(doc: Document, callback?: ErrorFirstCallback): boolean | void { const signedInfoCanon = this.getCanonSignedInfoXml(doc); const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); - const res = signer.verifySignature( - signedInfoCanon, - this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey, - this.signatureValue, - callback - ); - if (!res && !callback) { - this.validationErrors.push( - "invalid signature: the signature value " + this.signatureValue + " is incorrect" - ); + const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey; + if (key == null) { + throw new Error("KeyInfo or publicCert or privateKey is required to validate signature"); + } + if (typeof callback === "function") { + signer.verifySignature(signedInfoCanon, key, this.signatureValue, callback); + } else { + const res = signer.verifySignature(signedInfoCanon, key, this.signatureValue); + if (res === false) { + this.validationErrors.push( + "invalid signature: the signature value " + this.signatureValue + " is incorrect" + ); + } + return res; } - return res; } - calculateSignatureValue(doc, callback) { + calculateSignatureValue(doc: Document, callback?: ErrorFirstCallback) { const signedInfoCanon = this.getCanonSignedInfoXml(doc); const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); - this.signatureValue = signer.getSignature(signedInfoCanon, this.privateKey, callback); + if (this.privateKey == null) { + throw new Error("Private key is required to compute signature"); + } + if (typeof callback === "function") { + signer.getSignature(signedInfoCanon, this.privateKey, callback); + } else { + this.signatureValue = signer.getSignature(signedInfoCanon, this.privateKey); + } } - findSignatureAlgorithm(name) { + findSignatureAlgorithm(name: SignatureAlgorithmType) { const algo = this.SignatureAlgorithms[name]; if (algo) { return new algo(); @@ -246,7 +371,7 @@ class SignedXml { } } - findCanonicalizationAlgorithm(name) { + findCanonicalizationAlgorithm(name: CanonicalizationOrTransformAlgorithmType) { const algo = this.CanonicalizationAlgorithms[name]; if (algo) { return new algo(); @@ -255,7 +380,7 @@ class SignedXml { } } - findHashAlgorithm(name) { + findHashAlgorithm(name: HashAlgorithmType) { const algo = this.HashAlgorithms[name]; if (algo) { return new algo(); @@ -267,12 +392,12 @@ class SignedXml { validateReferences(doc) { for (const ref of this.references) { let elemXpath; - const uri = ref.uri[0] === "#" ? ref.uri.substring(1) : ref.uri; - let elem = []; + const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; + let elem: xpath.SelectReturnType = []; if (uri === "") { elem = xpath.select("//*", doc); - } else if (uri.indexOf("'") !== -1) { + } else if (uri?.indexOf("'") !== -1) { // xpath injection throw new Error("Cannot validate a uri with quotes inside it"); } else { @@ -280,8 +405,8 @@ class SignedXml { for (const attr of this.idAttributes) { const tmp_elemXpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; const tmp_elem = xpath.select(tmp_elemXpath, doc); - num_elements_for_id += tmp_elem.length; - if (tmp_elem.length > 0) { + if (utils.isArrayHasLength(tmp_elem)) { + num_elements_for_id += tmp_elem.length; elem = tmp_elem; elemXpath = tmp_elemXpath; } @@ -297,7 +422,8 @@ class SignedXml { ref.xpath = elemXpath; } - if (elem.length === 0) { + // Note, we are using the last found element from the loop above + if (!utils.isArrayHasLength(elem)) { this.validationErrors.push( "invalid signature: the signature references an element with uri " + ref.uri + @@ -327,7 +453,12 @@ class SignedXml { return true; } - loadSignature(signatureNode) { + /** + * Loads the signature information from the provided XML node or string. + * + * @param signatureNode The XML node or string representing the signature. + */ + loadSignature(signatureNode: Node | string): void { if (typeof signatureNode === "string") { this.signatureNode = signatureNode = new Dom().parseFromString(signatureNode); } else { @@ -340,22 +471,29 @@ class SignedXml { ".//*[local-name(.)='CanonicalizationMethod']/@Algorithm", signatureNode ); - if (nodes.length === 0) { + if (!utils.isArrayHasLength(nodes)) { throw new Error("could not find CanonicalizationMethod/@Algorithm element"); } - this.canonicalizationAlgorithm = nodes[0].value; - this.signatureAlgorithm = utils.findFirst( - signatureNode, - ".//*[local-name(.)='SignatureMethod']/@Algorithm" - ).value; + if (xpath.isAttribute(nodes[0])) { + this.canonicalizationAlgorithm = nodes[0].value as CanonicalizationAlgorithmType; + } + + const signatureAlgorithm = xpath.select1( + ".//*[local-name(.)='SignatureMethod']/@Algorithm", + signatureNode + ); + + if (xpath.isAttribute(signatureAlgorithm)) { + this.signatureAlgorithm = signatureAlgorithm.value as SignatureAlgorithmType; + } this.references = []; const references = xpath.select( ".//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference']", signatureNode ); - if (references.length === 0) { + if (!utils.isArrayHasLength(references)) { throw new Error("could not find any Reference elements"); } @@ -363,11 +501,21 @@ class SignedXml { this.loadReference(reference); } - this.signatureValue = utils - .findFirst(signatureNode, ".//*[local-name(.)='SignatureValue']/text()") - .data.replace(/\r?\n/g, ""); + const signatureValue = xpath.select1( + ".//*[local-name(.)='SignatureValue']/text()", + signatureNode + ); - this.keyInfo = xpath.select(".//*[local-name(.)='KeyInfo']", signatureNode); + if (xpath.isTextNode(signatureValue)) { + this.signatureValue = signatureValue.data.replace(/\r?\n/g, ""); + } + + const keyInfo = xpath.select1(".//*[local-name(.)='KeyInfo']", signatureNode); + + // TODO: should this just be a single return instead of an array that we always take the first entry of? + if (xpath.isNodeLike(keyInfo)) { + this.keyInfo = keyInfo; + } } /** @@ -391,135 +539,177 @@ class SignedXml { if (nodes.length === 0) { throw new Error("could not find DigestValue node in reference " + ref.toString()); } - if (nodes[0].childNodes.length === 0 || !nodes[0].firstChild.data) { + const firstChild = nodes[0].firstChild; + if (!firstChild || !("data" in firstChild)) { throw new Error("could not find the value of DigestValue in " + nodes[0].toString()); } - const digestValue = nodes[0].firstChild.data; + const digestValue = firstChild.data; - const transforms = []; - let trans; - let inclusiveNamespacesPrefixList; + const transforms: string[] = []; + let inclusiveNamespacesPrefixList: string[] = []; nodes = utils.findChilds(ref, "Transforms"); if (nodes.length !== 0) { const transformsNode = nodes[0]; const transformsAll = utils.findChilds(transformsNode, "Transform"); - for (const t of transformsAll) { - trans = t; - transforms.push(utils.findAttr(trans, "Algorithm").value); + for (const transform of transformsAll) { + const transformAttr = utils.findAttr(transform, "Algorithm"); + + if (transformAttr) { + transforms.push(transformAttr.value); + } } // This is a little strange, we are looking for children of the last child of `transformsNode` - const inclusiveNamespaces = utils.findChilds(trans, "InclusiveNamespaces"); - if (inclusiveNamespaces.length > 0) { - //Should really only be one prefix list, but maybe there's some circumstances where more than one to lets handle it - for (let i = 0; i < inclusiveNamespaces.length; i++) { - if (inclusiveNamespacesPrefixList) { - inclusiveNamespacesPrefixList = - inclusiveNamespacesPrefixList + - " " + - inclusiveNamespaces[i].getAttribute("PrefixList"); - } else { - inclusiveNamespacesPrefixList = inclusiveNamespaces[i].getAttribute("PrefixList"); - } - } + const inclusiveNamespaces = utils.findChilds( + transformsAll[transformsAll.length - 1], + "InclusiveNamespaces" + ); + if (utils.isArrayHasLength(inclusiveNamespaces)) { + // Should really only be one prefix list, but maybe there's some circumstances where more than one to let's handle it + inclusiveNamespacesPrefixList = inclusiveNamespaces + .flatMap((namespace) => (namespace.getAttribute("PrefixList") ?? "").split(" ")) + .filter((value) => value.length > 0); } - } - const hasImplicitTransforms = - Array.isArray(this.implicitTransforms) && this.implicitTransforms.length > 0; - if (hasImplicitTransforms) { - this.implicitTransforms.forEach(function (t) { - transforms.push(t); - }); - } + if (utils.isArrayHasLength(this.implicitTransforms)) { + this.implicitTransforms.forEach(function (t) { + transforms.push(t); + }); + } - /** - * 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: - * 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 - */ - if ( - transforms.length === 0 || - transforms[transforms.length - 1] === "http://www.w3.org/2000/09/xmldsig#enveloped-signature" - ) { - transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315"); - } + /** + * 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: + * 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 + */ + if ( + transforms.length === 0 || + transforms[transforms.length - 1] === + "http://www.w3.org/2000/09/xmldsig#enveloped-signature" + ) { + transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315"); + } - this.addReference( - null, - transforms, - digestAlgo, - utils.findAttr(ref, "URI").value, - digestValue, - inclusiveNamespacesPrefixList, - false - ); + this.addReference({ + transforms, + digestAlgorithm: digestAlgo, + uri: utils.findAttr(ref, "URI")?.value, + digestValue, + inclusiveNamespacesPrefixList, + isEmptyUri: false, + }); + } } - addReference( + /** + * Adds a reference to the signature. + * + * @param xpath The XPath expression to select the XML nodes to be referenced. + * @param transforms An array of transform algorithms to be applied to the selected nodes. Defaults to ["http://www.w3.org/2001/10/xml-exc-c14n#"]. + * @param digestAlgorithm The digest algorithm to use for computing the digest value. Defaults to "http://www.w3.org/2000/09/xmldsig#sha1". + * @param uri The URI identifier for the reference. If empty, an empty URI will be used. + * @param digestValue The expected digest value for the reference. + * @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization. + * @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`. + */ + addReference({ xpath, - transforms, - digestAlgorithm, - uri, + transforms = ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm = "http://www.w3.org/2000/09/xmldsig#sha1", + uri = "", digestValue, - inclusiveNamespacesPrefixList, - isEmptyUri - ) { + inclusiveNamespacesPrefixList = [], + isEmptyUri = false, + }: Partial & Pick): void { this.references.push({ - xpath: xpath, - transforms: transforms ? transforms : ["http://www.w3.org/2001/10/xml-exc-c14n#"], - digestAlgorithm: digestAlgorithm ? digestAlgorithm : "http://www.w3.org/2000/09/xmldsig#sha1", - uri: uri, - digestValue: digestValue, - inclusiveNamespacesPrefixList: inclusiveNamespacesPrefixList, - isEmptyUri: isEmptyUri, + xpath, + transforms, + digestAlgorithm, + uri, + digestValue, + inclusiveNamespacesPrefixList, + isEmptyUri, }); } /** - * Compute the signature of the given xml (using the already defined settings) + * Compute the signature of the given XML (using the already defined settings). * - * Options: + * @param xml The XML to compute the signature for. + * @param callback A callback function to handle the signature computation asynchronously. + * @returns void + * @throws TypeError If the xml can not be parsed. + */ + computeSignature(xml: string): void; + + /** + * Compute the signature of the given XML (using the already defined settings). * - * - `prefix` {String} Adds a prefix for the generated signature tags - * - `attrs` {Object} A hash of attributes and values `attrName: value` to add to the signature root node - * - `location` {{ reference: String, action: String }} - * - `existingPrefixes` {Object} A hash of prefixes and namespaces `prefix: namespace` already in the xml - * An object with a `reference` key which should - * contain a XPath expression, an `action` key which - * should contain one of the following values: - * `append`, `prepend`, `before`, `after` + * @param xml The XML to compute the signature for. + * @param callback A callback function to handle the signature computation asynchronously. + * @returns void + * @throws TypeError If the xml can not be parsed. + */ + computeSignature(xml: string, callback: ErrorFirstCallback): void; + + /** + * Compute the signature of the given XML (using the already defined settings). * + * @param xml The XML to compute the signature for. + * @param opts An object containing options for the signature computation. + * @returns If no callback is provided, returns `this` (the instance of SignedXml). + * @throws TypeError If the xml can not be parsed, or Error if there were invalid options passed. */ - computeSignature(xml, opts, callback) { - if (typeof opts === "function" && callback == null) { - callback = opts; - } + computeSignature(xml: string, options: ErrorFirstCallback): void; - if (callback != null && typeof callback !== "function") { - throw new Error("Last parameter must be a callback function"); + /** + * Compute the signature of the given XML (using the already defined settings). + * + * @param xml The XML to compute the signature for. + * @param opts An object containing options for the signature computation. + * @param callback A callback function to handle the signature computation asynchronously. + * @returns void + * @throws TypeError If the xml can not be parsed, or Error if there were invalid options passed. + */ + computeSignature( + xml: string, + options: ComputeSignatureOptions, + callback: ErrorFirstCallback + ): void; + + computeSignature( + xml: string, + options?: ComputeSignatureOptions | ErrorFirstCallback, + callbackParam?: ErrorFirstCallback + ): void { + let callback: ErrorFirstCallback; + if (typeof options === "function" && callbackParam == null) { + callback = options as ErrorFirstCallback; + options = {} as ComputeSignatureOptions; + } else { + callback = callbackParam as ErrorFirstCallback; + options = (options ?? {}) as ComputeSignatureOptions; } const doc = new Dom().parseFromString(xml); let xmlNsAttr = "xmlns"; - const signatureAttrs = []; - let currentPrefix; + const signatureAttrs: string[] = []; + let currentPrefix: string; const validActions = ["append", "prepend", "before", "after"]; - opts = opts || {}; - const prefix = opts.prefix; - const attrs = opts.attrs || {}; - const location = opts.location || {}; - const existingPrefixes = opts.existingPrefixes || {}; + const prefix = options.prefix; + const attrs = options.attrs || {}; + const location = options.location || {}; + const existingPrefixes = options.existingPrefixes || {}; this.namespaceResolver = { lookupNamespaceURI: function (prefix) { - return existingPrefixes[prefix]; + return prefix ? existingPrefixes[prefix] : null; }, }; @@ -538,7 +728,7 @@ class SignedXml { if (!callback) { throw err; } else { - callback(err, null); + callback(err); return; } } @@ -574,41 +764,52 @@ class SignedXml { }); // A trick to remove the namespaces that already exist in the xml - // This only works if the prefix and namespace match with those in te xml + // This only works if the prefix and namespace match with those in the xml const dummySignatureWrapper = "" + signatureXml + ""; const nodeXml = new Dom().parseFromString(dummySignatureWrapper); - const signatureDoc = nodeXml.documentElement.firstChild; - let referenceNode = xpath.select(location.reference, doc); + // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const signatureDoc = nodeXml.documentElement.firstChild!; - if (!referenceNode || referenceNode.length === 0) { + const referenceNode = xpath.select1(location.reference, doc); + + if (!xpath.isNodeLike(referenceNode)) { const err2 = new Error( "the following xpath cannot be used because it was not found: " + location.reference ); if (!callback) { throw err2; } else { - callback(err2, null); + callback(err2); return; } } - referenceNode = referenceNode[0]; - if (location.action === "append") { referenceNode.appendChild(signatureDoc); } else if (location.action === "prepend") { referenceNode.insertBefore(signatureDoc, referenceNode.firstChild); } else if (location.action === "before") { + if (referenceNode.parentNode == null) { + throw new Error( + "`location.reference` refers to the root node (by default), so we can't insert `before`" + ); + } referenceNode.parentNode.insertBefore(signatureDoc, referenceNode); } else if (location.action === "after") { + if (referenceNode.parentNode == null) { + throw new Error( + "`location.reference` refers to the root node (by default), so we can't insert `after`" + ); + } referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling); } this.signatureNode = signatureDoc; - let signedInfoNode = utils.findChilds(this.signatureNode, "SignedInfo"); - if (signedInfoNode.length === 0) { + const signedInfoNodes = utils.findChilds(this.signatureNode, "SignedInfo"); + if (signedInfoNodes.length === 0) { const err3 = new Error("could not find SignedInfo element in the message"); if (!callback) { throw err3; @@ -617,28 +818,28 @@ class SignedXml { return; } } - signedInfoNode = signedInfoNode[0]; + const signedInfoNode = signedInfoNodes[0]; - if (!callback) { - //Synchronous flow - this.calculateSignatureValue(doc); - signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); - this.signatureXml = signatureDoc.toString(); - this.signedXml = doc.toString(); - } else { + if (typeof callback === "function") { const self = this; //Asynchronous flow this.calculateSignatureValue(doc, function (err, signature) { if (err) { callback(err); } else { - self.signatureValue = signature; + self.signatureValue = signature || ""; signatureDoc.insertBefore(self.createSignature(prefix), signedInfoNode.nextSibling); self.signatureXml = signatureDoc.toString(); self.signedXml = doc.toString(); callback(null, self); } }); + } else { + //Synchronous flow + this.calculateSignatureValue(doc); + signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); + this.signatureXml = signatureDoc.toString(); + this.signedXml = doc.toString(); } } @@ -677,9 +878,9 @@ class SignedXml { prefix = prefix ? prefix + ":" : prefix; for (const ref of this.references) { - const nodes = xpath.selectWithResolver(ref.xpath, doc, this.namespaceResolver); + const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); - if (nodes.length === 0) { + if (!utils.isArrayHasLength(nodes)) { throw new Error( "the following xpath cannot be signed because it was not found: " + ref.xpath ); @@ -694,14 +895,14 @@ class SignedXml { res += "<" + prefix + 'Reference URI="#' + id + '">'; } res += "<" + prefix + "Transforms>"; - for (const trans of ref.transforms) { + for (const trans of ref.transforms || []) { const transform = this.findCanonicalizationAlgorithm(trans); res += "<" + prefix + 'Transform Algorithm="' + transform.getAlgorithmName() + '"'; - if (ref.inclusiveNamespacesPrefixList) { + if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { res += ">"; res += ''; @@ -739,14 +940,18 @@ class SignedXml { return res; } - getCanonXml(transforms, node, options) { + getCanonXml( + transforms: CanonicalizationAlgorithmType[], + node, + options: CanonicalizationOrTransformationAlgorithmProcessOptions + ) { options = options || {}; - options.defaultNsForPrefix = options.defaultNsForPrefix || SignedXml.defaultNsForPrefix; + options.defaultNsForPrefix = options.defaultNsForPrefix ?? SignedXml.defaultNsForPrefix; options.signatureNode = this.signatureNode; let canonXml = node.cloneNode(true); // Deep clone - Object.values(transforms).forEach((transformName) => { + transforms.forEach((transformName) => { const transform = this.findCanonicalizationAlgorithm(transformName); canonXml = transform.process(canonXml, options); //TODO: currently transform.process may return either Node or String value (enveloped transformation returns Node, exclusive-canonicalization returns String). @@ -775,8 +980,8 @@ class SignedXml { "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" ); } else { - Object.values(this.idAttributes).some((idAttribute) => { - attr = utils.findAttr(node, idAttribute, null); + this.idAttributes.some((idAttribute) => { + attr = utils.findAttr(node, idAttribute); return !!attr; // This will break the loop as soon as a truthy attr is found. }); } @@ -825,11 +1030,11 @@ class SignedXml { 'CanonicalizationMethod Algorithm="' + transform.getAlgorithmName() + '"'; - if (this.inclusiveNamespacesPrefixList) { + if (utils.isArrayHasLength(this.inclusiveNamespacesPrefixList)) { res += ">"; res += ''; @@ -848,7 +1053,7 @@ class SignedXml { * Create the Signature element * */ - createSignature(prefix) { + createSignature(prefix?: string) { let xmlNsAttr = "xmlns"; if (prefix) { @@ -874,28 +1079,36 @@ class SignedXml { "Signature>"; const doc = new Dom().parseFromString(dummySignatureWrapper); - return doc.documentElement.firstChild; + + // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return doc.documentElement.firstChild!; } - getSignatureXml() { + /** + * Returns just the signature part, must be called only after {@link computeSignature} + * + * @returns The signature XML. + */ + getSignatureXml(): string { return this.signatureXml; } - getOriginalXmlWithIds() { + /** + * Returns the original xml with Id attributes added on relevant elements (required for validation), must be called only after {@link computeSignature} + * + * @returns The original XML with IDs. + */ + getOriginalXmlWithIds(): string { return this.originalXmlWithIds; } - getSignedXml() { + /** + * Returns the original xml document with the signature in it, must be called only after {@link computeSignature} + * + * @returns The signed XML. + */ + getSignedXml(): string { return this.signedXml; } } - -SignedXml.defaultNsForPrefix = { - ds: "http://www.w3.org/2000/09/xmldsig#", -}; - -exports.SignedXml = SignedXml; - -module.exports = { - SignedXml, -}; diff --git a/src/utils.ts b/src/utils.ts index df1705d7..619579b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,23 +1,28 @@ -const xpath = require("xpath"); +import * as xpath from "xpath"; +import type { NamespacePrefix } from "./types"; -function attrEqualsExplicitly(attr, localName, namespace) { - return attr.localName === localName && (attr.namespaceURI === namespace || !namespace); +export function isArrayHasLength(array: unknown): array is unknown[] { + return Array.isArray(array) && array.length > 0; } -function attrEqualsImplicitly(attr, localName, namespace, node) { +function attrEqualsExplicitly(attr: Attr, localName: string, namespace?: string) { + return attr.localName === localName && (attr.namespaceURI === namespace || namespace == null); +} + +function attrEqualsImplicitly(attr: Attr, localName: string, namespace?: string, node?: Element) { return ( attr.localName === localName && - ((!attr.namespaceURI && node.namespaceURI === namespace) || !namespace) + ((!attr.namespaceURI && node?.namespaceURI === namespace) || namespace == null) ); } -function findAttr(node, localName, namespace) { - for (let i = 0; i < node.attributes.length; i++) { - const attr = node.attributes[i]; +export function findAttr(element: Element, localName: string, namespace?: string) { + for (let i = 0; i < element.attributes.length; i++) { + const attr = element.attributes[i]; if ( attrEqualsExplicitly(attr, localName, namespace) || - attrEqualsImplicitly(attr, localName, namespace, node) + attrEqualsImplicitly(attr, localName, namespace, element) ) { return attr; } @@ -25,20 +30,16 @@ function findAttr(node, localName, namespace) { return null; } -function findFirst(doc, path) { - const nodes = xpath.select(path, doc); - if (nodes.length === 0) { - throw "could not find xpath " + path; - } - return nodes[0]; -} - -function findChilds(node, localName, namespace) { - node = node.documentElement || node; - const res = []; - for (let i = 0; i < node.childNodes.length; i++) { - const child = node.childNodes[i]; - if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) { +export function findChilds(node: Node | Document, localName: string, namespace?: string) { + const element = (node as Document).documentElement ?? node; + const res: Element[] = []; + for (let i = 0; i < element.childNodes.length; i++) { + const child = element.childNodes[i]; + if ( + xpath.isElement(child) && + child.localName === localName && + (child.namespaceURI === namespace || namespace == null) + ) { res.push(child); } } @@ -61,7 +62,7 @@ const xml_special_to_encoded_text = { "\r": " ", }; -function encodeSpecialCharactersInAttribute(attributeValue) { +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) @@ -70,7 +71,7 @@ function encodeSpecialCharactersInAttribute(attributeValue) { }); } -function encodeSpecialCharactersInText(text) { +export function encodeSpecialCharactersInText(text) { return text.replace(/([&<>\r])/g, function (str, item) { // Special character normalization. See: // - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes) @@ -79,20 +80,52 @@ function encodeSpecialCharactersInText(text) { }); } -const EXTRACT_X509_CERTS = new RegExp( - "-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----", - "g" -); -const PEM_FORMAT_REGEX = new RegExp( +/** + * PEM format has wide range of usages, but this library + * is enforcing RFC7468 which focuses on PKIX, PKCS and CMS. + * + * https://www.rfc-editor.org/rfc/rfc7468 + * + * PEM_FORMAT_REGEX is validating given PEM file against RFC7468 'stricttextualmsg' definition. + * + * With few exceptions; + * - 'posteb' MAY have 'eol', but it is not mandatory. + * - 'preeb' and 'posteb' lines are limited to 64 characters, but + * should not cause any issues in context of PKIX, PKCS and CMS. + */ +export const PEM_FORMAT_REGEX = new RegExp( "^-----BEGIN [A-Z\x20]{1,48}-----([^-]*)-----END [A-Z\x20]{1,48}-----$", "s" ); -const BASE64_REGEX = new RegExp( +export const EXTRACT_X509_CERTS = new RegExp( + "-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----", + "g" +); +export const BASE64_REGEX = new RegExp( "^(?:[A-Za-z0-9\\+\\/]{4}\\n{0,1})*(?:[A-Za-z0-9\\+\\/]{2}==|[A-Za-z0-9\\+\\/]{3}=)?$", "s" ); -function normalizePem(pem) { +/** + * -----BEGIN [LABEL]----- + * base64([DATA]) + * -----END [LABEL]----- + * + * Above is shown what PEM file looks like. As can be seen, base64 data + * can be in single line or multiple lines. + * + * This function normalizes PEM presentation to; + * - contain PEM header and footer as they are given + * - normalize line endings to '\n' + * - normalize line length to maximum of 64 characters + * - ensure that 'preeb' has line ending '\n' + * + * With a couple of notes: + * - 'eol' is normalized to '\n' + * + * @param pem The PEM string to normalize to RFC7468 'stricttextualmsg' definition + */ +export function normalizePem(pem: string): string { return `${( pem .trim() @@ -101,14 +134,24 @@ function normalizePem(pem) { ).join("\n")}\n`; } -function pemToDer(pem) { +/** + * @param pem The PEM-encoded base64 certificate to strip headers from + */ +export function pemToDer(pem: string): string { return pem .replace(/(\r\n|\r)/g, "\n") .replace(/-----BEGIN [A-Z\x20]{1,48}-----\n?/, "") .replace(/-----END [A-Z\x20]{1,48}-----\n?/, ""); } -function derToPem(der, pemLabel) { +/** + * @param der The DER-encoded base64 certificate to add PEM headers too + * @param pemLabel The label of the header and footer to add + */ +export function derToPem( + der: string | Buffer, + pemLabel: "CERTIFICATE" | "PRIVATE KEY" | "RSA PUBLIC KEY" +): string { const base64Der = Buffer.isBuffer(der) ? der.toString("latin1").trim() : der.trim(); if (PEM_FORMAT_REGEX.test(base64Der)) { @@ -124,12 +167,15 @@ function derToPem(der, pemLabel) { throw new Error("Unknown DER format."); } -function collectAncestorNamespaces(node, nsArray) { - if (!nsArray) { - nsArray = []; +function collectAncestorNamespaces( + node: Element, + nsArray: NamespacePrefix[] = [] +): NamespacePrefix[] { + if (!xpath.isElement(node.parentNode)) { + return nsArray; } - const parent = node.parentNode; + const parent: Element = node.parentNode; if (!parent) { return nsArray; @@ -141,7 +187,7 @@ function collectAncestorNamespaces(node, nsArray) { if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:?/) !== -1) { nsArray.push({ prefix: attr.nodeName.replace(/^xmlns:?/, ""), - namespaceURI: attr.nodeValue, + namespaceURI: attr.nodeValue || "", }); } } @@ -161,6 +207,10 @@ function findNSPrefix(subset) { return subset.prefix || ""; } +function isElementSubset(docSubset: Node[]): docSubset is Element[] { + return docSubset.every((node) => xpath.isElement(node)); +} + /** * Extract ancestor namespaces in order to import it to root of document subset * which is being canonicalized for non-exclusive c14n. @@ -170,16 +220,24 @@ function findNSPrefix(subset) { * @param {object} namespaceResolver - xpath namespace resolver * @returns {Array} i.e. [{prefix: "saml", namespaceURI: "urn:oasis:names:tc:SAML:2.0:assertion"}] */ -function findAncestorNs(doc, docSubsetXpath, namespaceResolver) { +export function findAncestorNs( + doc: Node, + docSubsetXpath: string, + namespaceResolver?: XPathNSResolver +) { const docSubset = xpath.selectWithResolver(docSubsetXpath, doc, namespaceResolver); - if (!Array.isArray(docSubset) || docSubset.length < 1) { + if (!isArrayHasLength(docSubset)) { return []; } + if (!isElementSubset(docSubset)) { + throw new Error("Document subset must be list of elements"); + } + // Remove duplicate on ancestor namespace const ancestorNs = collectAncestorNamespaces(docSubset[0]); - const ancestorNsWithoutDuplicate = []; + const ancestorNsWithoutDuplicate: NamespacePrefix[] = []; for (let i = 0; i < ancestorNs.length; i++) { let notOnTheList = true; for (const v in ancestorNsWithoutDuplicate) { @@ -195,7 +253,7 @@ function findAncestorNs(doc, docSubsetXpath, namespaceResolver) { } // Remove namespaces which are already declared in the subset with the same prefix - const returningNs = []; + const returningNs: NamespacePrefix[] = []; const subsetNsPrefix = findNSPrefix(docSubset[0]); for (const ancestorNs of ancestorNsWithoutDuplicate) { if (ancestorNs.prefix !== subsetNsPrefix) { @@ -206,20 +264,9 @@ function findAncestorNs(doc, docSubsetXpath, namespaceResolver) { return returningNs; } -function validateDigestValue(digest, expectedDigest) { - let buffer; - let expectedBuffer; - - const majorVersion = /^v(\d+)/.exec(process.version)[1]; - - if (+majorVersion >= 6) { - buffer = Buffer.from(digest, "base64"); - expectedBuffer = Buffer.from(expectedDigest, "base64"); - } else { - // Compatibility with Node < 5.10.0 - buffer = new Buffer(digest, "base64"); - expectedBuffer = new Buffer(expectedDigest, "base64"); - } +export function validateDigestValue(digest, expectedDigest) { + const buffer = Buffer.from(digest, "base64"); + const expectedBuffer = Buffer.from(expectedDigest, "base64"); if (typeof buffer.equals === "function") { return buffer.equals(expectedBuffer); @@ -238,20 +285,3 @@ function validateDigestValue(digest, expectedDigest) { return true; } - -module.exports = { - findAttr, - findChilds, - encodeSpecialCharactersInAttribute, - encodeSpecialCharactersInText, - findFirst, - EXTRACT_X509_CERTS, - PEM_FORMAT_REGEX, - BASE64_REGEX, - pemToDer, - derToPem, - normalizePem, - collectAncestorNamespaces, - findAncestorNs, - validateDigestValue, -}; diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 1cbaaa47..d60e0278 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -1,17 +1,18 @@ -const expect = require("chai").expect; +import { expect } from "chai"; -const C14nCanonicalization = require("../lib/c14n-canonicalization").C14nCanonicalization; -const Dom = require("@xmldom/xmldom").DOMParser; -const select = require("xpath").select; -const utils = require("../lib/utils"); +import { C14nCanonicalization } from "../src/c14n-canonicalization"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import * as xpath from "xpath"; +import * as utils from "../src/utils"; -const test_C14nCanonicalization = function (xml, xpath, expected) { +const test_C14nCanonicalization = function (xml, xpathArg, expected) { const doc = new Dom().parseFromString(xml); - const elem = select(xpath, doc)[0]; + const elem = xpath.select1(xpathArg, doc); const can = new C14nCanonicalization(); const result = can + // @ts-expect-error FIXME .process(elem, { - ancestorNamespaces: utils.findAncestorNs(doc, xpath), + ancestorNamespaces: utils.findAncestorNs(doc, xpathArg), }) .toString(); diff --git a/test/c14nWithComments-unit-tests.spec.ts b/test/c14nWithComments-unit-tests.spec.ts index 14539bb4..3ea17a60 100644 --- a/test/c14nWithComments-unit-tests.spec.ts +++ b/test/c14nWithComments-unit-tests.spec.ts @@ -1,18 +1,15 @@ -const expect = require("chai").expect; +import { expect } from "chai"; -const c14nWithComments = - require("../lib/exclusive-canonicalization").ExclusiveCanonicalizationWithComments; -const Dom = require("@xmldom/xmldom").DOMParser; -const select = require("xpath").select; -const SignedXml = require("../lib/signed-xml.js").SignedXml; +import { ExclusiveCanonicalizationWithComments as c14nWithComments } from "../src/exclusive-canonicalization"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import * as xpath from "xpath"; +import { SignedXml } from "../src/index"; -const compare = function (xml, xpath, expected, inclusiveNamespacesPrefixList) { +const compare = function (xml, xpathArg, expected, inclusiveNamespacesPrefixList?: string[]) { const doc = new Dom().parseFromString(xml); - const elem = select(xpath, doc)[0]; + const elem = xpath.select1(xpathArg, doc); const can = new c14nWithComments(); - const result = can - .process(elem, { inclusiveNamespacesPrefixList: inclusiveNamespacesPrefixList }) - .toString(); + const result = can.process(elem, { inclusiveNamespacesPrefixList }).toString(); expect(result).to.equal(expected); }; @@ -71,7 +68,7 @@ describe("Exclusive canonicalization with comments", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); @@ -80,7 +77,7 @@ describe("Exclusive canonicalization with comments", function () { '123456', "//*[local-name(.)='child']", '123456', - "inclusive inclusive2" + ["inclusive", "inclusive2"] ); }); @@ -89,7 +86,7 @@ describe("Exclusive canonicalization with comments", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); @@ -98,7 +95,7 @@ describe("Exclusive canonicalization with comments", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); @@ -352,8 +349,9 @@ describe("Exclusive canonicalization with comments", function () { it("Multiple Canonicalization with namespace definition outside of signed element", function () { //var doc = new Dom().parseFromString("") const doc = new Dom().parseFromString(''); - const node = select("//*[local-name(.)='y']", doc)[0]; + const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); + // @ts-expect-error FIXME const res = sig.getCanonXml( [ "http://www.w3.org/2000/09/xmldsig#enveloped-signature", @@ -371,9 +369,10 @@ describe("Exclusive canonicalization with comments", function () { const xml = ''; const doc = new Dom().parseFromString(xml); - const node = select("//*[local-name(.)='y']", doc)[0]; + const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); const transforms = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]; + // @ts-expect-error FIXME const res = sig.getCanonXml(transforms, node); expect(res).to.equal(""); }); diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index 0a2199ba..08d6c5d7 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -1,19 +1,24 @@ -const expect = require("chai").expect; - -const ExclusiveCanonicalization = - require("../lib/exclusive-canonicalization").ExclusiveCanonicalization; -const Dom = require("@xmldom/xmldom").DOMParser; -const select = require("xpath").select; -const SignedXml = require("../lib/signed-xml.js").SignedXml; - -const compare = function (xml, xpath, expected, inclusiveNamespacesPrefixList, defaultNsForPrefix) { +import { expect } from "chai"; + +import { ExclusiveCanonicalization } from "../src/exclusive-canonicalization"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import * as xpath from "xpath"; +import { SignedXml } from "../src/index"; + +const compare = function ( + xml: string, + xpathArg: string, + expected: string, + inclusiveNamespacesPrefixList?: string[], + defaultNsForPrefix?: Record +) { const doc = new Dom().parseFromString(xml); - const elem = select(xpath, doc)[0]; + const elem = xpath.select1(xpathArg, doc); const can = new ExclusiveCanonicalization(); const result = can .process(elem, { - inclusiveNamespacesPrefixList: inclusiveNamespacesPrefixList, - defaultNsForPrefix: defaultNsForPrefix, + inclusiveNamespacesPrefixList, + defaultNsForPrefix, }) .toString(); @@ -84,7 +89,7 @@ describe("Canonicalization unit tests", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); @@ -93,7 +98,7 @@ describe("Canonicalization unit tests", function () { '123456', "//*[local-name(.)='child']", '123456', - "inclusive inclusive2" + ["inclusive", "inclusive2"] ); }); @@ -102,7 +107,7 @@ describe("Canonicalization unit tests", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); it("Exclusive canonicalization works on xml with prefixed namespace defined in inclusive list used on attribute", function () { @@ -110,7 +115,7 @@ describe("Canonicalization unit tests", function () { '123', "//*[local-name(.)='child']", '123', - "inclusive" + ["inclusive"] ); }); @@ -393,8 +398,9 @@ describe("Canonicalization unit tests", function () { it("Multiple Canonicalization with namespace definition outside of signed element", function () { //var doc = new Dom().parseFromString("") const doc = new Dom().parseFromString(''); - const node = select("//*[local-name(.)='y']", doc)[0]; + const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); + // @ts-expect-error FIXME const res = sig.getCanonXml( [ "http://www.w3.org/2000/09/xmldsig#enveloped-signature", @@ -412,9 +418,10 @@ describe("Canonicalization unit tests", function () { const xml = ''; const doc = new Dom().parseFromString(xml); - const node = select("//*[local-name(.)='y']", doc)[0]; + const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); const transforms = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]; + // @ts-expect-error FIXME const res = sig.getCanonXml(transforms, node); expect(res).to.equal(""); }); @@ -462,7 +469,7 @@ describe("Canonicalization unit tests", function () { '', "//*", '', - "ds" + ["ds"] ); }); }); diff --git a/test/document-tests.spec.ts b/test/document-tests.spec.ts index 0528d7d4..a875e0d5 100644 --- a/test/document-tests.spec.ts +++ b/test/document-tests.spec.ts @@ -1,14 +1,15 @@ -const crypto = require("../index"); -const xpath = require("xpath"); -const xmldom = require("@xmldom/xmldom"); -const fs = require("fs"); -const expect = require("chai").expect; +import { SignedXml } from "../src/index"; +import * as xpath from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as fs from "fs"; +import { expect } from "chai"; describe("Document tests", function () { it("test with a document (using FileKeyInfo)", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = new xmldom.DOMParser().parseFromString( + // @ts-expect-error FIXME xpath .select( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", @@ -16,7 +17,7 @@ describe("Document tests", function () { )[0] .toString() ); - const sig = new crypto.SignedXml(); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/feide_public.pem"); sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -28,6 +29,7 @@ describe("Document tests", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = new xmldom.DOMParser().parseFromString( + // @ts-expect-error FIXME xpath .select( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", @@ -35,7 +37,7 @@ describe("Document tests", function () { )[0] .toString() ); - const sig = new crypto.SignedXml(); + const sig = new SignedXml(); const feidePublicCert = fs.readFileSync("./test/static/feide_public.pem"); sig.publicCert = feidePublicCert; sig.loadSignature(signature); @@ -43,4 +45,4 @@ describe("Document tests", function () { expect(result).to.be.true; }); -}); \ No newline at end of file +}); diff --git a/test/hmac-tests.spec.ts b/test/hmac-tests.spec.ts index 1e52b7dc..fc24e864 100644 --- a/test/hmac-tests.spec.ts +++ b/test/hmac-tests.spec.ts @@ -1,20 +1,21 @@ -const crypto = require("../index"); -const xpath = require("xpath"); -const xmldom = require("@xmldom/xmldom"); -const fs = require("fs"); -const expect = require("chai").expect; +import { SignedXml } from "../src/index"; +import * as xpath from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as fs from "fs"; +import { expect } from "chai"; describe("HMAC tests", function () { it("test validating HMAC signature", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.enableHMAC(); sig.publicCert = fs.readFileSync("./test/static/hmac.key"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -24,13 +25,14 @@ describe("HMAC tests", function () { it("test HMAC signature with incorrect key", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.enableHMAC(); sig.publicCert = fs.readFileSync("./test/static/hmac-foobar.key"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -39,24 +41,25 @@ describe("HMAC tests", function () { it("test create and validate HMAC signature", function () { const xml = "" + "" + "Harry Potter" + "" + ""; - const sig = new crypto.SignedXml(); + const sig = new SignedXml(); sig.enableHMAC(); sig.privateKey = fs.readFileSync("./test/static/hmac.key"); sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; - sig.addReference("//*[local-name(.)='book']"); + sig.addReference({ xpath: "//*[local-name(.)='book']" }); sig.computeSignature(xml); const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const verify = new crypto.SignedXml(); + ); + const verify = new SignedXml(); verify.enableHMAC(); verify.publicCert = fs.readFileSync("./test/static/hmac.key"); + // @ts-expect-error FIXME verify.loadSignature(signature); const result = verify.checkSignature(sig.getSignedXml()); expect(result).to.be.true; }); -}); \ No newline at end of file +}); diff --git a/test/key-info-tests.spec.ts b/test/key-info-tests.spec.ts index ecde1dc9..964beb98 100644 --- a/test/key-info-tests.spec.ts +++ b/test/key-info-tests.spec.ts @@ -1,10 +1,9 @@ -const select = require("xpath").select; -const xmldom = require("@xmldom/xmldom"); -const SignedXml = require("../lib/signed-xml.js").SignedXml; -const fs = require("fs"); -const xpath = require("xpath"); -const crypto = require("../index.js"); -const expect = require("chai").expect; +import { select } from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as fs from "fs"; +import * as xpath from "xpath"; +import { SignedXml } from "../src/index"; +import { expect } from "chai"; describe("KeyInfo tests", function () { it("adds X509Certificate element during signature", function () { @@ -15,24 +14,25 @@ describe("KeyInfo tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); - const x509 = select("//*[local-name(.)='X509Certificate']", doc.documentElement); + const x509 = xpath.select("//*[local-name(.)='X509Certificate']", doc.documentElement); + // @ts-expect-error FIXME expect(x509.length, "X509Certificate element should exist").to.equal(1); }); it("make sure private hmac key is not leaked due to key confusion", function () { const xml = "" + "" + "Harry Potter" + "" + ""; - const sig = new crypto.SignedXml(); + const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/hmac.key"); sig.publicCert = fs.readFileSync("./test/static/hmac.key"); sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; sig.enableHMAC(); - sig.addReference("//*[local-name(.)='book']"); + sig.addReference({ xpath: "//*[local-name(.)='book']" }); sig.computeSignature(xml); const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); - const keyInfo = xpath.select("//*[local-name(.)='KeyInfo']", doc)[0]; + const keyInfo = xpath.select1("//*[local-name(.)='KeyInfo']", doc); expect(keyInfo).to.be.undefined; }); -}); \ No newline at end of file +}); diff --git a/test/saml-response-tests.spec.ts b/test/saml-response-tests.spec.ts index f80a7a79..34f5ca87 100644 --- a/test/saml-response-tests.spec.ts +++ b/test/saml-response-tests.spec.ts @@ -1,19 +1,20 @@ -const crypto = require("../index"); -const xpath = require("xpath"); -const xmldom = require("@xmldom/xmldom"); -const fs = require("fs"); -const expect = require("chai").expect; +import { SignedXml } from "../src/index"; +import * as xpath from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as fs from "fs"; +import { expect } from "chai"; describe("SAML response tests", function () { it("test validating SAML response", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/feide_public.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -23,13 +24,15 @@ describe("SAML response tests", function () { it("test validating wrapped assertion signature", function () { const xml = fs.readFileSync("./test/static/valid_saml_signature_wrapping.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const assertion = xpath.select("//*[local-name(.)='Assertion']", doc)[0]; - const signature = xpath.select( + const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); + const signature = xpath.select1( "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + // @ts-expect-error FIXME assertion - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/feide_public.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); expect(function () { sig.checkSignature(xml); @@ -41,12 +44,13 @@ describe("SAML response tests", function () { it("test validating SAML response where a namespace is defined outside the signed element", function () { const xml = fs.readFileSync("./test/static/saml_external_ns.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/saml_external_ns.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); expect(result).to.be.true; @@ -55,13 +59,15 @@ describe("SAML response tests", function () { it("test reference id does not contain quotes", function () { const xml = fs.readFileSync("./test/static/id_with_quotes.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const assertion = xpath.select("//*[local-name(.)='Assertion']", doc)[0]; - const signature = xpath.select( + const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); + const signature = xpath.select1( "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + // @ts-expect-error FIXME assertion - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/feide_public.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); expect(function () { sig.checkSignature(xml); @@ -71,12 +77,13 @@ describe("SAML response tests", function () { it("test validating SAML response WithComments", function () { const xml = fs.readFileSync("./test/static/valid_saml_withcomments.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/feide_public.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); // This doesn't matter, just want to make sure that we don't fail due to unknown algorithm diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index ed02ebba..b903ae5c 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -1,18 +1,18 @@ -const xpath = require("xpath"); -const Dom = require("@xmldom/xmldom").DOMParser; -const SignedXml = require("../lib/signed-xml.js").SignedXml; -const fs = require("fs"); -const crypto = require("../index"); -const expect = require("chai").expect; +import * as xpath from "xpath"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import { SignedXml } from "../src/index"; +import * as fs from "fs"; +import { expect } from "chai"; describe("Signature integration tests", function () { function verifySignature(xml, expected, xpath) { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.keyInfo = null; xpath.map(function (n) { - sig.addReference(n); + sig.addReference({ xpath: n }); }); sig.computeSignature(xml); @@ -86,7 +86,7 @@ describe("Signature integration tests", function () { "FONRc5/nnQE2GMuEV0wK5/ofUJMHH7dzZ6VVd+oHDLfjfWax/lCMzUahJxW1i/dtm9Pl0t2FbJONVd3wwDSZzy6u5uCnj++iWYkRpIEN19RAzEMD1ejfZET8j3db9NeBq2JjrPbw81Fm7qKvte6jGa9ThTTB+1MHFRkC8qjukRM=" + ""; - const sig = new crypto.SignedXml(); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/client_public.pem"); sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -104,14 +104,16 @@ describe("Signature integration tests", function () { xml = xml.replace(/>\s*<"); const doc = new Dom().parseFromString(xml); + // @ts-expect-error FIXME xml = doc.firstChild.toString(); - const signature = xpath.select( + const signature = xpath.select1( "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/windows_store_certificate.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -121,14 +123,16 @@ describe("Signature integration tests", function () { it("signature with inclusive namespaces", function () { let xml = fs.readFileSync("./test/static/signature_with_inclusivenamespaces.xml", "utf-8"); const doc = new Dom().parseFromString(xml); + // @ts-expect-error FIXME xml = doc.firstChild.toString(); - const signature = xpath.select( + const signature = xpath.select1( "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/signature_with_inclusivenamespaces.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -141,14 +145,16 @@ describe("Signature integration tests", function () { "utf-8" ); const doc = new Dom().parseFromString(xml); + // @ts-expect-error FIXME xml = doc.firstChild.toString(); - const signature = xpath.select( + const signature = xpath.select1( "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/signature_with_inclusivenamespaces.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -161,14 +167,16 @@ describe("Signature integration tests", function () { "utf-8" ); const doc = new Dom().parseFromString(xml); + // @ts-expect-error FIXME xml = doc.firstChild.toString(); - const signature = xpath.select( + const signature = xpath.select1( "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/signature_with_inclusivenamespaces.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); @@ -179,7 +187,7 @@ describe("Signature integration tests", function () { const xml = "" + "" + "Harry Potter" + "" + ""; const sig = new SignedXml(); - sig.addReference("//*[local-name(.)='book']"); + sig.addReference({ xpath: "//*[local-name(.)='book']" }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.computeSignature(xml); diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 971cf89c..6ed2918a 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1,20 +1,21 @@ -const select = require("xpath").select; -const dom = require("@xmldom/xmldom").DOMParser; -const SignedXml = require("../lib/signed-xml.js").SignedXml; -const fs = require("fs"); -const crypto = require("crypto"); -const expect = require("chai").expect; +import * as xpath from "xpath"; +import { DOMParser as Dom } from "@xmldom/xmldom"; +import { SignedXml } from "../src/index"; +import * as fs from "fs"; +import * as crypto from "crypto"; +import { expect } from "chai"; describe("Signature unit tests", function () { function verifySignature(xml, mode) { - const doc = new dom().parseFromString(xml); - const node = select( + const doc = new Dom().parseFromString(xml); + const node = xpath.select1( "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; + ); const sig = new SignedXml({ idMode: mode }); sig.publicCert = fs.readFileSync("./test/static/client_public.pem"); + // @ts-expect-error FIXME sig.loadSignature(node); try { const res = sig.checkSignature(xml); @@ -26,19 +27,20 @@ describe("Signature unit tests", function () { } function passValidSignature(file, mode) { - const xml = fs.readFileSync(file).toString(); + const xml = fs.readFileSync(file, "utf8"); const res = verifySignature(xml, mode); expect(res, "expected signature to be valid, but it was reported invalid").to.equal(true); } function passLoadSignature(file, toString) { - const xml = fs.readFileSync(file).toString(); - const doc = new dom().parseFromString(xml); - const node = select( + const xml = fs.readFileSync(file, "utf8"); + const doc = new Dom().parseFromString(xml); + const node = xpath.select1( "/*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; + ); const sig = new SignedXml(); + // @ts-expect-error FIXME sig.loadSignature(toString ? node.toString() : node); expect(sig.canonicalizationAlgorithm, "wrong canonicalization method").to.equal( @@ -49,16 +51,20 @@ describe("Signature unit tests", function () { "http://www.w3.org/2000/09/xmldsig#rsa-sha1" ); + // @ts-expect-error FIXME expect(sig.signatureValue, "wrong signature value").to.equal( "PI2xGt3XrVcxYZ34Kw7nFdq75c7Mmo7J0q7yeDhBprHuJal/KV9KyKG+Zy3bmQIxNwkPh0KMP5r1YMTKlyifwbWK0JitRCSa0Fa6z6+TgJi193yiR5S1MQ+esoQT0RzyIOBl9/GuJmXx/1rXnqrTxmL7UxtqKuM29/eHwF0QDUI=" ); - const keyInfo = select( + const keyInfo = xpath.select1( "//*[local-name(.)='KeyInfo']/*[local-name(.)='dummyKey']", - sig.keyInfo[0] - )[0]; + // @ts-expect-error FIXME + sig.keyInfo + ); + // @ts-expect-error FIXME expect(keyInfo.firstChild.data, "keyInfo clause not correctly loaded").to.equal("1234"); + // @ts-expect-error FIXME expect(sig.references.length).to.equal(3); const digests = [ @@ -67,7 +73,9 @@ describe("Signature unit tests", function () { "sH1gxKve8wlU8LlFVa2l6w3HMJ0=", ]; + // @ts-expect-error FIXME for (let i = 0; i < sig.references.length; i++) { + // @ts-expect-error FIXME const ref = sig.references[i]; const expectedUri = "#_" + i; expect( @@ -94,20 +102,22 @@ describe("Signature unit tests", function () { "Id='_1'>"; const sig = new SignedXml({ idMode: mode }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='x']"); + sig.addReference({ xpath: "//*[local-name(.)='x']" }); sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); - const doc = new dom().parseFromString(signedXml); - const attrs = select("//@*", doc); + const doc = new Dom().parseFromString(signedXml); + const attrs = xpath.select("//@*", doc); + // @ts-expect-error FIXME expect(attrs.length, "wrong number of attributes").to.equal(2); } - function nodeExists(doc, xpath) { - if (!doc && !xpath) { + function nodeExists(doc, xpathArg) { + if (!doc && !xpathArg) { return; } - const node = select(xpath, doc); - expect(node.length, "xpath " + xpath + " not found").to.equal(1); + const node = xpath.select(xpathArg, doc); + // @ts-expect-error FIXME + expect(node.length, "xpath " + xpathArg + " not found").to.equal(1); } function verifyAddsId(mode, nsMode) { @@ -115,25 +125,25 @@ describe("Signature unit tests", function () { const sig = new SignedXml({ idMode: mode }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='x']"); - sig.addReference("//*[local-name(.)='y']"); - sig.addReference("//*[local-name(.)='w']"); + sig.addReference({ xpath: "//*[local-name(.)='x']" }); + sig.addReference({ xpath: "//*[local-name(.)='y']" }); + sig.addReference({ xpath: "//*[local-name(.)='w']" }); sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); - const doc = new dom().parseFromString(signedXml); + const doc = new Dom().parseFromString(signedXml); const op = nsMode === "equal" ? "=" : "!="; - const xpath = + const xpathArg = "//*[local-name(.)='{elem}' and '_{id}' = @*[local-name(.)='Id' and namespace-uri(.)" + op + "'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd']]"; //verify each of the signed nodes now has an "Id" attribute with the right value - nodeExists(doc, xpath.replace("{id}", "0").replace("{elem}", "x")); - nodeExists(doc, xpath.replace("{id}", "1").replace("{elem}", "y")); - nodeExists(doc, xpath.replace("{id}", "2").replace("{elem}", "w")); + nodeExists(doc, xpathArg.replace("{id}", "0").replace("{elem}", "x")); + nodeExists(doc, xpathArg.replace("{id}", "1").replace("{elem}", "y")); + nodeExists(doc, xpathArg.replace("{id}", "2").replace("{elem}", "w")); } function verifyAddsAttrs() { @@ -147,14 +157,15 @@ describe("Signature unit tests", function () { sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='name']"); + sig.addReference({ xpath: "//*[local-name(.)='name']" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME attrs: attrs, }); const signedXml = sig.getSignatureXml(); - const doc = new dom().parseFromString(signedXml); + const doc = new Dom().parseFromString(signedXml); const signatureNode = doc.documentElement; expect( @@ -181,17 +192,19 @@ describe("Signature unit tests", function () { sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[@wsu:Id]"); + sig.addReference({ xpath: "//*[@wsu:Id]" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME existingPrefixes: { wsu: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", }, }); const signedXml = sig.getSignatureXml(); - const doc = new dom().parseFromString(signedXml); - const references = select("//*[local-name(.)='Reference']", doc); + const doc = new Dom().parseFromString(signedXml); + const references = xpath.select("//*[local-name(.)='Reference']", doc); + // @ts-expect-error FIXME expect(references.length).to.equal(2); } @@ -218,12 +231,13 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='name']"); + sig.addReference({ xpath: "//*[local-name(.)='name']" }); sig.computeSignature(xml); - const doc = new dom().parseFromString(sig.getSignedXml()); + const doc = new Dom().parseFromString(sig.getSignedXml()); expect( + // @ts-expect-error FIXME doc.documentElement.lastChild.localName, "the signature must be appended to the root node by default" ).to.equal("Signature"); @@ -234,19 +248,21 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='repository']"); + sig.addReference({ xpath: "//*[local-name(.)='repository']" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME location: { reference: "/root/name", action: "append", }, }); - const doc = new dom().parseFromString(sig.getSignedXml()); - const referenceNode = select("/root/name", doc)[0]; + const doc = new Dom().parseFromString(sig.getSignedXml()); + const referenceNode = xpath.select1("/root/name", doc); expect( + // @ts-expect-error FIXME referenceNode.lastChild.localName, "the signature should be appended to root/name" ).to.equal("Signature"); @@ -257,19 +273,21 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='repository']"); + sig.addReference({ xpath: "//*[local-name(.)='repository']" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME location: { reference: "/root/name", action: "prepend", }, }); - const doc = new dom().parseFromString(sig.getSignedXml()); - const referenceNode = select("/root/name", doc)[0]; + const doc = new Dom().parseFromString(sig.getSignedXml()); + const referenceNode = xpath.select1("/root/name", doc); expect( + // @ts-expect-error FIXME referenceNode.firstChild.localName, "the signature should be prepended to root/name" ).to.equal("Signature"); @@ -280,19 +298,21 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='repository']"); + sig.addReference({ xpath: "//*[local-name(.)='repository']" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME location: { reference: "/root/name", action: "before", }, }); - const doc = new dom().parseFromString(sig.getSignedXml()); - const referenceNode = select("/root/name", doc)[0]; + const doc = new Dom().parseFromString(sig.getSignedXml()); + const referenceNode = xpath.select1("/root/name", doc); expect( + // @ts-expect-error FIXME referenceNode.previousSibling.localName, "the signature should be inserted before to root/name" ).to.equal("Signature"); @@ -303,61 +323,63 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='repository']"); + sig.addReference({ xpath: "//*[local-name(.)='repository']" }); sig.computeSignature(xml, { + // @ts-expect-error FIXME location: { reference: "/root/name", action: "after", }, }); - const doc = new dom().parseFromString(sig.getSignedXml()); - const referenceNode = select("/root/name", doc)[0]; + const doc = new Dom().parseFromString(sig.getSignedXml()); + const referenceNode = xpath.select1("/root/name", doc); expect( + // @ts-expect-error FIXME referenceNode.nextSibling.localName, "the signature should be inserted after to root/name" ).to.equal("Signature"); }); it("signer creates signature with correct structure", function () { - function DummyDigest() { - this.getHash = function () { + class DummyDigest { + getHash = function () { return "dummy digest"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy digest algorithm"; }; } - function DummySignatureAlgorithm() { - this.getSignature = function () { + class DummySignatureAlgorithm { + getSignature = function () { return "dummy signature"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy algorithm"; }; } - function DummyTransformation() { - this.process = function () { + class DummyTransformation { + process = function () { return "< x/>"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy transformation"; }; } - function DummyCanonicalization() { - this.process = function () { + class DummyCanonicalization { + process = function () { return "< x/>"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy canonicalization"; }; } @@ -365,32 +387,37 @@ describe("Signature unit tests", function () { const xml = ''; const sig = new SignedXml(); + // @ts-expect-error FIXME sig.CanonicalizationAlgorithms["http://DummyTransformation"] = DummyTransformation; + // @ts-expect-error FIXME sig.CanonicalizationAlgorithms["http://DummyCanonicalization"] = DummyCanonicalization; sig.HashAlgorithms["http://dummyDigest"] = DummyDigest; + // @ts-expect-error FIXME sig.SignatureAlgorithms["http://dummySignatureAlgorithm"] = DummySignatureAlgorithm; sig.signatureAlgorithm = "http://dummySignatureAlgorithm"; sig.getKeyInfoContent = function () { return "dummy key info"; }; + // @ts-expect-error FIXME sig.canonicalizationAlgorithm = "http://DummyCanonicalization"; + sig.privateKey = ""; - sig.addReference( - "//*[local-name(.)='x']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); - sig.addReference( - "//*[local-name(.)='y']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); - sig.addReference( - "//*[local-name(.)='w']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); + sig.addReference({ + xpath: "//*[local-name(.)='y']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); + sig.addReference({ + xpath: "//*[local-name(.)='w']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); sig.computeSignature(xml); const signature = sig.getSignatureXml(); @@ -476,42 +503,42 @@ describe("Signature unit tests", function () { it("signer creates signature with correct structure (with prefix)", function () { const prefix = "ds"; - function DummyDigest() { - this.getHash = function () { + class DummyDigest { + getHash = function () { return "dummy digest"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy digest algorithm"; }; } - function DummySignatureAlgorithm() { - this.getSignature = function () { + class DummySignatureAlgorithm { + getSignature = function () { return "dummy signature"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy algorithm"; }; } - function DummyTransformation() { - this.process = function () { + class DummyTransformation { + process = function () { return "< x/>"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy transformation"; }; } - function DummyCanonicalization() { - this.process = function () { + class DummyCanonicalization { + process = function () { return "< x/>"; }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "dummy canonicalization"; }; } @@ -519,33 +546,39 @@ describe("Signature unit tests", function () { const xml = ''; const sig = new SignedXml(); + // @ts-expect-error FIXME sig.CanonicalizationAlgorithms["http://DummyTransformation"] = DummyTransformation; + // @ts-expect-error FIXME sig.CanonicalizationAlgorithms["http://DummyCanonicalization"] = DummyCanonicalization; sig.HashAlgorithms["http://dummyDigest"] = DummyDigest; + // @ts-expect-error FIXME sig.SignatureAlgorithms["http://dummySignatureAlgorithm"] = DummySignatureAlgorithm; sig.signatureAlgorithm = "http://dummySignatureAlgorithm"; sig.getKeyInfoContent = function () { return "dummy key info"; }; + // @ts-expect-error FIXME sig.canonicalizationAlgorithm = "http://DummyCanonicalization"; + sig.privateKey = ""; - sig.addReference( - "//*[local-name(.)='x']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); - sig.addReference( - "//*[local-name(.)='y']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); - sig.addReference( - "//*[local-name(.)='w']", - ["http://DummyTransformation"], - "http://dummyDigest" - ); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); + sig.addReference({ + xpath: "//*[local-name(.)='y']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); + sig.addReference({ + xpath: "//*[local-name(.)='w']", + transforms: ["http://DummyTransformation"], + digestAlgorithm: "http://dummyDigest", + }); + // @ts-expect-error FIXME sig.computeSignature(xml, { prefix: prefix }); const signature = sig.getSignatureXml(); @@ -633,11 +666,12 @@ describe("Signature unit tests", function () { ''; const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference("//*[local-name(.)='x']"); - sig.addReference("//*[local-name(.)='y']"); - sig.addReference("//*[local-name(.)='w']"); + sig.addReference({ xpath: "//*[local-name(.)='x']" }); + sig.addReference({ xpath: "//*[local-name(.)='y']" }); + sig.addReference({ xpath: "//*[local-name(.)='w']" }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -676,15 +710,15 @@ describe("Signature unit tests", function () { }); it("signer creates correct signature values using async callback", function () { - function DummySignatureAlgorithm() { - this.getSignature = function (signedInfo, privateKey, callback) { + class DummySignatureAlgorithm { + getSignature = function (signedInfo, privateKey, callback) { const signer = crypto.createSign("RSA-SHA1"); signer.update(signedInfo); const res = signer.sign(privateKey, "base64"); //Do some asynchronous things here callback(null, res); }; - this.getAlgorithmName = function () { + getAlgorithmName = function () { return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; }; } @@ -692,14 +726,16 @@ describe("Signature unit tests", function () { const xml = ''; const sig = new SignedXml(); + // @ts-expect-error FIXME sig.SignatureAlgorithms["http://dummySignatureAlgorithmAsync"] = DummySignatureAlgorithm; sig.signatureAlgorithm = "http://dummySignatureAlgorithmAsync"; sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference("//*[local-name(.)='x']"); - sig.addReference("//*[local-name(.)='y']"); - sig.addReference("//*[local-name(.)='w']"); + sig.addReference({ xpath: "//*[local-name(.)='x']" }); + sig.addReference({ xpath: "//*[local-name(.)='y']" }); + sig.addReference({ xpath: "//*[local-name(.)='w']" }); sig.computeSignature(xml, function () { const signedXml = sig.getSignedXml(); @@ -739,35 +775,92 @@ describe("Signature unit tests", function () { }); it("correctly loads signature", function () { + // @ts-expect-error FIXME passLoadSignature("./test/static/valid_signature.xml"); + }); + + it("correctly loads signature with validation", function () { passLoadSignature("./test/static/valid_signature.xml", true); + }); + + it("correctly loads signature with root level sig namespace", function () { + // @ts-expect-error FIXME passLoadSignature("./test/static/valid_signature_with_root_level_sig_namespace.xml"); }); - it("verify valid signature", function () { + it("verifies valid signature", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature.xml"); + }); + + it("verifies valid signature with lowercase id attribute", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature_with_lowercase_id_attribute.xml"); + }); + + it("verifies valid signature with wsu", function () { passValidSignature("./test/static/valid_signature wsu.xml", "wssecurity"); + }); + + it("verifies valid signature with reference keyInfo", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature_with_reference_keyInfo.xml"); + }); + + it("verifies valid signature with whitespace in digestvalue", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature_with_whitespace_in_digestvalue.xml"); + }); + + it("verifies valid utf8 signature", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature_utf8.xml"); + }); + + it("verifies valid signature with unused prefixes", function () { + // @ts-expect-error FIXME passValidSignature("./test/static/valid_signature_with_unused_prefixes.xml"); }); - it("fail invalid signature", function () { + it("fails invalid signature - signature value", function () { + // @ts-expect-error FIXME failInvalidSignature("./test/static/invalid_signature - signature value.xml"); + }); + + it("fails invalid signature - hash", function () { + // @ts-expect-error FIXME failInvalidSignature("./test/static/invalid_signature - hash.xml"); + }); + + it("fails invalid signature - non existing reference", function () { + // @ts-expect-error FIXME failInvalidSignature("./test/static/invalid_signature - non existing reference.xml"); + }); + + it("fails invalid signature - changed content", function () { + // @ts-expect-error FIXME failInvalidSignature("./test/static/invalid_signature - changed content.xml"); + }); + + it("fails invalid signature - wsu - invalid signature value", function () { failInvalidSignature( "./test/static/invalid_signature - wsu - invalid signature value.xml", "wssecurity" ); + }); + + it("fails invalid signature - wsu - hash", function () { failInvalidSignature("./test/static/invalid_signature - wsu - hash.xml", "wssecurity"); + }); + + it("fails invalid signature - wsu - non existing reference", function () { failInvalidSignature( "./test/static/invalid_signature - wsu - non existing reference.xml", "wssecurity" ); + }); + + it("fails invalid signature - wsu - changed content", function () { failInvalidSignature( "./test/static/invalid_signature - wsu - changed content.xml", "wssecurity" @@ -778,22 +871,24 @@ describe("Signature unit tests", function () { const xml = ""; const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference( - "//*[local-name(.)='root']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - "http://www.w3.org/2000/09/xmldsig#sha1", - "", - "", - "", - true - ); + sig.addReference({ + xpath: "//*[local-name(.)='root']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + uri: "", + digestValue: "", + inclusiveNamespacesPrefixList: [], + isEmptyUri: true, + }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const URI = select("//*[local-name(.)='Reference']/@URI", doc)[0]; + const doc = new Dom().parseFromString(signedXml); + const URI = xpath.select1("//*[local-name(.)='Reference']/@URI", doc); + // @ts-expect-error FIXME expect(URI.value, "uri should be empty but instead was " + URI.value).to.equal(""); }); @@ -802,10 +897,11 @@ describe("Signature unit tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.addReference("//*[local-name(.)='repository']"); + sig.addReference({ xpath: "//*[local-name(.)='repository']" }); try { sig.computeSignature(xml, { + // @ts-expect-error FIXME location: { reference: "/root/foobar", action: "append", @@ -845,6 +941,7 @@ describe("Signature unit tests", function () { sig.getKeyInfoContent = getKeyInfoContentWithAssertionId.bind(this, { assertionId }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.computeSignature(xml, { + // @ts-expect-error FIXME prefix: "ds", location: { reference: "//Assertion", @@ -865,27 +962,30 @@ describe("Signature unit tests", function () { const xml = ""; const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference( - "//*[local-name(.)='root']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - "http://www.w3.org/2000/09/xmldsig#sha1", - "", - "", - "prefix1 prefix2" - ); + sig.addReference({ + xpath: "//*[local-name(.)='root']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + uri: "", + digestValue: "", + inclusiveNamespacesPrefixList: ["prefix1", "prefix2"], + }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const inclusiveNamespaces = select( + const doc = new Dom().parseFromString(signedXml); + const inclusiveNamespaces = xpath.select( "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement ); + // @ts-expect-error FIXME expect(inclusiveNamespaces.length, "InclusiveNamespaces element should exist").to.equal(1); + // @ts-expect-error FIXME const prefixListAttribute = inclusiveNamespaces[0].getAttribute("PrefixList"); expect( prefixListAttribute, @@ -897,26 +997,28 @@ describe("Signature unit tests", function () { const xml = ""; const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference( - "//*[local-name(.)='root']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - "http://www.w3.org/2000/09/xmldsig#sha1", - "", - "", - "" - ); + sig.addReference({ + xpath: "//*[local-name(.)='root']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + uri: "", + digestValue: "", + inclusiveNamespacesPrefixList: [], + }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const inclusiveNamespaces = select( + const doc = new Dom().parseFromString(signedXml); + const inclusiveNamespaces = xpath.select( "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement ); + // @ts-expect-error FIXME expect(inclusiveNamespaces.length, "InclusiveNamespaces element should not exist").to.equal(0); }); @@ -924,28 +1026,31 @@ describe("Signature unit tests", function () { const xml = ""; const sig = new SignedXml({ inclusiveNamespacesPrefixList: "prefix1 prefix2" }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference( - "//*[local-name(.)='root']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - "http://www.w3.org/2000/09/xmldsig#sha1" - ); + sig.addReference({ + xpath: "//*[local-name(.)='root']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const inclusiveNamespaces = select( + const doc = new Dom().parseFromString(signedXml); + const inclusiveNamespaces = xpath.select( "//*[local-name(.)='CanonicalizationMethod']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement ); expect( + // @ts-expect-error FIXME inclusiveNamespaces.length, "InclusiveNamespaces element should exist inside CanonicalizationMethod" ).to.equal(1); + // @ts-expect-error FIXME const prefixListAttribute = inclusiveNamespaces[0].getAttribute("PrefixList"); expect( prefixListAttribute, @@ -957,24 +1062,26 @@ describe("Signature unit tests", function () { const xml = ""; const sig = new SignedXml(); // Omit inclusiveNamespacesPrefixList property sig.privateKey = fs.readFileSync("./test/static/client.pem"); + // @ts-expect-error FIXME sig.publicCert = null; - sig.addReference( - "//*[local-name(.)='root']", - ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - "http://www.w3.org/2000/09/xmldsig#sha1" - ); + sig.addReference({ + xpath: "//*[local-name(.)='root']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const inclusiveNamespaces = select( + const doc = new Dom().parseFromString(signedXml); + const inclusiveNamespaces = xpath.select( "//*[local-name(.)='CanonicalizationMethod']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement ); expect( + // @ts-expect-error FIXME inclusiveNamespaces.length, "InclusiveNamespaces element should not exist inside CanonicalizationMethod" ).to.equal(0); @@ -993,16 +1100,19 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); - const keyInfoElement = select("//*[local-name(.)='KeyInfo']", doc.documentElement); + const doc = new Dom().parseFromString(signedXml); + const keyInfoElement = xpath.select("//*[local-name(.)='KeyInfo']", doc.documentElement); + // @ts-expect-error FIXME expect(keyInfoElement.length, "KeyInfo element should exist").to.equal(1); + // @ts-expect-error FIXME const algorithmAttribute = keyInfoElement[0].getAttribute("CustomUri"); expect( algorithmAttribute, "KeyInfo element should have the correct CustomUri attribute value" ).to.equal("http://www.example.com/keyinfo"); + // @ts-expect-error FIXME const customAttribute = keyInfoElement[0].getAttribute("CustomAttribute"); expect( customAttribute, @@ -1019,12 +1129,18 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new dom().parseFromString(signedXml); + const doc = new Dom().parseFromString(signedXml); - const x509certificates = select("//*[local-name(.)='X509Certificate']", doc.documentElement); + const x509certificates = xpath.select( + "//*[local-name(.)='X509Certificate']", + doc.documentElement + ); + // @ts-expect-error FIXME expect(x509certificates.length, "There should be exactly two certificates").to.equal(2); + // @ts-expect-error FIXME const cert1 = x509certificates[0]; + // @ts-expect-error FIXME const cert2 = x509certificates[1]; expect(cert1.textContent, "X509Certificate[0] TextContent does not exist").to.exist; expect(cert2.textContent, "X509Certificate[1] TextContent does not exist").to.exist; diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index d6aa03eb..355475bc 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -1,6 +1,6 @@ -const fs = require("fs"); -const utils = require("../lib/utils"); -const expect = require("chai").expect; +import * as fs from "fs"; +import * as utils from "../src/utils"; +import { expect } from "chai"; describe("Utils tests", function () { describe("derToPem", function () { @@ -11,6 +11,7 @@ describe("Utils tests", function () { const nonNormalizedPem = pemAsArray[0] + "\n" + base64String + "\n" + pemAsArray[pemAsArray.length - 1]; + // @ts-expect-error FIXME expect(utils.derToPem(nonNormalizedPem)).to.equal(normalizedPem); }); @@ -23,7 +24,8 @@ describe("Utils tests", function () { }); it("will throw if the format is neither PEM nor DER", function () { + // @ts-expect-error FIXME expect(() => utils.derToPem("not a pem")).to.throw(); }); }); -}); \ No newline at end of file +}); diff --git a/test/wsfed-metadata-tests.spec.ts b/test/wsfed-metadata-tests.spec.ts index 84d7e851..d774d2d9 100644 --- a/test/wsfed-metadata-tests.spec.ts +++ b/test/wsfed-metadata-tests.spec.ts @@ -1,22 +1,23 @@ -const crypto = require("../index"); -const xpath = require("xpath"); -const xmldom = require("@xmldom/xmldom"); -const fs = require("fs"); -const expect = require("chai").expect; +import { SignedXml } from "../src/index"; +import * as xpath from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as fs from "fs"; +import { expect } from "chai"; describe("WS-Fed Metadata tests", function () { it("test validating WS-Fed Metadata", function () { const xml = fs.readFileSync("./test/static/wsfederation_metadata.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); - const signature = xpath.select( + const signature = xpath.select1( "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc - )[0]; - const sig = new crypto.SignedXml(); + ); + const sig = new SignedXml(); sig.publicCert = fs.readFileSync("./test/static/wsfederation_metadata.pem"); + // @ts-expect-error FIXME sig.loadSignature(signature); const result = sig.checkSignature(xml); expect(result).to.be.true; }); -}); \ No newline at end of file +});