diff --git a/index.d.ts b/index.d.ts index b202d217..6580201b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,7 @@ /// import { SelectedValue } from "xpath"; +import * as crypto from "crypto"; type CanonicalizationAlgorithmType = | "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" @@ -87,21 +88,35 @@ export interface Reference { } /** Implement this to create a new HashAlgorithm */ -export interface HashAlgorithm { +export class HashAlgorithm { getAlgorithmName(): HashAlgorithmType; getHash(xml: string): string; } /** Implement this to create a new SignatureAlgorithm */ -export interface SignatureAlgorithm { +export class SignatureAlgorithm { getAlgorithmName(): SignatureAlgorithmType; - getSignature(signedInfo: Node, privateKey: Buffer): string; + getSignature( + signedInfo: crypto.BinaryLike, + privateKey: crypto.KeyLike, + callback?: (err: Error, signedInfo: string) => never + ): string; + + /** + * @param key a public cert, public key, or private key can be passed here + */ + verifySignature( + material: string, + key: crypto.KeyLike, + signatureValue: string, + callback?: (err: Error, verified: boolean) => never + ): boolean; } /** Implement this to create a new TransformAlgorithm */ -export interface TransformAlgorithm { +export class TransformAlgorithm { getAlgorithmName(): TransformAlgorithmType; process(node: Node): string; diff --git a/lib/hash-algorithms.js b/lib/hash-algorithms.js new file mode 100644 index 00000000..070d7170 --- /dev/null +++ b/lib/hash-algorithms.js @@ -0,0 +1,61 @@ +const crypto = require("crypto"); + +/** + * @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; + }; + + this.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; + }; + + this.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; + }; + + this.getAlgorithmName = function () { + return "http://www.w3.org/2001/04/xmlenc#sha512"; + }; + } +} + +module.exports = { + Sha1, + Sha256, + Sha512, +}; diff --git a/lib/signature-algorithms.js b/lib/signature-algorithms.js new file mode 100644 index 00000000..2c1a66f8 --- /dev/null +++ b/lib/signature-algorithms.js @@ -0,0 +1,134 @@ +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) { + 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) { + const verifier = crypto.createVerify("RSA-SHA1"); + verifier.update(str); + 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"; + }; + } +} + +/** + * @type { import("../index.d.ts").SignatureAlgorithm} SignatureAlgorithm + */ +class RsaSha256 { + constructor() { + this.getSignature = function (signedInfo, privateKey, callback) { + 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) { + const verifier = crypto.createVerify("RSA-SHA256"); + verifier.update(str); + 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"; + }; + } +} + +/** + * @type { import("../index.d.ts").SignatureAlgorithm} + */ +class RsaSha512 { + constructor() { + this.getSignature = function (signedInfo, privateKey, callback) { + 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) { + const verifier = crypto.createVerify("RSA-SHA512"); + verifier.update(str); + 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"; + }; + } +} + +/** + * @type { import("../index.d.ts").SignatureAlgorithm} + */ +class HmacSha1 { + constructor() { + this.verifySignature = function (str, key, signatureValue) { + const verifier = crypto.createHmac("SHA1", key); + verifier.update(str); + const res = verifier.digest("base64"); + return res === signatureValue; + }; + + this.getAlgorithmName = function () { + return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; + }; + + this.getSignature = function (signedInfo, privateKey) { + const verifier = crypto.createHmac("SHA1", privateKey); + verifier.update(signedInfo); + const res = verifier.digest("base64"); + return res; + }; + } +} + +module.exports = { + RsaSha1, + RsaSha256, + RsaSha512, + HmacSha1, +}; diff --git a/lib/signed-xml.js b/lib/signed-xml.js index 5de4529d..17777624 100644 --- a/lib/signed-xml.js +++ b/lib/signed-xml.js @@ -4,307 +4,13 @@ const utils = require("./utils"); const c14n = require("./c14n-canonicalization"); const execC14n = require("./exclusive-canonicalization"); const EnvelopedSignature = require("./enveloped-signature").EnvelopedSignature; -const crypto = require("crypto"); +const hashAlgorithms = require("./hash-algorithms"); +const signatureAlgorithms = require("./signature-algorithms"); /** - * Hash algorithm implementation - * - */ -function SHA1() { - this.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"; - }; -} - -function SHA256() { - this.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"; - }; -} - -function SHA512() { - this.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"; - }; -} - -/** - * Signature algorithm implementation - * - */ -function RSASHA1() { - /** - * Sign the given string using the given key - * - */ - this.getSignature = function (signedInfo, privateKey, callback) { - 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) { - const verifier = crypto.createVerify("RSA-SHA1"); - verifier.update(str); - 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"; - }; -} - -/** - * Signature algorithm implementation - * - */ -function RSASHA256() { - /** - * Sign the given string using the given key - * - */ - this.getSignature = function (signedInfo, privateKey, callback) { - const signer = crypto.createSign("RSA-SHA256"); - 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) { - const verifier = crypto.createVerify("RSA-SHA256"); - verifier.update(str); - 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"; - }; -} - -/** - * Signature algorithm implementation - * - */ -function RSASHA512() { - /** - * Sign the given string using the given key - * - */ - this.getSignature = function (signedInfo, privateKey, callback) { - const signer = crypto.createSign("RSA-SHA512"); - 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) { - const verifier = crypto.createVerify("RSA-SHA512"); - verifier.update(str); - 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"; - }; -} - -function HMACSHA1() { - this.verifySignature = function (str, key, signatureValue) { - const verifier = crypto.createHmac("SHA1", key); - verifier.update(str); - const res = verifier.digest("base64"); - return res === signatureValue; - }; - - this.getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; - }; - - this.getSignature = function (signedInfo, privateKey) { - const verifier = crypto.createHmac("SHA1", privateKey); - verifier.update(signedInfo); - const res = verifier.digest("base64"); - return res; - }; -} - -function collectAncestorNamespaces(node, nsArray) { - if (!nsArray) { - nsArray = []; - } - - const parent = node.parentNode; - - if (!parent) { - return nsArray; - } - - if (parent.attributes && parent.attributes.length > 0) { - for (let i = 0; i < parent.attributes.length; i++) { - const attr = parent.attributes[i]; - if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:/) !== -1) { - nsArray.push({ - prefix: attr.nodeName.replace(/^xmlns:/, ""), - namespaceURI: attr.nodeValue, - }); - } - } - } - - return collectAncestorNamespaces(parent, nsArray); -} - -/** - * Extract ancestor namespaces in order to import it to root of document subset - * which is being canonicalized for non-exclusive c14n. - * - * @param {object} doc - Usually a product from `new DOMParser().parseFromString()` - * @param {string} docSubsetXpath - xpath query to get document subset being canonicalized - * @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) { - const docSubset = xpath.selectWithResolver(docSubsetXpath, doc, namespaceResolver); - - if (!Array.isArray(docSubset) || docSubset.length < 1) { - return []; - } - - // Remove duplicate on ancestor namespace - const ancestorNs = collectAncestorNamespaces(docSubset[0]); - const ancestorNsWithoutDuplicate = []; - for (let i = 0; i < ancestorNs.length; i++) { - let notOnTheList = true; - for (const v in ancestorNsWithoutDuplicate) { - if (ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix) { - notOnTheList = false; - break; - } - } - - if (notOnTheList) { - ancestorNsWithoutDuplicate.push(ancestorNs[i]); - } - } - - // Remove namespaces which are already declared in the subset with the same prefix - const returningNs = []; - const subsetAttributes = docSubset[0].attributes; - for (let j = 0; j < ancestorNsWithoutDuplicate.length; j++) { - let isUnique = true; - for (let k = 0; k < subsetAttributes.length; k++) { - const nodeName = subsetAttributes[k].nodeName; - if (nodeName.search(/^xmlns:/) === -1) { - continue; - } - const prefix = nodeName.replace(/^xmlns:/, ""); - if (ancestorNsWithoutDuplicate[j].prefix === prefix) { - isUnique = false; - break; - } - } - - if (isUnique) { - returningNs.push(ancestorNsWithoutDuplicate[j]); - } - } - - 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"); - } - - if (typeof buffer.equals === "function") { - return buffer.equals(expectedBuffer); - } - - // Compatibility with Node < 0.11.13 - if (buffer.length !== expectedBuffer.length) { - return false; - } - - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] !== expectedBuffer[i]) { - return false; - } - } - - return true; -} /** - * Xml signature implementation - * - * @param {string} idMode. Value of "wssecurity" will create/validate id's with the ws-security namespace - * @param {object} options. Initial configurations + * @typedef { import("../index.d.ts").SignedXml} */ class SignedXml { constructor(idMode, options = {}) { @@ -346,17 +52,17 @@ class SignedXml { }; this.HashAlgorithms = { - "http://www.w3.org/2000/09/xmldsig#sha1": SHA1, - "http://www.w3.org/2001/04/xmlenc#sha256": SHA256, - "http://www.w3.org/2001/04/xmlenc#sha512": SHA512, + "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": RSASHA1, - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": RSASHA256, - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": RSASHA512, + "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': HMACSHA1 + // 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': SignatureAlgorithms.HmacSha1 }; } @@ -367,7 +73,7 @@ class SignedXml { */ enableHMAC() { this.SignatureAlgorithms = { - "http://www.w3.org/2000/09/xmldsig#hmac-sha1": HMACSHA1, + "http://www.w3.org/2000/09/xmldsig#hmac-sha1": signatureAlgorithms.HmacSha1, }; this.getKeyInfoContent = () => null; } @@ -470,7 +176,7 @@ class SignedXml { * Search for ancestor namespaces before canonicalization. */ let ancestorNamespaces = []; - ancestorNamespaces = findAncestorNs(doc, "//*[local-name()='SignedInfo']"); + ancestorNamespaces = utils.findAncestorNs(doc, "//*[local-name()='SignedInfo']"); const c14nOptions = { ancestorNamespaces: ancestorNamespaces, @@ -483,7 +189,7 @@ class SignedXml { * Search for ancestor namespaces before canonicalization. */ if (Array.isArray(ref.transforms)) { - ref.ancestorNamespaces = findAncestorNs(doc, ref.xpath, this.namespaceResolver); + ref.ancestorNamespaces = utils.findAncestorNs(doc, ref.xpath, this.namespaceResolver); } const c14nOptions = { @@ -600,7 +306,7 @@ class SignedXml { const hash = this.findHashAlgorithm(ref.digestAlgorithm); const digest = hash.getHash(canonXml); - if (!validateDigestValue(digest, ref.digestValue)) { + if (!utils.validateDigestValue(digest, ref.digestValue)) { this.validationErrors.push( "invalid signature: for uri " + ref.uri + @@ -1218,5 +924,4 @@ exports.SignedXml = SignedXml; module.exports = { SignedXml, - findAncestorNs, }; diff --git a/lib/utils.js b/lib/utils.js index 30b87c15..16a5c762 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,4 @@ -const select = require("xpath").select; +const xpath = require("xpath"); function attrEqualsExplicitly(attr, localName, namespace) { return attr.localName == localName && (attr.namespaceURI == namespace || !namespace); @@ -25,10 +25,10 @@ function findAttr(node, localName, namespace) { return null; } -function findFirst(doc, xpath) { - const nodes = select(xpath, doc); +function findFirst(doc, path) { + const nodes = xpath.select(path, doc); if (nodes.length == 0) { - throw "could not find xpath " + xpath; + throw "could not find xpath " + path; } return nodes[0]; } @@ -124,14 +124,136 @@ function derToPem(der, pemLabel) { throw new Error("Unknown DER format."); } -exports.findAttr = findAttr; -exports.findChilds = findChilds; -exports.encodeSpecialCharactersInAttribute = encodeSpecialCharactersInAttribute; -exports.encodeSpecialCharactersInText = encodeSpecialCharactersInText; -exports.findFirst = findFirst; -exports.EXTRACT_X509_CERTS = EXTRACT_X509_CERTS; -exports.PEM_FORMAT_REGEX = PEM_FORMAT_REGEX; -exports.BASE64_REGEX = BASE64_REGEX; -exports.pemToDer = pemToDer; -exports.derToPem = derToPem; -exports.normalizePem = normalizePem; +function collectAncestorNamespaces(node, nsArray) { + if (!nsArray) { + nsArray = []; + } + + const parent = node.parentNode; + + if (!parent) { + return nsArray; + } + + if (parent.attributes && parent.attributes.length > 0) { + for (let i = 0; i < parent.attributes.length; i++) { + const attr = parent.attributes[i]; + if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:/) !== -1) { + nsArray.push({ + prefix: attr.nodeName.replace(/^xmlns:/, ""), + namespaceURI: attr.nodeValue, + }); + } + } + } + + return collectAncestorNamespaces(parent, nsArray); +} + +/** + * Extract ancestor namespaces in order to import it to root of document subset + * which is being canonicalized for non-exclusive c14n. + * + * @param {object} doc - Usually a product from `new DOMParser().parseFromString()` + * @param {string} docSubsetXpath - xpath query to get document subset being canonicalized + * @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) { + const docSubset = xpath.selectWithResolver(docSubsetXpath, doc, namespaceResolver); + + if (!Array.isArray(docSubset) || docSubset.length < 1) { + return []; + } + + // Remove duplicate on ancestor namespace + const ancestorNs = collectAncestorNamespaces(docSubset[0]); + const ancestorNsWithoutDuplicate = []; + for (let i = 0; i < ancestorNs.length; i++) { + let notOnTheList = true; + for (const v in ancestorNsWithoutDuplicate) { + if (ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix) { + notOnTheList = false; + break; + } + } + + if (notOnTheList) { + ancestorNsWithoutDuplicate.push(ancestorNs[i]); + } + } + + // Remove namespaces which are already declared in the subset with the same prefix + const returningNs = []; + const subsetAttributes = docSubset[0].attributes; + for (let j = 0; j < ancestorNsWithoutDuplicate.length; j++) { + let isUnique = true; + for (let k = 0; k < subsetAttributes.length; k++) { + const nodeName = subsetAttributes[k].nodeName; + if (nodeName.search(/^xmlns:/) === -1) { + continue; + } + const prefix = nodeName.replace(/^xmlns:/, ""); + if (ancestorNsWithoutDuplicate[j].prefix === prefix) { + isUnique = false; + break; + } + } + + if (isUnique) { + returningNs.push(ancestorNsWithoutDuplicate[j]); + } + } + + 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"); + } + + if (typeof buffer.equals === "function") { + return buffer.equals(expectedBuffer); + } + + // Compatibility with Node < 0.11.13 + if (buffer.length !== expectedBuffer.length) { + return false; + } + + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] !== expectedBuffer[i]) { + return false; + } + } + + 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-test.js b/test/c14n-non-exclusive-unit-test.js index ab96756f..3d54d80b 100644 --- a/test/c14n-non-exclusive-unit-test.js +++ b/test/c14n-non-exclusive-unit-test.js @@ -3,7 +3,7 @@ const expect = require("chai").expect; const C14nCanonicalization = require("../lib/c14n-canonicalization").C14nCanonicalization; const Dom = require("@xmldom/xmldom").DOMParser; const select = require("xpath").select; -const findAncestorNs = require("../lib/signed-xml").findAncestorNs; +const utils = require("../lib/utils"); const test_C14nCanonicalization = function (xml, xpath, expected) { const doc = new Dom().parseFromString(xml); @@ -11,7 +11,7 @@ const test_C14nCanonicalization = function (xml, xpath, expected) { const can = new C14nCanonicalization(); const result = can .process(elem, { - ancestorNamespaces: findAncestorNs(doc, xpath), + ancestorNamespaces: utils.findAncestorNs(doc, xpath), }) .toString(); @@ -20,7 +20,7 @@ const test_C14nCanonicalization = function (xml, xpath, expected) { const test_findAncestorNs = function (xml, xpath, expected) { const doc = new Dom().parseFromString(xml); - const result = findAncestorNs(doc, xpath); + const result = utils.findAncestorNs(doc, xpath); expect(result).to.deep.equal(expected); };