xml-crypto
Advanced tools
Comparing version 3.2.0 to 4.0.0
@@ -1,294 +0,224 @@ | ||
/* jshint laxcomma: true */ | ||
var utils = require("./utils"); | ||
exports.C14nCanonicalization = C14nCanonicalization; | ||
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments; | ||
function C14nCanonicalization() { | ||
this.includeComments = false; | ||
} | ||
C14nCanonicalization.prototype.attrCompare = function (a, b) { | ||
if (!a.namespaceURI && b.namespaceURI) { | ||
return -1; | ||
} | ||
if (!b.namespaceURI && a.namespaceURI) { | ||
return 1; | ||
} | ||
var left = a.namespaceURI + a.localName; | ||
var right = b.namespaceURI + b.localName; | ||
if (left === right) return 0; | ||
else if (left < right) return -1; | ||
else return 1; | ||
}; | ||
C14nCanonicalization.prototype.nsCompare = function (a, b) { | ||
var attr1 = a.prefix; | ||
var attr2 = b.prefix; | ||
if (attr1 == attr2) { | ||
return 0; | ||
} | ||
return attr1.localeCompare(attr2); | ||
}; | ||
C14nCanonicalization.prototype.renderAttrs = function (node, defaultNS) { | ||
var a, | ||
i, | ||
attr, | ||
res = [], | ||
attrListToRender = []; | ||
if (node.nodeType === 8) { | ||
return this.renderComment(node); | ||
} | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//ignore namespace definition attributes | ||
if (attr.name.indexOf("xmlns") === 0) { | ||
continue; | ||
} | ||
attrListToRender.push(attr); | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.C14nCanonicalizationWithComments = exports.C14nCanonicalization = void 0; | ||
const utils = require("./utils"); | ||
const xpath = require("xpath"); | ||
class C14nCanonicalization { | ||
constructor() { | ||
this.includeComments = false; | ||
} | ||
} | ||
attrListToRender.sort(this.attrCompare); | ||
for (a in attrListToRender) { | ||
if (!attrListToRender.hasOwnProperty(a)) { | ||
continue; | ||
attrCompare(a, b) { | ||
if (!a.namespaceURI && b.namespaceURI) { | ||
return -1; | ||
} | ||
if (!b.namespaceURI && a.namespaceURI) { | ||
return 1; | ||
} | ||
const left = a.namespaceURI + a.localName; | ||
const right = b.namespaceURI + b.localName; | ||
if (left === right) { | ||
return 0; | ||
} | ||
else if (left < right) { | ||
return -1; | ||
} | ||
else { | ||
return 1; | ||
} | ||
} | ||
attr = attrListToRender[a]; | ||
res.push(" ", attr.name, '="', utils.encodeSpecialCharactersInAttribute(attr.value), '"'); | ||
} | ||
return res.join(""); | ||
}; | ||
/** | ||
* 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} | ||
* @api private | ||
*/ | ||
C14nCanonicalization.prototype.renderNs = function ( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
ancestorNamespaces | ||
) { | ||
var a, | ||
i, | ||
p, | ||
attr, | ||
res = [], | ||
newDefaultNs = defaultNs, | ||
nsListToRender = [], | ||
currNs = node.namespaceURI || ""; | ||
//handle the namespaceof the node itself | ||
if (node.prefix) { | ||
if (prefixesInScope.indexOf(node.prefix) == -1) { | ||
nsListToRender.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
prefixesInScope.push(node.prefix); | ||
nsCompare(a, b) { | ||
const attr1 = a.prefix; | ||
const attr2 = b.prefix; | ||
if (attr1 === attr2) { | ||
return 0; | ||
} | ||
return attr1.localeCompare(attr2); | ||
} | ||
} else if (defaultNs != currNs) { | ||
//new default ns | ||
newDefaultNs = node.namespaceURI; | ||
res.push(' xmlns="', newDefaultNs, '"'); | ||
} | ||
//handle the attributes namespace | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//handle all prefixed attributes that are included in the prefix list and where | ||
//the prefix is not defined already. New prefixes can only be defined by `xmlns:`. | ||
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) { | ||
nsListToRender.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
prefixesInScope.push(attr.localName); | ||
} | ||
//handle all prefixed attributes that are not xmlns definitions and where | ||
//the prefix is not defined already | ||
if ( | ||
attr.prefix && | ||
prefixesInScope.indexOf(attr.prefix) == -1 && | ||
attr.prefix != "xmlns" && | ||
attr.prefix != "xml" | ||
) { | ||
nsListToRender.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
prefixesInScope.push(attr.prefix); | ||
} | ||
renderAttrs(node) { | ||
let i; | ||
let attr; | ||
const attrListToRender = []; | ||
if (xpath.isComment(node)) { | ||
return this.renderComment(node); | ||
} | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//ignore namespace definition attributes | ||
if (attr.name.indexOf("xmlns") === 0) { | ||
continue; | ||
} | ||
attrListToRender.push(attr); | ||
} | ||
} | ||
attrListToRender.sort(this.attrCompare); | ||
const res = attrListToRender.map((attr) => { | ||
return ` ${attr.name}="${utils.encodeSpecialCharactersInAttribute(attr.value)}"`; | ||
}); | ||
return res.join(""); | ||
} | ||
} | ||
if (Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0) { | ||
// Remove namespaces which are already present in nsListToRender | ||
for (var p1 in ancestorNamespaces) { | ||
if (!ancestorNamespaces.hasOwnProperty(p1)) continue; | ||
var alreadyListed = false; | ||
for (var p2 in nsListToRender) { | ||
if ( | ||
nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix && | ||
nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI | ||
) { | ||
alreadyListed = true; | ||
/** | ||
* Create the string of all namespace declarations that should appear on this element | ||
* | ||
* @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) { | ||
let i; | ||
let attr; | ||
const res = []; | ||
let newDefaultNs = defaultNs; | ||
const nsListToRender = []; | ||
const currNs = node.namespaceURI || ""; | ||
//handle the namespace of the node itself | ||
if (node.prefix) { | ||
if (prefixesInScope.indexOf(node.prefix) === -1) { | ||
nsListToRender.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
prefixesInScope.push(node.prefix); | ||
} | ||
} | ||
} | ||
if (!alreadyListed) { | ||
nsListToRender.push(ancestorNamespaces[p1]); | ||
} | ||
else if (defaultNs !== currNs) { | ||
//new default ns | ||
newDefaultNs = node.namespaceURI || ""; | ||
res.push(' xmlns="', newDefaultNs, '"'); | ||
} | ||
//handle the attributes namespace | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//handle all prefixed attributes that are included in the prefix list and where | ||
//the prefix is not defined already. New prefixes can only be defined by `xmlns:`. | ||
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) { | ||
nsListToRender.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
prefixesInScope.push(attr.localName); | ||
} | ||
//handle all prefixed attributes that are not xmlns definitions and where | ||
//the prefix is not defined already | ||
if (attr.prefix && | ||
prefixesInScope.indexOf(attr.prefix) === -1 && | ||
attr.prefix !== "xmlns" && | ||
attr.prefix !== "xml") { | ||
nsListToRender.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
prefixesInScope.push(attr.prefix); | ||
} | ||
} | ||
} | ||
if (utils.isArrayHasLength(ancestorNamespaces)) { | ||
// Remove namespaces which are already present in nsListToRender | ||
for (const ancestorNamespace of ancestorNamespaces) { | ||
let alreadyListed = false; | ||
for (const nsToRender of nsListToRender) { | ||
if (nsToRender.prefix === ancestorNamespace.prefix && | ||
nsToRender.namespaceURI === ancestorNamespace.namespaceURI) { | ||
alreadyListed = true; | ||
} | ||
} | ||
if (!alreadyListed) { | ||
nsListToRender.push(ancestorNamespace); | ||
} | ||
} | ||
} | ||
nsListToRender.sort(this.nsCompare); | ||
//render namespaces | ||
res.push(...nsListToRender.map((attr) => { | ||
if (attr.prefix) { | ||
return ` xmlns:${attr.prefix}="${attr.namespaceURI}"`; | ||
} | ||
return ` xmlns="${attr.namespaceURI}"`; | ||
})); | ||
return { rendered: res.join(""), newDefaultNs }; | ||
} | ||
} | ||
nsListToRender.sort(this.nsCompare); | ||
//render namespaces | ||
for (a in nsListToRender) { | ||
if (!nsListToRender.hasOwnProperty(a)) { | ||
continue; | ||
processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) { | ||
if (xpath.isComment(node)) { | ||
return this.renderComment(node); | ||
} | ||
if (node.data) { | ||
return utils.encodeSpecialCharactersInText(node.data); | ||
} | ||
let i; | ||
let pfxCopy; | ||
const ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces); | ||
const res = ["<", node.tagName, ns.rendered, this.renderAttrs(node), ">"]; | ||
for (i = 0; i < node.childNodes.length; ++i) { | ||
pfxCopy = prefixesInScope.slice(0); | ||
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, [])); | ||
} | ||
res.push("</", node.tagName, ">"); | ||
return res.join(""); | ||
} | ||
p = nsListToRender[a]; | ||
res.push(" xmlns", p.prefix ? ":" + p.prefix : "", '="', p.namespaceURI, '"'); | ||
} | ||
return { rendered: res.join(""), newDefaultNs: newDefaultNs }; | ||
}; | ||
C14nCanonicalization.prototype.processInner = function ( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
ancestorNamespaces | ||
) { | ||
if (node.nodeType === 8) { | ||
return this.renderComment(node); | ||
} | ||
if (node.data) { | ||
return utils.encodeSpecialCharactersInText(node.data); | ||
} | ||
var i, | ||
pfxCopy, | ||
ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces), | ||
res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"]; | ||
for (i = 0; i < node.childNodes.length; ++i) { | ||
pfxCopy = prefixesInScope.slice(0); | ||
res.push( | ||
this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, []) | ||
); | ||
} | ||
res.push("</", node.tagName, ">"); | ||
return res.join(""); | ||
}; | ||
// Thanks to deoxxa/xml-c14n for comment renderer | ||
C14nCanonicalization.prototype.renderComment = function (node) { | ||
if (!this.includeComments) { | ||
return ""; | ||
} | ||
var isOutsideDocument = node.ownerDocument === node.parentNode, | ||
isBeforeDocument = null, | ||
isAfterDocument = null; | ||
if (isOutsideDocument) { | ||
var nextNode = node, | ||
previousNode = node; | ||
while (nextNode !== null) { | ||
if (nextNode === node.ownerDocument.documentElement) { | ||
isBeforeDocument = true; | ||
break; | ||
} | ||
nextNode = nextNode.nextSibling; | ||
// Thanks to deoxxa/xml-c14n for comment renderer | ||
renderComment(node) { | ||
if (!this.includeComments) { | ||
return ""; | ||
} | ||
const isOutsideDocument = node.ownerDocument === node.parentNode; | ||
let isBeforeDocument = false; | ||
let isAfterDocument = false; | ||
if (isOutsideDocument) { | ||
let nextNode = node; | ||
let previousNode = node; | ||
while (nextNode !== null) { | ||
if (nextNode === node.ownerDocument.documentElement) { | ||
isBeforeDocument = true; | ||
break; | ||
} | ||
nextNode = nextNode.nextSibling; | ||
} | ||
while (previousNode !== null) { | ||
if (previousNode === node.ownerDocument.documentElement) { | ||
isAfterDocument = true; | ||
break; | ||
} | ||
previousNode = previousNode.previousSibling; | ||
} | ||
} | ||
const afterDocument = isAfterDocument ? "\n" : ""; | ||
const beforeDocument = isBeforeDocument ? "\n" : ""; | ||
const encodedText = utils.encodeSpecialCharactersInText(node.data); | ||
return `${afterDocument}<!--${encodedText}-->${beforeDocument}`; | ||
} | ||
while (previousNode !== null) { | ||
if (previousNode === node.ownerDocument.documentElement) { | ||
isAfterDocument = true; | ||
break; | ||
} | ||
previousNode = previousNode.previousSibling; | ||
/** | ||
* Perform canonicalization of the given node | ||
* | ||
* @param {Node} node | ||
* @return {String} | ||
* @api public | ||
*/ | ||
process(node, options) { | ||
options = options || {}; | ||
const defaultNs = options.defaultNs || ""; | ||
const defaultNsForPrefix = options.defaultNsForPrefix || {}; | ||
const ancestorNamespaces = options.ancestorNamespaces || []; | ||
const prefixesInScope = []; | ||
for (let i = 0; i < ancestorNamespaces.length; i++) { | ||
prefixesInScope.push(ancestorNamespaces[i].prefix); | ||
} | ||
const res = this.processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces); | ||
return res; | ||
} | ||
} | ||
return ( | ||
(isAfterDocument ? "\n" : "") + | ||
"<!--" + | ||
utils.encodeSpecialCharactersInText(node.data) + | ||
"-->" + | ||
(isBeforeDocument ? "\n" : "") | ||
); | ||
}; | ||
getAlgorithmName() { | ||
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; | ||
} | ||
} | ||
exports.C14nCanonicalization = C14nCanonicalization; | ||
/** | ||
* Perform canonicalization of the given node | ||
* | ||
* @param {Node} node | ||
* @return {String} | ||
* @api public | ||
* Add c14n#WithComments here (very simple subclass) | ||
*/ | ||
C14nCanonicalization.prototype.process = function (node, options) { | ||
options = options || {}; | ||
var defaultNs = options.defaultNs || ""; | ||
var defaultNsForPrefix = options.defaultNsForPrefix || {}; | ||
var ancestorNamespaces = options.ancestorNamespaces || []; | ||
var prefixesInScope = []; | ||
for (var i = 0; i < ancestorNamespaces.length; i++) { | ||
prefixesInScope.push(ancestorNamespaces[i].prefix); | ||
} | ||
var res = this.processInner( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
ancestorNamespaces | ||
); | ||
return res; | ||
}; | ||
C14nCanonicalization.prototype.getAlgorithmName = function () { | ||
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; | ||
}; | ||
// Add c14n#WithComments here (very simple subclass) | ||
class C14nCanonicalizationWithComments extends C14nCanonicalization { | ||
constructor() { | ||
super(); | ||
this.includeComments = true; | ||
} | ||
getAlgorithmName() { | ||
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"; | ||
} | ||
} | ||
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments; | ||
function C14nCanonicalizationWithComments() { | ||
C14nCanonicalization.call(this); | ||
this.includeComments = true; | ||
} | ||
C14nCanonicalizationWithComments.prototype = Object.create(C14nCanonicalization.prototype); | ||
C14nCanonicalizationWithComments.prototype.constructor = C14nCanonicalizationWithComments; | ||
C14nCanonicalizationWithComments.prototype.getAlgorithmName = function () { | ||
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"; | ||
}; | ||
//# sourceMappingURL=c14n-canonicalization.js.map |
@@ -1,43 +0,41 @@ | ||
var xpath = require("xpath"); | ||
var utils = require("./utils"); | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.EnvelopedSignature = void 0; | ||
const xpath = require("xpath"); | ||
class EnvelopedSignature { | ||
constructor() { | ||
this.includeComments = false; | ||
} | ||
process(node, options) { | ||
if (null == options.signatureNode) { | ||
const signature = xpath.select1("./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", node); | ||
if (xpath.isNodeLike(signature) && signature.parentNode) { | ||
signature.parentNode.removeChild(signature); | ||
} | ||
return node; | ||
} | ||
const signatureNode = options.signatureNode; | ||
const expectedSignatureValue = xpath.select1(".//*[local-name(.)='SignatureValue']/text()", signatureNode); | ||
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() { | ||
return "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; | ||
} | ||
} | ||
exports.EnvelopedSignature = EnvelopedSignature; | ||
function EnvelopedSignature() {} | ||
EnvelopedSignature.prototype.process = function (node, options) { | ||
if (null == options.signatureNode) { | ||
// leave this for the moment... | ||
var signature = xpath.select( | ||
"./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", | ||
node | ||
)[0]; | ||
if (signature) signature.parentNode.removeChild(signature); | ||
return node; | ||
} | ||
var signatureNode = options.signatureNode; | ||
var expectedSignatureValue = utils.findFirst( | ||
signatureNode, | ||
".//*[local-name(.)='SignatureValue']/text()" | ||
).data; | ||
var signatures = xpath.select( | ||
".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", | ||
node | ||
); | ||
for (var h in signatures) { | ||
if (!signatures.hasOwnProperty(h)) continue; | ||
var nodeSignature = signatures[h]; | ||
var signatureValue = utils.findFirst( | ||
nodeSignature, | ||
".//*[local-name(.)='SignatureValue']/text()" | ||
).data; | ||
if (expectedSignatureValue === signatureValue) { | ||
nodeSignature.parentNode.removeChild(nodeSignature); | ||
} | ||
} | ||
return node; | ||
}; | ||
EnvelopedSignature.prototype.getAlgorithmName = function () { | ||
return "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; | ||
}; | ||
//# sourceMappingURL=enveloped-signature.js.map |
@@ -1,348 +0,241 @@ | ||
/* jshint laxcomma: true */ | ||
var utils = require("./utils"); | ||
exports.ExclusiveCanonicalization = ExclusiveCanonicalization; | ||
exports.ExclusiveCanonicalizationWithComments = ExclusiveCanonicalizationWithComments; | ||
function ExclusiveCanonicalization() { | ||
this.includeComments = false; | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ExclusiveCanonicalizationWithComments = exports.ExclusiveCanonicalization = void 0; | ||
const utils = require("./utils"); | ||
const xpath = require("xpath"); | ||
function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { | ||
let ret = false; | ||
prefixesInScope.forEach(function (pf) { | ||
if (pf.prefix === prefix && pf.namespaceURI === namespaceURI) { | ||
ret = true; | ||
} | ||
}); | ||
return ret; | ||
} | ||
ExclusiveCanonicalization.prototype.attrCompare = function (a, b) { | ||
if (!a.namespaceURI && b.namespaceURI) { | ||
return -1; | ||
} | ||
if (!b.namespaceURI && a.namespaceURI) { | ||
return 1; | ||
} | ||
var left = a.namespaceURI + a.localName; | ||
var right = b.namespaceURI + b.localName; | ||
if (left === right) return 0; | ||
else if (left < right) return -1; | ||
else return 1; | ||
}; | ||
ExclusiveCanonicalization.prototype.nsCompare = function (a, b) { | ||
var attr1 = a.prefix; | ||
var attr2 = b.prefix; | ||
if (attr1 == attr2) { | ||
return 0; | ||
} | ||
return attr1.localeCompare(attr2); | ||
}; | ||
ExclusiveCanonicalization.prototype.renderAttrs = function (node, defaultNS) { | ||
var a, | ||
i, | ||
attr, | ||
res = [], | ||
attrListToRender = []; | ||
if (node.nodeType === 8) { | ||
return this.renderComment(node); | ||
} | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//ignore namespace definition attributes | ||
if (attr.name.indexOf("xmlns") === 0) { | ||
continue; | ||
} | ||
attrListToRender.push(attr); | ||
class ExclusiveCanonicalization { | ||
constructor() { | ||
this.includeComments = false; | ||
} | ||
} | ||
attrListToRender.sort(this.attrCompare); | ||
for (a in attrListToRender) { | ||
if (!attrListToRender.hasOwnProperty(a)) { | ||
continue; | ||
attrCompare(a, b) { | ||
if (!a.namespaceURI && b.namespaceURI) { | ||
return -1; | ||
} | ||
if (!b.namespaceURI && a.namespaceURI) { | ||
return 1; | ||
} | ||
const left = a.namespaceURI + a.localName; | ||
const right = b.namespaceURI + b.localName; | ||
if (left === right) { | ||
return 0; | ||
} | ||
else if (left < right) { | ||
return -1; | ||
} | ||
else { | ||
return 1; | ||
} | ||
} | ||
attr = attrListToRender[a]; | ||
res.push(" ", attr.name, '="', utils.encodeSpecialCharactersInAttribute(attr.value), '"'); | ||
} | ||
return res.join(""); | ||
}; | ||
function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { | ||
var ret = false; | ||
prefixesInScope.forEach(function (pf) { | ||
if (pf.prefix === prefix && pf.namespaceURI === namespaceURI) { | ||
ret = true; | ||
nsCompare(a, b) { | ||
const attr1 = a.prefix; | ||
const attr2 = b.prefix; | ||
if (attr1 === attr2) { | ||
return 0; | ||
} | ||
return attr1.localeCompare(attr2); | ||
} | ||
}); | ||
return ret; | ||
} | ||
/** | ||
* 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 | ||
* @return {String} | ||
* @api private | ||
*/ | ||
ExclusiveCanonicalization.prototype.renderNs = function ( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
inclusiveNamespacesPrefixList | ||
) { | ||
var a, | ||
i, | ||
p, | ||
attr, | ||
res = [], | ||
newDefaultNs = defaultNs, | ||
nsListToRender = [], | ||
currNs = node.namespaceURI || ""; | ||
//handle the namespaceof the node itself | ||
if (node.prefix) { | ||
if ( | ||
!isPrefixInScope( | ||
prefixesInScope, | ||
node.prefix, | ||
node.namespaceURI || defaultNsForPrefix[node.prefix] | ||
) | ||
) { | ||
nsListToRender.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
prefixesInScope.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
renderAttrs(node) { | ||
let i; | ||
let attr; | ||
const res = []; | ||
const attrListToRender = []; | ||
if (xpath.isComment(node)) { | ||
return this.renderComment(node); | ||
} | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//ignore namespace definition attributes | ||
if (attr.name.indexOf("xmlns") === 0) { | ||
continue; | ||
} | ||
attrListToRender.push(attr); | ||
} | ||
} | ||
attrListToRender.sort(this.attrCompare); | ||
for (attr of attrListToRender) { | ||
res.push(" ", attr.name, '="', utils.encodeSpecialCharactersInAttribute(attr.value), '"'); | ||
} | ||
return res.join(""); | ||
} | ||
} else if (defaultNs != currNs) { | ||
//new default ns | ||
newDefaultNs = node.namespaceURI; | ||
res.push(' xmlns="', newDefaultNs, '"'); | ||
} | ||
//handle the attributes namespace | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//handle all prefixed attributes that are included in the prefix list and where | ||
//the prefix is not defined already | ||
if ( | ||
attr.prefix && | ||
!isPrefixInScope(prefixesInScope, attr.localName, attr.value) && | ||
inclusiveNamespacesPrefixList.indexOf(attr.localName) >= 0 | ||
) { | ||
nsListToRender.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
prefixesInScope.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
} | ||
//handle all prefixed attributes that are not xmlns definitions and where | ||
//the prefix is not defined already | ||
if ( | ||
attr.prefix && | ||
!isPrefixInScope(prefixesInScope, attr.prefix, attr.namespaceURI) && | ||
attr.prefix != "xmlns" && | ||
attr.prefix != "xml" | ||
) { | ||
nsListToRender.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
prefixesInScope.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
} | ||
/** | ||
* 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 | ||
* @return {String} | ||
* @api private | ||
*/ | ||
renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList) { | ||
let i; | ||
let attr; | ||
const res = []; | ||
let newDefaultNs = defaultNs; | ||
const nsListToRender = []; | ||
const currNs = node.namespaceURI || ""; | ||
//handle the namespaceof the node itself | ||
if (node.prefix) { | ||
if (!isPrefixInScope(prefixesInScope, node.prefix, node.namespaceURI || defaultNsForPrefix[node.prefix])) { | ||
nsListToRender.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
prefixesInScope.push({ | ||
prefix: node.prefix, | ||
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], | ||
}); | ||
} | ||
} | ||
else if (defaultNs !== currNs) { | ||
//new default ns | ||
newDefaultNs = node.namespaceURI; | ||
res.push(' xmlns="', newDefaultNs, '"'); | ||
} | ||
//handle the attributes namespace | ||
if (node.attributes) { | ||
for (i = 0; i < node.attributes.length; ++i) { | ||
attr = node.attributes[i]; | ||
//handle all prefixed attributes that are included in the prefix list and where | ||
//the prefix is not defined already | ||
if (attr.prefix && | ||
!isPrefixInScope(prefixesInScope, attr.localName, attr.value) && | ||
inclusiveNamespacesPrefixList.indexOf(attr.localName) >= 0) { | ||
nsListToRender.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
prefixesInScope.push({ prefix: attr.localName, namespaceURI: attr.value }); | ||
} | ||
//handle all prefixed attributes that are not xmlns definitions and where | ||
//the prefix is not defined already | ||
if (attr.prefix && | ||
!isPrefixInScope(prefixesInScope, attr.prefix, attr.namespaceURI) && | ||
attr.prefix !== "xmlns" && | ||
attr.prefix !== "xml") { | ||
nsListToRender.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
prefixesInScope.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI }); | ||
} | ||
} | ||
} | ||
nsListToRender.sort(this.nsCompare); | ||
//render namespaces | ||
for (const p of nsListToRender) { | ||
res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"'); | ||
} | ||
return { rendered: res.join(""), newDefaultNs: newDefaultNs }; | ||
} | ||
} | ||
nsListToRender.sort(this.nsCompare); | ||
//render namespaces | ||
for (a in nsListToRender) { | ||
if (!nsListToRender.hasOwnProperty(a)) { | ||
continue; | ||
processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList) { | ||
if (xpath.isComment(node)) { | ||
return this.renderComment(node); | ||
} | ||
if (node.data) { | ||
return utils.encodeSpecialCharactersInText(node.data); | ||
} | ||
let i; | ||
let pfxCopy; | ||
const ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList); | ||
const res = ["<", node.tagName, ns.rendered, this.renderAttrs(node), ">"]; | ||
for (i = 0; i < node.childNodes.length; ++i) { | ||
pfxCopy = prefixesInScope.slice(0); | ||
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList)); | ||
} | ||
res.push("</", node.tagName, ">"); | ||
return res.join(""); | ||
} | ||
p = nsListToRender[a]; | ||
res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"'); | ||
} | ||
return { rendered: res.join(""), newDefaultNs: newDefaultNs }; | ||
}; | ||
ExclusiveCanonicalization.prototype.processInner = function ( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
inclusiveNamespacesPrefixList | ||
) { | ||
if (node.nodeType === 8) { | ||
return this.renderComment(node); | ||
} | ||
if (node.data) { | ||
return utils.encodeSpecialCharactersInText(node.data); | ||
} | ||
var i, | ||
pfxCopy, | ||
ns = this.renderNs( | ||
node, | ||
prefixesInScope, | ||
defaultNs, | ||
defaultNsForPrefix, | ||
inclusiveNamespacesPrefixList | ||
), | ||
res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"]; | ||
for (i = 0; i < node.childNodes.length; ++i) { | ||
pfxCopy = prefixesInScope.slice(0); | ||
res.push( | ||
this.processInner( | ||
node.childNodes[i], | ||
pfxCopy, | ||
ns.newDefaultNs, | ||
defaultNsForPrefix, | ||
inclusiveNamespacesPrefixList | ||
) | ||
); | ||
} | ||
res.push("</", node.tagName, ">"); | ||
return res.join(""); | ||
}; | ||
// Thanks to deoxxa/xml-c14n for comment renderer | ||
ExclusiveCanonicalization.prototype.renderComment = function (node) { | ||
if (!this.includeComments) { | ||
return ""; | ||
} | ||
var isOutsideDocument = node.ownerDocument === node.parentNode, | ||
isBeforeDocument = null, | ||
isAfterDocument = null; | ||
if (isOutsideDocument) { | ||
var nextNode = node, | ||
previousNode = node; | ||
while (nextNode !== null) { | ||
if (nextNode === node.ownerDocument.documentElement) { | ||
isBeforeDocument = true; | ||
break; | ||
} | ||
nextNode = nextNode.nextSibling; | ||
// Thanks to deoxxa/xml-c14n for comment renderer | ||
renderComment(node) { | ||
if (!this.includeComments) { | ||
return ""; | ||
} | ||
const isOutsideDocument = node.ownerDocument === node.parentNode; | ||
let isBeforeDocument = false; | ||
let isAfterDocument = false; | ||
if (isOutsideDocument) { | ||
let nextNode = node; | ||
let previousNode = node; | ||
while (nextNode != null) { | ||
if (nextNode === node.ownerDocument.documentElement) { | ||
isBeforeDocument = true; | ||
break; | ||
} | ||
nextNode = nextNode.nextSibling; | ||
} | ||
while (previousNode != null) { | ||
if (previousNode === node.ownerDocument.documentElement) { | ||
isAfterDocument = true; | ||
break; | ||
} | ||
previousNode = previousNode.previousSibling; | ||
} | ||
} | ||
const afterDocument = isAfterDocument ? "\n" : ""; | ||
const beforeDocument = isBeforeDocument ? "\n" : ""; | ||
const encodedText = utils.encodeSpecialCharactersInText(node.data); | ||
return `${afterDocument}<!--${encodedText}-->${beforeDocument}`; | ||
} | ||
while (previousNode !== null) { | ||
if (previousNode === node.ownerDocument.documentElement) { | ||
isAfterDocument = true; | ||
break; | ||
} | ||
previousNode = previousNode.previousSibling; | ||
/** | ||
* Perform canonicalization of the given node | ||
* | ||
* @param {Node} node | ||
* @return {String} | ||
* @api public | ||
*/ | ||
process(node, options) { | ||
options = options || {}; | ||
let inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; | ||
const defaultNs = options.defaultNs || ""; | ||
const defaultNsForPrefix = options.defaultNsForPrefix || {}; | ||
const ancestorNamespaces = options.ancestorNamespaces || []; | ||
/** | ||
* If the inclusiveNamespacesPrefixList has not been explicitly provided then look it up in CanonicalizationMethod/InclusiveNamespaces | ||
*/ | ||
if (!utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { | ||
const CanonicalizationMethod = utils.findChilds(node, "CanonicalizationMethod"); | ||
if (CanonicalizationMethod.length !== 0) { | ||
const inclusiveNamespaces = utils.findChilds(CanonicalizationMethod[0], "InclusiveNamespaces"); | ||
if (inclusiveNamespaces.length !== 0) { | ||
inclusiveNamespacesPrefixList = (inclusiveNamespaces[0].getAttribute("PrefixList") || "").split(" "); | ||
} | ||
} | ||
} | ||
/** | ||
* If you have a PrefixList then use it and the ancestors to add the necessary namespaces | ||
*/ | ||
if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { | ||
inclusiveNamespacesPrefixList.forEach(function (prefix) { | ||
if (ancestorNamespaces) { | ||
ancestorNamespaces.forEach(function (ancestorNamespace) { | ||
if (prefix === ancestorNamespace.prefix) { | ||
node.setAttributeNS("http://www.w3.org/2000/xmlns/", `xmlns:${prefix}`, ancestorNamespace.namespaceURI); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
const res = this.processInner(node, [], defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList); | ||
return res; | ||
} | ||
} | ||
return ( | ||
(isAfterDocument ? "\n" : "") + | ||
"<!--" + | ||
utils.encodeSpecialCharactersInText(node.data) + | ||
"-->" + | ||
(isBeforeDocument ? "\n" : "") | ||
); | ||
}; | ||
/** | ||
* Perform canonicalization of the given node | ||
* | ||
* @param {Node} node | ||
* @return {String} | ||
* @api public | ||
*/ | ||
ExclusiveCanonicalization.prototype.process = function (node, options) { | ||
options = options || {}; | ||
var inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; | ||
var defaultNs = options.defaultNs || ""; | ||
var defaultNsForPrefix = options.defaultNsForPrefix || {}; | ||
if (!(inclusiveNamespacesPrefixList instanceof Array)) { | ||
inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList.split(" "); | ||
} | ||
var ancestorNamespaces = options.ancestorNamespaces || []; | ||
/** | ||
* If the inclusiveNamespacesPrefixList has not been explicitly provided then look it up in CanonicalizationMethod/InclusiveNamespaces | ||
*/ | ||
if (inclusiveNamespacesPrefixList.length == 0) { | ||
var CanonicalizationMethod = utils.findChilds(node, "CanonicalizationMethod"); | ||
if (CanonicalizationMethod.length != 0) { | ||
var inclusiveNamespaces = utils.findChilds(CanonicalizationMethod[0], "InclusiveNamespaces"); | ||
if (inclusiveNamespaces.length != 0) { | ||
inclusiveNamespacesPrefixList = inclusiveNamespaces[0] | ||
.getAttribute("PrefixList") | ||
.split(" "); | ||
} | ||
getAlgorithmName() { | ||
return "http://www.w3.org/2001/10/xml-exc-c14n#"; | ||
} | ||
} | ||
/** | ||
* If you have a PrefixList then use it and the ancestors to add the necessary namespaces | ||
*/ | ||
if (inclusiveNamespacesPrefixList) { | ||
var prefixList = | ||
inclusiveNamespacesPrefixList instanceof Array | ||
? inclusiveNamespacesPrefixList | ||
: inclusiveNamespacesPrefixList.split(" "); | ||
prefixList.forEach(function (prefix) { | ||
if (ancestorNamespaces) { | ||
ancestorNamespaces.forEach(function (ancestorNamespace) { | ||
if (prefix == ancestorNamespace.prefix) { | ||
node.setAttributeNS( | ||
"http://www.w3.org/2000/xmlns/", | ||
"xmlns:" + prefix, | ||
ancestorNamespace.namespaceURI | ||
); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
var res = this.processInner( | ||
node, | ||
[], | ||
defaultNs, | ||
defaultNsForPrefix, | ||
inclusiveNamespacesPrefixList | ||
); | ||
return res; | ||
}; | ||
ExclusiveCanonicalization.prototype.getAlgorithmName = function () { | ||
return "http://www.w3.org/2001/10/xml-exc-c14n#"; | ||
}; | ||
// Add c14n#WithComments here (very simple subclass) | ||
} | ||
exports.ExclusiveCanonicalization = ExclusiveCanonicalization; | ||
class ExclusiveCanonicalizationWithComments extends ExclusiveCanonicalization { | ||
constructor() { | ||
super(); | ||
this.includeComments = true; | ||
} | ||
getAlgorithmName() { | ||
return "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; | ||
} | ||
} | ||
exports.ExclusiveCanonicalizationWithComments = ExclusiveCanonicalizationWithComments; | ||
function ExclusiveCanonicalizationWithComments() { | ||
ExclusiveCanonicalization.call(this); | ||
this.includeComments = true; | ||
} | ||
ExclusiveCanonicalizationWithComments.prototype = Object.create( | ||
ExclusiveCanonicalization.prototype | ||
); | ||
ExclusiveCanonicalizationWithComments.prototype.constructor = ExclusiveCanonicalizationWithComments; | ||
ExclusiveCanonicalizationWithComments.prototype.getAlgorithmName = function () { | ||
return "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; | ||
}; | ||
//# sourceMappingURL=exclusive-canonicalization.js.map |
@@ -1,1134 +0,797 @@ | ||
var xpath = require("xpath"), | ||
Dom = require("@xmldom/xmldom").DOMParser, | ||
utils = require("./utils"), | ||
c14n = require("./c14n-canonicalization"), | ||
execC14n = require("./exclusive-canonicalization"), | ||
EnvelopedSignature = require("./enveloped-signature").EnvelopedSignature, | ||
StringKeyInfo = require("./string-key-info"), | ||
FileKeyInfo = require("./file-key-info"), | ||
crypto = require("crypto"); | ||
exports.SignedXml = SignedXml; | ||
exports.StringKeyInfo = StringKeyInfo; | ||
exports.FileKeyInfo = FileKeyInfo; | ||
/** | ||
* Hash algorithm implementation | ||
* | ||
*/ | ||
function SHA1() { | ||
this.getHash = function (xml) { | ||
var shasum = crypto.createHash("sha1"); | ||
shasum.update(xml, "utf8"); | ||
var res = shasum.digest("base64"); | ||
return res; | ||
}; | ||
this.getAlgorithmName = function () { | ||
return "http://www.w3.org/2000/09/xmldsig#sha1"; | ||
}; | ||
} | ||
function SHA256() { | ||
this.getHash = function (xml) { | ||
var shasum = crypto.createHash("sha256"); | ||
shasum.update(xml, "utf8"); | ||
var res = shasum.digest("base64"); | ||
return res; | ||
}; | ||
this.getAlgorithmName = function () { | ||
return "http://www.w3.org/2001/04/xmlenc#sha256"; | ||
}; | ||
} | ||
function SHA512() { | ||
this.getHash = function (xml) { | ||
var shasum = crypto.createHash("sha512"); | ||
shasum.update(xml, "utf8"); | ||
var 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, signingKey, callback) { | ||
var signer = crypto.createSign("RSA-SHA1"); | ||
signer.update(signedInfo); | ||
var res = signer.sign(signingKey, "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) { | ||
var verifier = crypto.createVerify("RSA-SHA1"); | ||
verifier.update(str); | ||
var 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, signingKey, callback) { | ||
var signer = crypto.createSign("RSA-SHA256"); | ||
signer.update(signedInfo); | ||
var res = signer.sign(signingKey, "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) { | ||
var verifier = crypto.createVerify("RSA-SHA256"); | ||
verifier.update(str); | ||
var 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, signingKey, callback) { | ||
var signer = crypto.createSign("RSA-SHA512"); | ||
signer.update(signedInfo); | ||
var res = signer.sign(signingKey, "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) { | ||
var verifier = crypto.createVerify("RSA-SHA512"); | ||
verifier.update(str); | ||
var 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) { | ||
var verifier = crypto.createHmac("SHA1", key); | ||
verifier.update(str); | ||
var res = verifier.digest("base64"); | ||
return res === signatureValue; | ||
}; | ||
this.getAlgorithmName = function () { | ||
return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; | ||
}; | ||
this.getSignature = function (signedInfo, signingKey) { | ||
var verifier = crypto.createHmac("SHA1", signingKey); | ||
verifier.update(signedInfo); | ||
var res = verifier.digest("base64"); | ||
return res; | ||
}; | ||
} | ||
/** | ||
* 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) { | ||
var docSubset = xpath.selectWithResolver(docSubsetXpath, doc, namespaceResolver); | ||
if (!Array.isArray(docSubset) || docSubset.length < 1) { | ||
return []; | ||
} | ||
// Remove duplicate on ancestor namespace | ||
var ancestorNs = collectAncestorNamespaces(docSubset[0]); | ||
var ancestorNsWithoutDuplicate = []; | ||
for (var i = 0; i < ancestorNs.length; i++) { | ||
var notOnTheList = true; | ||
for (var v in ancestorNsWithoutDuplicate) { | ||
if (ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix) { | ||
notOnTheList = false; | ||
break; | ||
} | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.SignedXml = void 0; | ||
const xpath = require("xpath"); | ||
const xmldom_1 = require("@xmldom/xmldom"); | ||
const utils = require("./utils"); | ||
const c14n = require("./c14n-canonicalization"); | ||
const execC14n = require("./exclusive-canonicalization"); | ||
const envelopedSignatures = require("./enveloped-signature"); | ||
const hashAlgorithms = require("./hash-algorithms"); | ||
const signatureAlgorithms = require("./signature-algorithms"); | ||
class SignedXml { | ||
/** | ||
* The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using | ||
* @param options {@link SignedXmlOptions} | ||
*/ | ||
constructor(options = {}) { | ||
/** | ||
* One of the supported signature algorithms. See {@link SignatureAlgorithmType} | ||
*/ | ||
this.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; | ||
/** | ||
* Rules used to convert an XML document into its canonical form. | ||
*/ | ||
this.canonicalizationAlgorithm = "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. | ||
*/ | ||
this.inclusiveNamespacesPrefixList = []; | ||
this.namespaceResolver = { | ||
lookupNamespaceURI: function ( /* prefix */) { | ||
throw new Error("Not implemented"); | ||
}, | ||
}; | ||
this.implicitTransforms = []; | ||
this.keyInfoAttributes = {}; | ||
this.getKeyInfoContent = SignedXml.getKeyInfoContent; | ||
this.getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; | ||
// Internal state | ||
/** | ||
* Specifies the data to be signed within an XML document. See {@link Reference} | ||
*/ | ||
this.references = []; | ||
this.id = 0; | ||
this.signedXml = ""; | ||
this.signatureXml = ""; | ||
this.signatureNode = null; | ||
this.signatureValue = ""; | ||
this.originalXmlWithIds = ""; | ||
/** | ||
* Contains validation errors (if any) after {@link checkSignature} method is called | ||
*/ | ||
this.validationErrors = []; | ||
this.keyInfo = 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} | ||
*/ | ||
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": 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} | ||
*/ | ||
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, | ||
}; | ||
/** | ||
* 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} | ||
*/ | ||
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 | ||
}; | ||
const { idMode, idAttribute, privateKey, publicCert, signatureAlgorithm, canonicalizationAlgorithm, inclusiveNamespacesPrefixList, implicitTransforms, keyInfoAttributes, getKeyInfoContent, getCertFromKeyInfo, } = options; | ||
// Options | ||
this.idMode = idMode; | ||
this.idAttributes = ["Id", "ID", "id"]; | ||
if (idAttribute) { | ||
this.idAttributes.unshift(idAttribute); | ||
} | ||
this.privateKey = privateKey; | ||
this.publicCert = publicCert; | ||
this.signatureAlgorithm = signatureAlgorithm !== null && signatureAlgorithm !== void 0 ? signatureAlgorithm : this.signatureAlgorithm; | ||
this.canonicalizationAlgorithm = canonicalizationAlgorithm !== null && canonicalizationAlgorithm !== void 0 ? canonicalizationAlgorithm : this.canonicalizationAlgorithm; | ||
if (typeof inclusiveNamespacesPrefixList === "string") { | ||
this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList.split(" "); | ||
} | ||
else if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { | ||
this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList; | ||
} | ||
this.implicitTransforms = implicitTransforms !== null && implicitTransforms !== void 0 ? implicitTransforms : this.implicitTransforms; | ||
this.keyInfoAttributes = keyInfoAttributes !== null && keyInfoAttributes !== void 0 ? keyInfoAttributes : this.keyInfoAttributes; | ||
this.getKeyInfoContent = getKeyInfoContent !== null && getKeyInfoContent !== void 0 ? getKeyInfoContent : this.getKeyInfoContent; | ||
this.getCertFromKeyInfo = getCertFromKeyInfo !== null && getCertFromKeyInfo !== void 0 ? getCertFromKeyInfo : this.getCertFromKeyInfo; | ||
this.CanonicalizationAlgorithms; | ||
this.HashAlgorithms; | ||
this.SignatureAlgorithms; | ||
} | ||
if (notOnTheList) { | ||
ancestorNsWithoutDuplicate.push(ancestorNs[i]); | ||
/** | ||
* Due to key-confusion issues, it's risky to have both hmac | ||
* and digital signature algorithms enabled at the same time. | ||
* This enables HMAC and disables other signing algorithms. | ||
*/ | ||
enableHMAC() { | ||
this.SignatureAlgorithms = { | ||
"http://www.w3.org/2000/09/xmldsig#hmac-sha1": signatureAlgorithms.HmacSha1, | ||
}; | ||
this.getKeyInfoContent = () => null; | ||
} | ||
} | ||
// Remove namespaces which are already declared in the subset with the same prefix | ||
var returningNs = []; | ||
const subsetNsPrefix = findNSPrefix(docSubset[0]); | ||
for (const ancestorNs of ancestorNsWithoutDuplicate) { | ||
if (ancestorNs.prefix !== subsetNsPrefix) { | ||
returningNs.push(ancestorNs); | ||
/** | ||
* 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 "<foo:X509Data></foo:X509Data>" | ||
* | ||
* @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 }) { | ||
if (publicCert == null) { | ||
return null; | ||
} | ||
prefix = prefix ? `${prefix}:` : ""; | ||
let x509Certs = ""; | ||
if (Buffer.isBuffer(publicCert)) { | ||
publicCert = publicCert.toString("latin1"); | ||
} | ||
let publicCertMatches = []; | ||
if (typeof publicCert === "string") { | ||
publicCertMatches = publicCert.match(utils.EXTRACT_X509_CERTS) || []; | ||
} | ||
if (publicCertMatches.length > 0) { | ||
x509Certs = publicCertMatches | ||
.map((c) => `<X509Certificate>${utils.pemToDer(c)}</X509Certificate>`) | ||
.join(""); | ||
} | ||
return `<${prefix}X509Data>${x509Certs}</${prefix}X509Data>`; | ||
} | ||
} | ||
return returningNs; | ||
} | ||
function collectAncestorNamespaces(node, nsArray) { | ||
if (!nsArray) { | ||
nsArray = []; | ||
} | ||
var parent = node.parentNode; | ||
if (!parent) { | ||
return nsArray; | ||
} | ||
if (parent.attributes && parent.attributes.length > 0) { | ||
for (var i = 0; i < parent.attributes.length; i++) { | ||
var attr = parent.attributes[i]; | ||
if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:?/) !== -1) { | ||
nsArray.push({ | ||
prefix: attr.nodeName.replace(/^xmlns:?/, ""), | ||
namespaceURI: attr.nodeValue, | ||
}); | ||
} | ||
/** | ||
* 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) { | ||
if (keyInfo != null) { | ||
const certs = xpath.select1(".//*[local-name(.)='X509Certificate']", keyInfo); | ||
if (xpath.isNodeLike(certs)) { | ||
return utils.derToPem(certs.textContent || "", "CERTIFICATE"); | ||
} | ||
} | ||
return null; | ||
} | ||
} | ||
return collectAncestorNamespaces(parent, nsArray); | ||
} | ||
function findNSPrefix(subset) { | ||
const subsetAttributes = subset.attributes; | ||
for (let k = 0; k < subsetAttributes.length; k++) { | ||
const nodeName = subsetAttributes[k].nodeName; | ||
if (nodeName.search(/^xmlns:?/) !== -1) { | ||
return nodeName.replace(/^xmlns:?/, ""); | ||
checkSignature(xml, callback) { | ||
if (callback != null && typeof callback !== "function") { | ||
throw new Error("Last parameter must be a callback function"); | ||
} | ||
this.validationErrors = []; | ||
this.signedXml = xml; | ||
const doc = new xmldom_1.DOMParser().parseFromString(xml); | ||
if (!this.validateReferences(doc)) { | ||
if (!callback) { | ||
return false; | ||
} | ||
else { | ||
callback(new Error("Could not validate references")); | ||
return; | ||
} | ||
} | ||
if (!callback) { | ||
// Synchronous flow | ||
if (!this.validateSignatureValue(doc)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
else { | ||
// Asynchronous flow | ||
this.validateSignatureValue(doc, (err, isValidSignature) => { | ||
if (err) { | ||
this.validationErrors.push(`invalid signature: the signature value ${this.signatureValue} is incorrect`); | ||
callback(err); | ||
} | ||
else { | ||
callback(null, isValidSignature); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
return subset.prefix || ""; | ||
} | ||
/** | ||
* Xml signature implementation | ||
* | ||
* @param {string} idMode. Value of "wssecurity" will create/validate id's with the ws-security namespace | ||
* @param {object} options. Initial configurations | ||
*/ | ||
function SignedXml(idMode, options) { | ||
this.options = options || {}; | ||
this.idMode = idMode; | ||
this.references = []; | ||
this.id = 0; | ||
this.signingKey = null; | ||
this.signingCert = null; | ||
this.signatureAlgorithm = | ||
this.options.signatureAlgorithm || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; | ||
this.keyInfoProvider = null; | ||
this.canonicalizationAlgorithm = | ||
this.options.canonicalizationAlgorithm || "http://www.w3.org/2001/10/xml-exc-c14n#"; | ||
this.inclusiveNamespacesPrefixList = this.options.inclusiveNamespacesPrefixList || ""; | ||
this.signedXml = ""; | ||
this.signatureXml = ""; | ||
this.signatureNode = null; | ||
this.signatureValue = ""; | ||
this.originalXmlWithIds = ""; | ||
this.validationErrors = []; | ||
this.keyInfo = null; | ||
this.idAttributes = ["Id", "ID", "id"]; | ||
if (this.options.idAttribute) this.idAttributes.splice(0, 0, this.options.idAttribute); | ||
this.implicitTransforms = this.options.implicitTransforms || []; | ||
} | ||
SignedXml.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, | ||
}; | ||
SignedXml.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, | ||
}; | ||
SignedXml.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, | ||
// Disabled by default due to key confusion concerns. | ||
// 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': HMACSHA1 | ||
}; | ||
/** | ||
* Due to key-confusion issues, its risky to have both hmac | ||
* and digital signature algos enabled at the same time. | ||
* This enables HMAC and disables other signing algos. | ||
*/ | ||
SignedXml.enableHMAC = function () { | ||
SignedXml.SignatureAlgorithms = { | ||
"http://www.w3.org/2000/09/xmldsig#hmac-sha1": HMACSHA1, | ||
}; | ||
}; | ||
SignedXml.defaultNsForPrefix = { | ||
ds: "http://www.w3.org/2000/09/xmldsig#", | ||
}; | ||
SignedXml.findAncestorNs = findAncestorNs; | ||
SignedXml.prototype.checkSignature = function (xml, callback) { | ||
if (callback != null && typeof callback !== "function") { | ||
throw new Error("Last parameter must be a callback function"); | ||
} | ||
this.validationErrors = []; | ||
this.signedXml = xml; | ||
if (!this.keyInfoProvider) { | ||
var err = new Error("cannot validate signature since no key info resolver was provided"); | ||
if (!callback) { | ||
throw err; | ||
} else { | ||
callback(err); | ||
return; | ||
getCanonSignedInfoXml(doc) { | ||
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"); | ||
} | ||
if (this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" || | ||
this.canonicalizationAlgorithm === | ||
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments") { | ||
if (!doc || typeof doc !== "object") { | ||
throw new Error("When canonicalization method is non-exclusive, whole xml dom must be provided as an argument"); | ||
} | ||
} | ||
/** | ||
* Search for ancestor namespaces before canonicalization. | ||
*/ | ||
const ancestorNamespaces = utils.findAncestorNs(doc, "//*[local-name()='SignedInfo']"); | ||
const c14nOptions = { | ||
ancestorNamespaces: ancestorNamespaces, | ||
}; | ||
return this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0], c14nOptions); | ||
} | ||
} | ||
this.signingKey = this.keyInfoProvider.getKey(this.keyInfo); | ||
if (!this.signingKey) { | ||
var err2 = new Error("key info provider could not resolve key info " + this.keyInfo); | ||
if (!callback) { | ||
throw err2; | ||
} else { | ||
callback(err2); | ||
return; | ||
getCanonReferenceXml(doc, ref, node) { | ||
/** | ||
* Search for ancestor namespaces before canonicalization. | ||
*/ | ||
if (Array.isArray(ref.transforms)) { | ||
ref.ancestorNamespaces = utils.findAncestorNs(doc, ref.xpath, this.namespaceResolver); | ||
} | ||
const c14nOptions = { | ||
inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList, | ||
ancestorNamespaces: ref.ancestorNamespaces, | ||
}; | ||
return this.getCanonXml(ref.transforms, node, c14nOptions); | ||
} | ||
} | ||
var doc = new Dom().parseFromString(xml); | ||
if (!this.validateReferences(doc)) { | ||
if (!callback) { | ||
return false; | ||
} else { | ||
callback(new Error("Could not validate references")); | ||
return; | ||
validateSignatureValue(doc, callback) { | ||
const signedInfoCanon = this.getCanonSignedInfoXml(doc); | ||
const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
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; | ||
} | ||
} | ||
} | ||
if (!callback) { | ||
// Synchronous flow | ||
if (!this.validateSignatureValue(doc)) { | ||
return false; | ||
calculateSignatureValue(doc, callback) { | ||
const signedInfoCanon = this.getCanonSignedInfoXml(doc); | ||
const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
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); | ||
} | ||
} | ||
return true; | ||
} else { | ||
// Asynchronous flow | ||
this.validateSignatureValue(doc, function (err, isValidSignature) { | ||
if (err) { | ||
this.validationErrors.push( | ||
"invalid signature: the signature value " + this.signatureValue + " is incorrect" | ||
); | ||
callback(err); | ||
} else { | ||
callback(null, isValidSignature); | ||
} | ||
}); | ||
} | ||
}; | ||
SignedXml.prototype.getCanonSignedInfoXml = function (doc) { | ||
var signedInfo = utils.findChilds(this.signatureNode, "SignedInfo"); | ||
if (signedInfo.length == 0) throw new Error("could not find SignedInfo element in the message"); | ||
if ( | ||
this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" || | ||
this.canonicalizationAlgorithm === | ||
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" | ||
) { | ||
if (!doc || typeof doc !== "object") { | ||
throw new Error( | ||
"When canonicalization method is non-exclusive, whole xml dom must be provided as an argument" | ||
); | ||
findSignatureAlgorithm(name) { | ||
const algo = this.SignatureAlgorithms[name]; | ||
if (algo) { | ||
return new algo(); | ||
} | ||
else { | ||
throw new Error(`signature algorithm '${name}' is not supported`); | ||
} | ||
} | ||
} | ||
/** | ||
* Search for ancestor namespaces before canonicalization. | ||
*/ | ||
var ancestorNamespaces = []; | ||
ancestorNamespaces = findAncestorNs(doc, "//*[local-name()='SignedInfo']"); | ||
var c14nOptions = { | ||
ancestorNamespaces: ancestorNamespaces, | ||
}; | ||
return this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0], c14nOptions); | ||
}; | ||
SignedXml.prototype.getCanonReferenceXml = function (doc, ref, node) { | ||
/** | ||
* Search for ancestor namespaces before canonicalization. | ||
*/ | ||
if (Array.isArray(ref.transforms)) { | ||
ref.ancestorNamespaces = findAncestorNs(doc, ref.xpath, this.namespaceResolver); | ||
} | ||
var c14nOptions = { | ||
inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList, | ||
ancestorNamespaces: ref.ancestorNamespaces, | ||
}; | ||
return this.getCanonXml(ref.transforms, node, c14nOptions); | ||
}; | ||
SignedXml.prototype.validateSignatureValue = function (doc, callback) { | ||
var signedInfoCanon = this.getCanonSignedInfoXml(doc); | ||
var signer = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
var res = signer.verifySignature(signedInfoCanon, this.signingKey, this.signatureValue, callback); | ||
if (!res && !callback) | ||
this.validationErrors.push( | ||
"invalid signature: the signature value " + this.signatureValue + " is incorrect" | ||
); | ||
return res; | ||
}; | ||
SignedXml.prototype.calculateSignatureValue = function (doc, callback) { | ||
var signedInfoCanon = this.getCanonSignedInfoXml(doc); | ||
var signer = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
this.signatureValue = signer.getSignature(signedInfoCanon, this.signingKey, callback); | ||
}; | ||
SignedXml.prototype.findSignatureAlgorithm = function (name) { | ||
var algo = SignedXml.SignatureAlgorithms[name]; | ||
if (algo) return new algo(); | ||
else throw new Error("signature algorithm '" + name + "' is not supported"); | ||
}; | ||
SignedXml.prototype.findCanonicalizationAlgorithm = function (name) { | ||
var algo = SignedXml.CanonicalizationAlgorithms[name]; | ||
if (algo) return new algo(); | ||
else throw new Error("canonicalization algorithm '" + name + "' is not supported"); | ||
}; | ||
SignedXml.prototype.findHashAlgorithm = function (name) { | ||
var algo = SignedXml.HashAlgorithms[name]; | ||
if (algo) return new algo(); | ||
else throw new Error("hash algorithm '" + name + "' is not supported"); | ||
}; | ||
SignedXml.prototype.validateReferences = function (doc) { | ||
for (var r in this.references) { | ||
if (!this.references.hasOwnProperty(r)) continue; | ||
var ref = this.references[r]; | ||
var uri = ref.uri[0] == "#" ? ref.uri.substring(1) : ref.uri; | ||
var elem = []; | ||
if (uri == "") { | ||
elem = xpath.select("//*", doc); | ||
} else if (uri.indexOf("'") != -1) { | ||
// xpath injection | ||
throw new Error("Cannot validate a uri with quotes inside it"); | ||
} else { | ||
var elemXpath; | ||
var num_elements_for_id = 0; | ||
for (var index in this.idAttributes) { | ||
if (!this.idAttributes.hasOwnProperty(index)) continue; | ||
var tmp_elemXpath = | ||
"//*[@*[local-name(.)='" + this.idAttributes[index] + "']='" + uri + "']"; | ||
var tmp_elem = xpath.select(tmp_elemXpath, doc); | ||
num_elements_for_id += tmp_elem.length; | ||
if (tmp_elem.length > 0) { | ||
elem = tmp_elem; | ||
elemXpath = tmp_elemXpath; | ||
findCanonicalizationAlgorithm(name) { | ||
const algo = this.CanonicalizationAlgorithms[name]; | ||
if (algo) { | ||
return new algo(); | ||
} | ||
} | ||
if (num_elements_for_id > 1) { | ||
throw new Error( | ||
"Cannot validate a document which contains multiple elements with the " + | ||
"same value for the ID / Id / Id attributes, in order to prevent " + | ||
"signature wrapping attack." | ||
); | ||
} | ||
ref.xpath = elemXpath; | ||
else { | ||
throw new Error(`canonicalization algorithm '${name}' is not supported`); | ||
} | ||
} | ||
if (elem.length == 0) { | ||
this.validationErrors.push( | ||
"invalid signature: the signature references an element with uri " + | ||
ref.uri + | ||
" but could not find such element in the xml" | ||
); | ||
return false; | ||
findHashAlgorithm(name) { | ||
const algo = this.HashAlgorithms[name]; | ||
if (algo) { | ||
return new algo(); | ||
} | ||
else { | ||
throw new Error(`hash algorithm '${name}' is not supported`); | ||
} | ||
} | ||
var canonXml = this.getCanonReferenceXml(doc, ref, elem[0]); | ||
var hash = this.findHashAlgorithm(ref.digestAlgorithm); | ||
var digest = hash.getHash(canonXml); | ||
if (!validateDigestValue(digest, ref.digestValue)) { | ||
this.validationErrors.push( | ||
"invalid signature: for uri " + | ||
ref.uri + | ||
" calculated digest is " + | ||
digest + | ||
" but the xml to validate supplies digest " + | ||
ref.digestValue | ||
); | ||
return false; | ||
validateReferences(doc) { | ||
var _a; | ||
for (const ref of this.references) { | ||
let elemXpath; | ||
const uri = ((_a = ref.uri) === null || _a === void 0 ? void 0 : _a[0]) === "#" ? ref.uri.substring(1) : ref.uri; | ||
let elem = []; | ||
if (uri === "") { | ||
elem = xpath.select("//*", doc); | ||
} | ||
else if ((uri === null || uri === void 0 ? void 0 : uri.indexOf("'")) !== -1) { | ||
// xpath injection | ||
throw new Error("Cannot validate a uri with quotes inside it"); | ||
} | ||
else { | ||
let num_elements_for_id = 0; | ||
for (const attr of this.idAttributes) { | ||
const tmp_elemXpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; | ||
const tmp_elem = xpath.select(tmp_elemXpath, doc); | ||
if (utils.isArrayHasLength(tmp_elem)) { | ||
num_elements_for_id += tmp_elem.length; | ||
elem = tmp_elem; | ||
elemXpath = tmp_elemXpath; | ||
} | ||
} | ||
if (num_elements_for_id > 1) { | ||
throw new Error("Cannot validate a document which contains multiple elements with the " + | ||
"same value for the ID / Id / Id attributes, in order to prevent " + | ||
"signature wrapping attack."); | ||
} | ||
ref.xpath = elemXpath; | ||
} | ||
// 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} but could not find such element in the xml`); | ||
return false; | ||
} | ||
const canonXml = this.getCanonReferenceXml(doc, ref, elem[0]); | ||
const hash = this.findHashAlgorithm(ref.digestAlgorithm); | ||
const digest = hash.getHash(canonXml); | ||
if (!utils.validateDigestValue(digest, ref.digestValue)) { | ||
this.validationErrors.push(`invalid signature: for uri ${ref.uri} calculated digest is ${digest} but the xml to validate supplies digest ${ref.digestValue}`); | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
} | ||
return true; | ||
}; | ||
function validateDigestValue(digest, expectedDigest) { | ||
var buffer, expectedBuffer; | ||
var 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 (var i = 0; i < buffer.length; i++) { | ||
if (buffer[i] !== expectedBuffer[i]) { | ||
return false; | ||
/** | ||
* Loads the signature information from the provided XML node or string. | ||
* | ||
* @param signatureNode The XML node or string representing the signature. | ||
*/ | ||
loadSignature(signatureNode) { | ||
if (typeof signatureNode === "string") { | ||
this.signatureNode = signatureNode = new xmldom_1.DOMParser().parseFromString(signatureNode); | ||
} | ||
else { | ||
this.signatureNode = signatureNode; | ||
} | ||
this.signatureXml = signatureNode.toString(); | ||
const nodes = xpath.select(".//*[local-name(.)='CanonicalizationMethod']/@Algorithm", signatureNode); | ||
if (!utils.isArrayHasLength(nodes)) { | ||
throw new Error("could not find CanonicalizationMethod/@Algorithm element"); | ||
} | ||
if (xpath.isAttribute(nodes[0])) { | ||
this.canonicalizationAlgorithm = nodes[0].value; | ||
} | ||
const signatureAlgorithm = xpath.select1(".//*[local-name(.)='SignatureMethod']/@Algorithm", signatureNode); | ||
if (xpath.isAttribute(signatureAlgorithm)) { | ||
this.signatureAlgorithm = signatureAlgorithm.value; | ||
} | ||
this.references = []; | ||
const references = xpath.select(".//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference']", signatureNode); | ||
if (!utils.isArrayHasLength(references)) { | ||
throw new Error("could not find any Reference elements"); | ||
} | ||
for (const reference of references) { | ||
this.loadReference(reference); | ||
} | ||
const signatureValue = xpath.select1(".//*[local-name(.)='SignatureValue']/text()", 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; | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
SignedXml.prototype.loadSignature = function (signatureNode) { | ||
if (typeof signatureNode === "string") { | ||
this.signatureNode = signatureNode = new Dom().parseFromString(signatureNode); | ||
} else { | ||
this.signatureNode = signatureNode; | ||
} | ||
this.signatureXml = signatureNode.toString(); | ||
var nodes = xpath.select( | ||
".//*[local-name(.)='CanonicalizationMethod']/@Algorithm", | ||
signatureNode | ||
); | ||
if (nodes.length == 0) | ||
throw new Error("could not find CanonicalizationMethod/@Algorithm element"); | ||
this.canonicalizationAlgorithm = nodes[0].value; | ||
this.signatureAlgorithm = utils.findFirst( | ||
signatureNode, | ||
".//*[local-name(.)='SignatureMethod']/@Algorithm" | ||
).value; | ||
this.references = []; | ||
var references = xpath.select( | ||
".//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference']", | ||
signatureNode | ||
); | ||
if (references.length == 0) throw new Error("could not find any Reference elements"); | ||
for (var i in references) { | ||
if (!references.hasOwnProperty(i)) continue; | ||
this.loadReference(references[i]); | ||
} | ||
this.signatureValue = utils | ||
.findFirst(signatureNode, ".//*[local-name(.)='SignatureValue']/text()") | ||
.data.replace(/\r?\n/g, ""); | ||
this.keyInfo = xpath.select(".//*[local-name(.)='KeyInfo']", signatureNode); | ||
}; | ||
/** | ||
* Load the reference xml node to a model | ||
* | ||
*/ | ||
SignedXml.prototype.loadReference = function (ref) { | ||
var nodes = utils.findChilds(ref, "DigestMethod"); | ||
if (nodes.length == 0) | ||
throw new Error("could not find DigestMethod in reference " + ref.toString()); | ||
var digestAlgoNode = nodes[0]; | ||
var attr = utils.findAttr(digestAlgoNode, "Algorithm"); | ||
if (!attr) | ||
throw new Error("could not find Algorithm attribute in node " + digestAlgoNode.toString()); | ||
var digestAlgo = attr.value; | ||
nodes = utils.findChilds(ref, "DigestValue"); | ||
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) { | ||
throw new Error("could not find the value of DigestValue in " + nodes[0].toString()); | ||
} | ||
var digestValue = nodes[0].firstChild.data; | ||
var transforms = []; | ||
var inclusiveNamespacesPrefixList; | ||
nodes = utils.findChilds(ref, "Transforms"); | ||
if (nodes.length != 0) { | ||
var transformsNode = nodes[0]; | ||
var transformsAll = utils.findChilds(transformsNode, "Transform"); | ||
for (var t in transformsAll) { | ||
if (!transformsAll.hasOwnProperty(t)) continue; | ||
var trans = transformsAll[t]; | ||
transforms.push(utils.findAttr(trans, "Algorithm").value); | ||
} | ||
var 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 (var i = 0; i < inclusiveNamespaces.length; i++) { | ||
if (inclusiveNamespacesPrefixList) { | ||
inclusiveNamespacesPrefixList = | ||
inclusiveNamespacesPrefixList + " " + inclusiveNamespaces[i].getAttribute("PrefixList"); | ||
} else { | ||
inclusiveNamespacesPrefixList = inclusiveNamespaces[i].getAttribute("PrefixList"); | ||
/** | ||
* Load the reference xml node to a model | ||
* | ||
*/ | ||
loadReference(ref) { | ||
var _a; | ||
let nodes = utils.findChilds(ref, "DigestMethod"); | ||
if (nodes.length === 0) { | ||
throw new Error(`could not find DigestMethod in reference ${ref.toString()}`); | ||
} | ||
} | ||
const digestAlgoNode = nodes[0]; | ||
const attr = utils.findAttr(digestAlgoNode, "Algorithm"); | ||
if (!attr) { | ||
throw new Error(`could not find Algorithm attribute in node ${digestAlgoNode.toString()}`); | ||
} | ||
const digestAlgo = attr.value; | ||
nodes = utils.findChilds(ref, "DigestValue"); | ||
if (nodes.length === 0) { | ||
throw new Error(`could not find DigestValue node in reference ${ref.toString()}`); | ||
} | ||
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 = firstChild.data; | ||
const transforms = []; | ||
let inclusiveNamespacesPrefixList = []; | ||
nodes = utils.findChilds(ref, "Transforms"); | ||
if (nodes.length !== 0) { | ||
const transformsNode = nodes[0]; | ||
const transformsAll = utils.findChilds(transformsNode, "Transform"); | ||
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(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) => { var _a; return ((_a = namespace.getAttribute("PrefixList")) !== null && _a !== void 0 ? _a : "").split(" "); }) | ||
.filter((value) => value.length > 0); | ||
} | ||
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"); | ||
} | ||
this.addReference({ | ||
transforms, | ||
digestAlgorithm: digestAlgo, | ||
uri: (_a = utils.findAttr(ref, "URI")) === null || _a === void 0 ? void 0 : _a.value, | ||
digestValue, | ||
inclusiveNamespacesPrefixList, | ||
isEmptyUri: false, | ||
}); | ||
} | ||
} | ||
} | ||
var hasImplicitTransforms = | ||
Array.isArray(this.implicitTransforms) && this.implicitTransforms.length > 0; | ||
if (hasImplicitTransforms) { | ||
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"); | ||
} | ||
this.addReference( | ||
null, | ||
transforms, | ||
digestAlgo, | ||
utils.findAttr(ref, "URI").value, | ||
digestValue, | ||
inclusiveNamespacesPrefixList, | ||
false | ||
); | ||
}; | ||
SignedXml.prototype.addReference = function ( | ||
xpath, | ||
transforms, | ||
digestAlgorithm, | ||
uri, | ||
digestValue, | ||
inclusiveNamespacesPrefixList, | ||
isEmptyUri | ||
) { | ||
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, | ||
}); | ||
}; | ||
/** | ||
* Compute the signature of the given xml (using the already defined settings) | ||
* | ||
* Options: | ||
* | ||
* - `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` | ||
* | ||
*/ | ||
SignedXml.prototype.computeSignature = function (xml, opts, callback) { | ||
if (typeof opts === "function" && callback == null) { | ||
callback = opts; | ||
} | ||
if (callback != null && typeof callback !== "function") { | ||
throw new Error("Last parameter must be a callback function"); | ||
} | ||
var doc = new Dom().parseFromString(xml), | ||
xmlNsAttr = "xmlns", | ||
signatureAttrs = [], | ||
location, | ||
attrs, | ||
prefix, | ||
currentPrefix; | ||
var validActions = ["append", "prepend", "before", "after"]; | ||
opts = opts || {}; | ||
prefix = opts.prefix; | ||
attrs = opts.attrs || {}; | ||
location = opts.location || {}; | ||
var existingPrefixes = opts.existingPrefixes || {}; | ||
this.namespaceResolver = { | ||
lookupNamespaceURI: function (prefix) { | ||
return existingPrefixes[prefix]; | ||
}, | ||
}; | ||
// defaults to the root node | ||
location.reference = location.reference || "/*"; | ||
// defaults to append action | ||
location.action = location.action || "append"; | ||
if (validActions.indexOf(location.action) === -1) { | ||
var err = new Error( | ||
"location.action option has an invalid action: " + | ||
location.action + | ||
", must be any of the following values: " + | ||
validActions.join(", ") | ||
); | ||
if (!callback) { | ||
throw err; | ||
} else { | ||
callback(err, null); | ||
return; | ||
/** | ||
* 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 = ["http://www.w3.org/2001/10/xml-exc-c14n#"], digestAlgorithm = "http://www.w3.org/2000/09/xmldsig#sha1", uri = "", digestValue, inclusiveNamespacesPrefixList = [], isEmptyUri = false, }) { | ||
this.references.push({ | ||
xpath, | ||
transforms, | ||
digestAlgorithm, | ||
uri, | ||
digestValue, | ||
inclusiveNamespacesPrefixList, | ||
isEmptyUri, | ||
}); | ||
} | ||
} | ||
// automatic insertion of `:` | ||
if (prefix) { | ||
xmlNsAttr += ":" + prefix; | ||
currentPrefix = prefix + ":"; | ||
} else { | ||
currentPrefix = ""; | ||
} | ||
Object.keys(attrs).forEach(function (name) { | ||
if (name !== "xmlns" && name !== xmlNsAttr) { | ||
signatureAttrs.push(name + '="' + attrs[name] + '"'); | ||
computeSignature(xml, options, callbackParam) { | ||
let callback; | ||
if (typeof options === "function" && callbackParam == null) { | ||
callback = options; | ||
options = {}; | ||
} | ||
else { | ||
callback = callbackParam; | ||
options = (options !== null && options !== void 0 ? options : {}); | ||
} | ||
const doc = new xmldom_1.DOMParser().parseFromString(xml); | ||
let xmlNsAttr = "xmlns"; | ||
const signatureAttrs = []; | ||
let currentPrefix; | ||
const validActions = ["append", "prepend", "before", "after"]; | ||
const prefix = options.prefix; | ||
const attrs = options.attrs || {}; | ||
const location = options.location || {}; | ||
const existingPrefixes = options.existingPrefixes || {}; | ||
this.namespaceResolver = { | ||
lookupNamespaceURI: function (prefix) { | ||
return prefix ? existingPrefixes[prefix] : null; | ||
}, | ||
}; | ||
// defaults to the root node | ||
location.reference = location.reference || "/*"; | ||
// defaults to append action | ||
location.action = location.action || "append"; | ||
if (validActions.indexOf(location.action) === -1) { | ||
const err = new Error(`location.action option has an invalid action: ${location.action}, must be any of the following values: ${validActions.join(", ")}`); | ||
if (!callback) { | ||
throw err; | ||
} | ||
else { | ||
callback(err); | ||
return; | ||
} | ||
} | ||
// automatic insertion of `:` | ||
if (prefix) { | ||
xmlNsAttr += `:${prefix}`; | ||
currentPrefix = `${prefix}:`; | ||
} | ||
else { | ||
currentPrefix = ""; | ||
} | ||
Object.keys(attrs).forEach(function (name) { | ||
if (name !== "xmlns" && name !== xmlNsAttr) { | ||
signatureAttrs.push(`${name}="${attrs[name]}"`); | ||
} | ||
}); | ||
// add the xml namespace attribute | ||
signatureAttrs.push(`${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#"`); | ||
let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`; | ||
signatureXml += this.createSignedInfo(doc, prefix); | ||
signatureXml += this.getKeyInfo(prefix); | ||
signatureXml += `</${currentPrefix}Signature>`; | ||
this.originalXmlWithIds = doc.toString(); | ||
let existingPrefixesString = ""; | ||
Object.keys(existingPrefixes).forEach(function (key) { | ||
existingPrefixesString += `xmlns:${key}="${existingPrefixes[key]}" `; | ||
}); | ||
// A trick to remove the namespaces that already exist in the xml | ||
// This only works if the prefix and namespace match with those in the xml | ||
const dummySignatureWrapper = `<Dummy ${existingPrefixesString}>${signatureXml}</Dummy>`; | ||
const nodeXml = new xmldom_1.DOMParser().parseFromString(dummySignatureWrapper); | ||
// 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; | ||
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); | ||
return; | ||
} | ||
} | ||
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; | ||
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; | ||
} | ||
else { | ||
callback(err3); | ||
return; | ||
} | ||
} | ||
const signedInfoNode = signedInfoNodes[0]; | ||
if (typeof callback === "function") { | ||
// Asynchronous flow | ||
this.calculateSignatureValue(doc, (err, signature) => { | ||
if (err) { | ||
callback(err); | ||
} | ||
else { | ||
this.signatureValue = signature || ""; | ||
signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); | ||
this.signatureXml = signatureDoc.toString(); | ||
this.signedXml = doc.toString(); | ||
callback(null, this); | ||
} | ||
}); | ||
} | ||
else { | ||
// Synchronous flow | ||
this.calculateSignatureValue(doc); | ||
signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); | ||
this.signatureXml = signatureDoc.toString(); | ||
this.signedXml = doc.toString(); | ||
} | ||
} | ||
}); | ||
// add the xml namespace attribute | ||
signatureAttrs.push(xmlNsAttr + '="http://www.w3.org/2000/09/xmldsig#"'); | ||
var signatureXml = "<" + currentPrefix + "Signature " + signatureAttrs.join(" ") + ">"; | ||
signatureXml += this.createSignedInfo(doc, prefix); | ||
signatureXml += this.getKeyInfo(prefix); | ||
signatureXml += "</" + currentPrefix + "Signature>"; | ||
this.originalXmlWithIds = doc.toString(); | ||
var existingPrefixesString = ""; | ||
Object.keys(existingPrefixes).forEach(function (key) { | ||
existingPrefixesString += "xmlns:" + key + '="' + existingPrefixes[key] + '" '; | ||
}); | ||
// 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 | ||
var dummySignatureWrapper = "<Dummy " + existingPrefixesString + ">" + signatureXml + "</Dummy>"; | ||
var nodeXml = new Dom().parseFromString(dummySignatureWrapper); | ||
var signatureDoc = nodeXml.documentElement.firstChild; | ||
var referenceNode = xpath.select(location.reference, doc); | ||
if (!referenceNode || referenceNode.length === 0) { | ||
var err2 = new Error( | ||
"the following xpath cannot be used because it was not found: " + location.reference | ||
); | ||
if (!callback) { | ||
throw err2; | ||
} else { | ||
callback(err2, null); | ||
return; | ||
getKeyInfo(prefix) { | ||
const currentPrefix = prefix ? `${prefix}:` : ""; | ||
let keyInfoAttrs = ""; | ||
if (this.keyInfoAttributes) { | ||
Object.keys(this.keyInfoAttributes).forEach((name) => { | ||
keyInfoAttrs += ` ${name}="${this.keyInfoAttributes[name]}"`; | ||
}); | ||
} | ||
const keyInfoContent = this.getKeyInfoContent({ publicCert: this.publicCert, prefix }); | ||
if (keyInfoAttrs || keyInfoContent) { | ||
return `<${currentPrefix}KeyInfo${keyInfoAttrs}>${keyInfoContent}</${currentPrefix}KeyInfo>`; | ||
} | ||
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") { | ||
referenceNode.parentNode.insertBefore(signatureDoc, referenceNode); | ||
} else if (location.action === "after") { | ||
referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling); | ||
} | ||
this.signatureNode = signatureDoc; | ||
var signedInfoNode = utils.findChilds(this.signatureNode, "SignedInfo"); | ||
if (signedInfoNode.length == 0) { | ||
var err3 = new Error("could not find SignedInfo element in the message"); | ||
if (!callback) { | ||
throw err3; | ||
} else { | ||
callback(err3); | ||
return; | ||
/** | ||
* Generate the Reference nodes (as part of the signature process) | ||
* | ||
*/ | ||
createReferences(doc, prefix) { | ||
var _a; | ||
let res = ""; | ||
prefix = prefix || ""; | ||
prefix = prefix ? `${prefix}:` : prefix; | ||
for (const ref of this.references) { | ||
const nodes = xpath.selectWithResolver((_a = ref.xpath) !== null && _a !== void 0 ? _a : "", doc, this.namespaceResolver); | ||
if (!utils.isArrayHasLength(nodes)) { | ||
throw new Error(`the following xpath cannot be signed because it was not found: ${ref.xpath}`); | ||
} | ||
for (const node of nodes) { | ||
if (ref.isEmptyUri) { | ||
res += `<${prefix}Reference URI="">`; | ||
} | ||
else { | ||
const id = this.ensureHasId(node); | ||
ref.uri = id; | ||
res += `<${prefix}Reference URI="#${id}">`; | ||
} | ||
res += `<${prefix}Transforms>`; | ||
for (const trans of ref.transforms || []) { | ||
const transform = this.findCanonicalizationAlgorithm(trans); | ||
res += `<${prefix}Transform Algorithm="${transform.getAlgorithmName()}"`; | ||
if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { | ||
res += ">"; | ||
res += `<InclusiveNamespaces PrefixList="${ref.inclusiveNamespacesPrefixList.join(" ")}" xmlns="${transform.getAlgorithmName()}"/>`; | ||
res += `</${prefix}Transform>`; | ||
} | ||
else { | ||
res += " />"; | ||
} | ||
} | ||
const canonXml = this.getCanonReferenceXml(doc, ref, node); | ||
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); | ||
res += | ||
`</${prefix}Transforms>` + | ||
`<${prefix}DigestMethod Algorithm="${digestAlgorithm.getAlgorithmName()}" />` + | ||
`<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}</${prefix}DigestValue>` + | ||
`</${prefix}Reference>`; | ||
} | ||
} | ||
return res; | ||
} | ||
} | ||
signedInfoNode = signedInfoNode[0]; | ||
if (!callback) { | ||
//Synchronous flow | ||
this.calculateSignatureValue(doc); | ||
signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); | ||
this.signatureXml = signatureDoc.toString(); | ||
this.signedXml = doc.toString(); | ||
} else { | ||
var self = this; | ||
//Asynchronous flow | ||
this.calculateSignatureValue(doc, function (err, signature) { | ||
if (err) { | ||
callback(err); | ||
} else { | ||
self.signatureValue = signature; | ||
signatureDoc.insertBefore(self.createSignature(prefix), signedInfoNode.nextSibling); | ||
self.signatureXml = signatureDoc.toString(); | ||
self.signedXml = doc.toString(); | ||
callback(null, self); | ||
} | ||
}); | ||
} | ||
}; | ||
SignedXml.prototype.getKeyInfo = function (prefix) { | ||
var res = ""; | ||
var currentPrefix; | ||
currentPrefix = prefix || ""; | ||
currentPrefix = currentPrefix ? currentPrefix + ":" : currentPrefix; | ||
if (this.keyInfoProvider) { | ||
var keyInfoAttrs = ""; | ||
if (this.keyInfoProvider.attrs) { | ||
Object.keys(this.keyInfoProvider.attrs).forEach((name) => { | ||
keyInfoAttrs += " " + name + '="' + this.keyInfoProvider.attrs[name] + '"'; | ||
}); | ||
getCanonXml(transforms, node, options) { | ||
var _a; | ||
options = options || {}; | ||
options.defaultNsForPrefix = (_a = options.defaultNsForPrefix) !== null && _a !== void 0 ? _a : SignedXml.defaultNsForPrefix; | ||
options.signatureNode = this.signatureNode; | ||
let canonXml = node.cloneNode(true); // Deep clone | ||
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). | ||
//This either needs to be more explicit in the API, or all should return the same. | ||
//exclusive-canonicalization returns String since it builds the Xml by hand. If it had used xmldom it would incorrectly minimize empty tags | ||
//to <x/> instead of <x></x> and also incorrectly handle some delicate line break issues. | ||
//enveloped transformation returns Node since if it would return String consider this case: | ||
//<x xmlns:p='ns'><p:y/></x> | ||
//if only y is the node to sign then a string would be <p:y/> without the definition of the p namespace. probably xmldom toString() should have added it. | ||
}); | ||
return canonXml.toString(); | ||
} | ||
res += "<" + currentPrefix + "KeyInfo" + keyInfoAttrs + ">"; | ||
res += this.keyInfoProvider.getKeyInfo(this.signingCert || this.signingKey, prefix); | ||
res += "</" + currentPrefix + "KeyInfo>"; | ||
} | ||
return res; | ||
}; | ||
/** | ||
* Generate the Reference nodes (as part of the signature process) | ||
* | ||
*/ | ||
SignedXml.prototype.createReferences = function (doc, prefix) { | ||
var res = ""; | ||
prefix = prefix || ""; | ||
prefix = prefix ? prefix + ":" : prefix; | ||
for (var n in this.references) { | ||
if (!this.references.hasOwnProperty(n)) continue; | ||
var ref = this.references[n], | ||
nodes = xpath.selectWithResolver(ref.xpath, doc, this.namespaceResolver); | ||
if (nodes.length == 0) { | ||
throw new Error( | ||
"the following xpath cannot be signed because it was not found: " + ref.xpath | ||
); | ||
/** | ||
* Ensure an element has Id attribute. If not create it with unique value. | ||
* Work with both normal and wssecurity Id flavour | ||
*/ | ||
ensureHasId(node) { | ||
let attr; | ||
if (this.idMode === "wssecurity") { | ||
attr = utils.findAttr(node, "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); | ||
} | ||
else { | ||
this.idAttributes.some((idAttribute) => { | ||
attr = utils.findAttr(node, idAttribute); | ||
return !!attr; // This will break the loop as soon as a truthy attr is found. | ||
}); | ||
} | ||
if (attr) { | ||
return attr.value; | ||
} | ||
//add the attribute | ||
const id = `_${this.id++}`; | ||
if (this.idMode === "wssecurity") { | ||
node.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); | ||
node.setAttributeNS("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "wsu:Id", id); | ||
} | ||
else { | ||
node.setAttribute("Id", id); | ||
} | ||
return id; | ||
} | ||
for (var h in nodes) { | ||
if (!nodes.hasOwnProperty(h)) continue; | ||
var node = nodes[h]; | ||
if (ref.isEmptyUri) { | ||
res += "<" + prefix + 'Reference URI="">'; | ||
} else { | ||
var id = this.ensureHasId(node); | ||
ref.uri = id; | ||
res += "<" + prefix + 'Reference URI="#' + id + '">'; | ||
} | ||
res += "<" + prefix + "Transforms>"; | ||
for (var t in ref.transforms) { | ||
if (!ref.transforms.hasOwnProperty(t)) continue; | ||
var trans = ref.transforms[t]; | ||
var transform = this.findCanonicalizationAlgorithm(trans); | ||
res += "<" + prefix + 'Transform Algorithm="' + transform.getAlgorithmName() + '"'; | ||
if (ref.inclusiveNamespacesPrefixList) { | ||
res += ">"; | ||
res += | ||
'<InclusiveNamespaces PrefixList="' + | ||
ref.inclusiveNamespacesPrefixList + | ||
'" xmlns="' + | ||
transform.getAlgorithmName() + | ||
'"/>'; | ||
res += "</" + prefix + "Transform>"; | ||
} else { | ||
res += " />"; | ||
/** | ||
* Create the SignedInfo element | ||
* | ||
*/ | ||
createSignedInfo(doc, prefix) { | ||
const transform = this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm); | ||
const algo = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
let currentPrefix; | ||
currentPrefix = prefix || ""; | ||
currentPrefix = currentPrefix ? `${currentPrefix}:` : currentPrefix; | ||
let res = `<${currentPrefix}SignedInfo>`; | ||
res += `<${currentPrefix}CanonicalizationMethod Algorithm="${transform.getAlgorithmName()}"`; | ||
if (utils.isArrayHasLength(this.inclusiveNamespacesPrefixList)) { | ||
res += ">"; | ||
res += `<InclusiveNamespaces PrefixList="${this.inclusiveNamespacesPrefixList.join(" ")}" xmlns="${transform.getAlgorithmName()}"/>`; | ||
res += `</${currentPrefix}CanonicalizationMethod>`; | ||
} | ||
} | ||
var canonXml = this.getCanonReferenceXml(doc, ref, node); | ||
var digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); | ||
res += | ||
"</" + | ||
prefix + | ||
"Transforms>" + | ||
"<" + | ||
prefix + | ||
'DigestMethod Algorithm="' + | ||
digestAlgorithm.getAlgorithmName() + | ||
'" />' + | ||
"<" + | ||
prefix + | ||
"DigestValue>" + | ||
digestAlgorithm.getHash(canonXml) + | ||
"</" + | ||
prefix + | ||
"DigestValue>" + | ||
"</" + | ||
prefix + | ||
"Reference>"; | ||
else { | ||
res += " />"; | ||
} | ||
res += `<${currentPrefix}SignatureMethod Algorithm="${algo.getAlgorithmName()}" />`; | ||
res += this.createReferences(doc, prefix); | ||
res += `</${currentPrefix}SignedInfo>`; | ||
return res; | ||
} | ||
} | ||
return res; | ||
}; | ||
SignedXml.prototype.getCanonXml = function (transforms, node, options) { | ||
options = options || {}; | ||
options.defaultNsForPrefix = options.defaultNsForPrefix || SignedXml.defaultNsForPrefix; | ||
options.signatureNode = this.signatureNode; | ||
var canonXml = node.cloneNode(true); // Deep clone | ||
for (var t in transforms) { | ||
if (!transforms.hasOwnProperty(t)) continue; | ||
var transform = this.findCanonicalizationAlgorithm(transforms[t]); | ||
canonXml = transform.process(canonXml, options); | ||
//TODO: currently transform.process may return either Node or String value (enveloped transformation returns Node, exclusive-canonicalization returns String). | ||
//This either needs to be more explicit in the API, or all should return the same. | ||
//exclusive-canonicalization returns String since it builds the Xml by hand. If it had used xmldom it would incorrectly minimize empty tags | ||
//to <x/> instead of <x></x> and also incorrectly handle some delicate line break issues. | ||
//enveloped transformation returns Node since if it would return String consider this case: | ||
//<x xmlns:p='ns'><p:y/></x> | ||
//if only y is the node to sign then a string would be <p:y/> without the definition of the p namespace. probably xmldom toString() should have added it. | ||
} | ||
return canonXml.toString(); | ||
}; | ||
/** | ||
* Ensure an element has Id attribute. If not create it with unique value. | ||
* Work with both normal and wssecurity Id flavour | ||
*/ | ||
SignedXml.prototype.ensureHasId = function (node) { | ||
var attr; | ||
if (this.idMode == "wssecurity") { | ||
attr = utils.findAttr( | ||
node, | ||
"Id", | ||
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" | ||
); | ||
} else { | ||
for (var index in this.idAttributes) { | ||
if (!this.idAttributes.hasOwnProperty(index)) continue; | ||
attr = utils.findAttr(node, this.idAttributes[index], null); | ||
if (attr) break; | ||
/** | ||
* Create the Signature element | ||
* | ||
*/ | ||
createSignature(prefix) { | ||
let xmlNsAttr = "xmlns"; | ||
if (prefix) { | ||
xmlNsAttr += `:${prefix}`; | ||
prefix += ":"; | ||
} | ||
else { | ||
prefix = ""; | ||
} | ||
const signatureValueXml = `<${prefix}SignatureValue>${this.signatureValue}</${prefix}SignatureValue>`; | ||
//the canonicalization requires to get a valid xml node. | ||
//we need to wrap the info in a dummy signature since it contains the default namespace. | ||
const dummySignatureWrapper = `<${prefix}Signature ${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#">${signatureValueXml}</${prefix}Signature>`; | ||
const doc = new xmldom_1.DOMParser().parseFromString(dummySignatureWrapper); | ||
// 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; | ||
} | ||
} | ||
if (attr) return attr.value; | ||
//add the attribute | ||
var id = "_" + this.id++; | ||
if (this.idMode == "wssecurity") { | ||
node.setAttributeNS( | ||
"http://www.w3.org/2000/xmlns/", | ||
"xmlns:wsu", | ||
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" | ||
); | ||
node.setAttributeNS( | ||
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", | ||
"wsu:Id", | ||
id | ||
); | ||
} else { | ||
node.setAttribute("Id", id); | ||
} | ||
return id; | ||
/** | ||
* Returns just the signature part, must be called only after {@link computeSignature} | ||
* | ||
* @returns The signature XML. | ||
*/ | ||
getSignatureXml() { | ||
return this.signatureXml; | ||
} | ||
/** | ||
* 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() { | ||
return this.originalXmlWithIds; | ||
} | ||
/** | ||
* Returns the original xml document with the signature in it, must be called only after {@link computeSignature} | ||
* | ||
* @returns The signed XML. | ||
*/ | ||
getSignedXml() { | ||
return this.signedXml; | ||
} | ||
} | ||
exports.SignedXml = SignedXml; | ||
SignedXml.defaultNsForPrefix = { | ||
ds: "http://www.w3.org/2000/09/xmldsig#", | ||
}; | ||
/** | ||
* Create the SignedInfo element | ||
* | ||
*/ | ||
SignedXml.prototype.createSignedInfo = function (doc, prefix) { | ||
var transform = this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm); | ||
var algo = this.findSignatureAlgorithm(this.signatureAlgorithm); | ||
var currentPrefix; | ||
currentPrefix = prefix || ""; | ||
currentPrefix = currentPrefix ? currentPrefix + ":" : currentPrefix; | ||
var res = "<" + currentPrefix + "SignedInfo>"; | ||
res += | ||
"<" + currentPrefix + 'CanonicalizationMethod Algorithm="' + transform.getAlgorithmName() + '"'; | ||
if (this.inclusiveNamespacesPrefixList) { | ||
res += ">"; | ||
res += | ||
'<InclusiveNamespaces PrefixList="' + | ||
this.inclusiveNamespacesPrefixList + | ||
'" xmlns="' + | ||
transform.getAlgorithmName() + | ||
'"/>'; | ||
res += "</" + currentPrefix + "CanonicalizationMethod>"; | ||
} else { | ||
res += " />"; | ||
} | ||
res += "<" + currentPrefix + 'SignatureMethod Algorithm="' + algo.getAlgorithmName() + '" />'; | ||
res += this.createReferences(doc, prefix); | ||
res += "</" + currentPrefix + "SignedInfo>"; | ||
return res; | ||
}; | ||
/** | ||
* Create the Signature element | ||
* | ||
*/ | ||
SignedXml.prototype.createSignature = function (prefix) { | ||
var xmlNsAttr = "xmlns"; | ||
if (prefix) { | ||
xmlNsAttr += ":" + prefix; | ||
prefix += ":"; | ||
} else { | ||
prefix = ""; | ||
} | ||
var signatureValueXml = | ||
"<" + prefix + "SignatureValue>" + this.signatureValue + "</" + prefix + "SignatureValue>"; | ||
//the canonicalization requires to get a valid xml node. | ||
//we need to wrap the info in a dummy signature since it contains the default namespace. | ||
var dummySignatureWrapper = | ||
"<" + | ||
prefix + | ||
"Signature " + | ||
xmlNsAttr + | ||
'="http://www.w3.org/2000/09/xmldsig#">' + | ||
signatureValueXml + | ||
"</" + | ||
prefix + | ||
"Signature>"; | ||
var doc = new Dom().parseFromString(dummySignatureWrapper); | ||
return doc.documentElement.firstChild; | ||
}; | ||
SignedXml.prototype.getSignatureXml = function () { | ||
return this.signatureXml; | ||
}; | ||
SignedXml.prototype.getOriginalXmlWithIds = function () { | ||
return this.originalXmlWithIds; | ||
}; | ||
SignedXml.prototype.getSignedXml = function () { | ||
return this.signedXml; | ||
}; | ||
//# sourceMappingURL=signed-xml.js.map |
298
lib/utils.js
@@ -1,84 +0,238 @@ | ||
var select = require("xpath").select; | ||
function findAttr(node, localName, namespace) { | ||
for (var i = 0; i < node.attributes.length; i++) { | ||
var attr = node.attributes[i]; | ||
if ( | ||
attrEqualsExplicitly(attr, localName, namespace) || | ||
attrEqualsImplicitly(attr, localName, namespace, node) | ||
) { | ||
return attr; | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.validateDigestValue = exports.findAncestorNs = exports.derToPem = exports.pemToDer = exports.normalizePem = exports.BASE64_REGEX = exports.EXTRACT_X509_CERTS = exports.PEM_FORMAT_REGEX = exports.encodeSpecialCharactersInText = exports.encodeSpecialCharactersInAttribute = exports.findChilds = exports.findAttr = exports.isArrayHasLength = void 0; | ||
const xpath = require("xpath"); | ||
function isArrayHasLength(array) { | ||
return Array.isArray(array) && array.length > 0; | ||
} | ||
exports.isArrayHasLength = isArrayHasLength; | ||
function attrEqualsExplicitly(attr, localName, namespace) { | ||
return attr.localName === localName && (attr.namespaceURI === namespace || namespace == null); | ||
} | ||
function attrEqualsImplicitly(attr, localName, namespace, node) { | ||
return (attr.localName === localName && | ||
((!attr.namespaceURI && (node === null || node === void 0 ? void 0 : node.namespaceURI) === namespace) || namespace == null)); | ||
} | ||
function findAttr(element, localName, namespace) { | ||
for (let i = 0; i < element.attributes.length; i++) { | ||
const attr = element.attributes[i]; | ||
if (attrEqualsExplicitly(attr, localName, namespace) || | ||
attrEqualsImplicitly(attr, localName, namespace, element)) { | ||
return attr; | ||
} | ||
} | ||
} | ||
return null; | ||
return null; | ||
} | ||
function findFirst(doc, xpath) { | ||
var nodes = select(xpath, doc); | ||
if (nodes.length == 0) throw "could not find xpath " + xpath; | ||
return nodes[0]; | ||
} | ||
exports.findAttr = findAttr; | ||
function findChilds(node, localName, namespace) { | ||
node = node.documentElement || node; | ||
var res = []; | ||
for (var i = 0; i < node.childNodes.length; i++) { | ||
var child = node.childNodes[i]; | ||
if (child.localName == localName && (child.namespaceURI == namespace || !namespace)) { | ||
res.push(child); | ||
var _a; | ||
const element = (_a = node.documentElement) !== null && _a !== void 0 ? _a : node; | ||
const res = []; | ||
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); | ||
} | ||
} | ||
} | ||
return res; | ||
return res; | ||
} | ||
function attrEqualsExplicitly(attr, localName, namespace) { | ||
return attr.localName == localName && (attr.namespaceURI == namespace || !namespace); | ||
} | ||
function attrEqualsImplicitly(attr, localName, namespace, node) { | ||
return ( | ||
attr.localName == localName && | ||
((!attr.namespaceURI && node.namespaceURI == namespace) || !namespace) | ||
); | ||
} | ||
var xml_special_to_encoded_attribute = { | ||
"&": "&", | ||
"<": "<", | ||
'"': """, | ||
"\r": "
", | ||
"\n": "
", | ||
"\t": "	", | ||
exports.findChilds = findChilds; | ||
const xml_special_to_encoded_attribute = { | ||
"&": "&", | ||
"<": "<", | ||
'"': """, | ||
"\r": "
", | ||
"\n": "
", | ||
"\t": "	", | ||
}; | ||
var xml_special_to_encoded_text = { | ||
"&": "&", | ||
"<": "<", | ||
">": ">", | ||
"\r": "
", | ||
const xml_special_to_encoded_text = { | ||
"&": "&", | ||
"<": "<", | ||
">": ">", | ||
"\r": "
", | ||
}; | ||
function encodeSpecialCharactersInAttribute(attributeValue) { | ||
return attributeValue.replace(/([&<"\r\n\t])/g, function (str, item) { | ||
// Special character normalization. See: | ||
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes) | ||
// - https://www.w3.org/TR/xml-c14n#Example-Chars | ||
return xml_special_to_encoded_attribute[item]; | ||
}); | ||
return attributeValue.replace(/([&<"\r\n\t])/g, function (str, item) { | ||
// Special character normalization. See: | ||
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes) | ||
// - https://www.w3.org/TR/xml-c14n#Example-Chars | ||
return xml_special_to_encoded_attribute[item]; | ||
}); | ||
} | ||
exports.encodeSpecialCharactersInAttribute = encodeSpecialCharactersInAttribute; | ||
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) | ||
// - https://www.w3.org/TR/xml-c14n#Example-Chars | ||
return xml_special_to_encoded_text[item]; | ||
}); | ||
return text.replace(/([&<>\r])/g, function (str, item) { | ||
// Special character normalization. See: | ||
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes) | ||
// - https://www.w3.org/TR/xml-c14n#Example-Chars | ||
return xml_special_to_encoded_text[item]; | ||
}); | ||
} | ||
exports.findAttr = findAttr; | ||
exports.findChilds = findChilds; | ||
exports.encodeSpecialCharactersInAttribute = encodeSpecialCharactersInAttribute; | ||
exports.encodeSpecialCharactersInText = encodeSpecialCharactersInText; | ||
exports.findFirst = findFirst; | ||
/** | ||
* 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. | ||
*/ | ||
exports.PEM_FORMAT_REGEX = new RegExp("^-----BEGIN [A-Z\x20]{1,48}-----([^-]*)-----END [A-Z\x20]{1,48}-----$", "s"); | ||
exports.EXTRACT_X509_CERTS = new RegExp("-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----", "g"); | ||
exports.BASE64_REGEX = new RegExp("^(?:[A-Za-z0-9\\+\\/]{4}\\n{0,1})*(?:[A-Za-z0-9\\+\\/]{2}==|[A-Za-z0-9\\+\\/]{3}=)?$", "s"); | ||
/** | ||
* -----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 | ||
*/ | ||
function normalizePem(pem) { | ||
var _a; | ||
return `${((_a = pem | ||
.trim() | ||
.replace(/(\r\n|\r)/g, "\n") | ||
.match(/.{1,64}/g)) !== null && _a !== void 0 ? _a : []).join("\n")}\n`; | ||
} | ||
exports.normalizePem = normalizePem; | ||
/** | ||
* @param pem The PEM-encoded base64 certificate to strip headers from | ||
*/ | ||
function pemToDer(pem) { | ||
return pem | ||
.replace(/(\r\n|\r)/g, "\n") | ||
.replace(/-----BEGIN [A-Z\x20]{1,48}-----\n?/, "") | ||
.replace(/-----END [A-Z\x20]{1,48}-----\n?/, ""); | ||
} | ||
exports.pemToDer = pemToDer; | ||
/** | ||
* @param der The DER-encoded base64 certificate to add PEM headers too | ||
* @param pemLabel The label of the header and footer to add | ||
*/ | ||
function derToPem(der, pemLabel) { | ||
const base64Der = Buffer.isBuffer(der) ? der.toString("latin1").trim() : der.trim(); | ||
if (exports.PEM_FORMAT_REGEX.test(base64Der)) { | ||
return normalizePem(base64Der); | ||
} | ||
if (exports.BASE64_REGEX.test(base64Der)) { | ||
const pem = `-----BEGIN ${pemLabel}-----\n${base64Der}\n-----END ${pemLabel}-----`; | ||
return normalizePem(pem); | ||
} | ||
throw new Error("Unknown DER format."); | ||
} | ||
exports.derToPem = derToPem; | ||
function collectAncestorNamespaces(node, nsArray = []) { | ||
if (!xpath.isElement(node.parentNode)) { | ||
return 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); | ||
} | ||
function findNSPrefix(subset) { | ||
const subsetAttributes = subset.attributes; | ||
for (let k = 0; k < subsetAttributes.length; k++) { | ||
const nodeName = subsetAttributes[k].nodeName; | ||
if (nodeName.search(/^xmlns:?/) !== -1) { | ||
return nodeName.replace(/^xmlns:?/, ""); | ||
} | ||
} | ||
return subset.prefix || ""; | ||
} | ||
function isElementSubset(docSubset) { | ||
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. | ||
* | ||
* @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 (!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 = []; | ||
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 subsetNsPrefix = findNSPrefix(docSubset[0]); | ||
for (const ancestorNs of ancestorNsWithoutDuplicate) { | ||
if (ancestorNs.prefix !== subsetNsPrefix) { | ||
returningNs.push(ancestorNs); | ||
} | ||
} | ||
return returningNs; | ||
} | ||
exports.findAncestorNs = findAncestorNs; | ||
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); | ||
} | ||
// 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; | ||
} | ||
exports.validateDigestValue = validateDigestValue; | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "xml-crypto", | ||
"version": "3.2.0", | ||
"version": "4.0.0", | ||
"private": false, | ||
@@ -22,16 +22,14 @@ "description": "Xml digital signature and encryption library for Node.js", | ||
], | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"main": "./lib", | ||
"files": [ | ||
"CHANGELOG.md", | ||
"index.d.ts", | ||
"lib" | ||
"lib", | ||
"LICENSE", | ||
"README.md" | ||
], | ||
"directories": { | ||
"lib": "lib" | ||
}, | ||
"scripts": { | ||
"build": "npx tsc", | ||
"changelog": "gren changelog --override --generate", | ||
"lint": "eslint --ext .js \"**/*.js\" --cache && npm run prettier-check", | ||
"lint:fix": "eslint --ext .js --fix \"**/*.js\" && npm run prettier-format", | ||
"lint": "eslint --ext .ts \"{src,test}/*.ts\" --cache && npm run prettier-check", | ||
"lint:fix": "eslint --ext .ts --fix \"{src,test}/*.ts\" && npm run prettier-format", | ||
"prepare": "tsc", | ||
"prettier-check": "prettier --config .prettierrc.json --check .", | ||
@@ -41,22 +39,36 @@ "prettier-format": "prettier --config .prettierrc.json --write .", | ||
"release": "release-it", | ||
"test": "nodeunit ./test/canonicalization-unit-tests.js ./test/c14nWithComments-unit-tests.js ./test/signature-unit-tests.js ./test/saml-response-test.js ./test/signature-integration-tests.js ./test/document-test.js ./test/wsfed-metadata-test.js ./test/hmac-tests.js ./test/c14n-non-exclusive-unit-test.js" | ||
"test": "nyc mocha" | ||
}, | ||
"dependencies": { | ||
"@xmldom/xmldom": "^0.8.8", | ||
"xpath": "0.0.32" | ||
"@xmldom/xmldom": "^0.8.10", | ||
"xpath": "0.0.33" | ||
}, | ||
"devDependencies": { | ||
"@cjbarth/github-release-notes": "^4.1.0", | ||
"@prettier/plugin-xml": "^2.2.0", | ||
"ejs": "3.1.9", | ||
"eslint": "^8.42.0", | ||
"@istanbuljs/nyc-config-typescript": "^1.0.2", | ||
"@prettier/plugin-xml": "^3.1.1", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^20.3.2", | ||
"@typescript-eslint/eslint-plugin": "^5.62.0", | ||
"@typescript-eslint/parser": "^5.62.0", | ||
"chai": "^4.3.7", | ||
"choma": "^1.2.1", | ||
"ejs": "^3.1.9", | ||
"eslint": "^8.45.0", | ||
"eslint-config-prettier": "^8.8.0", | ||
"nodeunit": "^0.11.3", | ||
"prettier": "^2.8.8", | ||
"prettier-plugin-packagejson": "^2.4.3", | ||
"release-it": "^15.11.0" | ||
"eslint-plugin-deprecation": "^1.4.1", | ||
"lcov": "^1.16.0", | ||
"mocha": "^10.2.0", | ||
"nyc": "^15.1.0", | ||
"prettier": "^3.0.0", | ||
"prettier-plugin-packagejson": "^2.4.5", | ||
"release-it": "^16.1.3", | ||
"source-map-support": "^0.5.21", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.1.6" | ||
}, | ||
"engines": { | ||
"node": ">=4.0.0" | ||
"node": ">=14" | ||
} | ||
} |
164
README.md
@@ -46,8 +46,4 @@ # xml-crypto | ||
to enable HMAC-SHA1, do: | ||
to enable HMAC-SHA1, call `enableHMAC()` on your instance of `SignedXml`. | ||
```javascript | ||
require("xml-crypto").SignedXml.enableHMAC(); | ||
``` | ||
This will enable HMAC and disable digital signature algorithms. Due to key | ||
@@ -69,8 +65,8 @@ confusion issues, it is risky to have both HMAC-based and public key digital | ||
When signing a xml document you can specify the following properties on a `SignedXml` instance to customize the signature process: | ||
When signing a xml document you can pass the following options to the `SignedXml` constructor to customize the signature process: | ||
- `sign.signingKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key | ||
- `sign.keyInfoProvider` - **[optional]** a key info provider instance, see [customizing algorithms](#customizing-algorithms) for an implementation example | ||
- `sign.signatureAlgorithm` - **[optional]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` | ||
- `sign.canonicalizationAlgorithm` - **[optional]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` | ||
- `privateKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key | ||
- `publicCert` - **[optional]** a `Buffer` or pem encoded `String` containing your public key | ||
- `signatureAlgorithm` - **[optional]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` | ||
- `canonicalizationAlgorithm` - **[optional]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` | ||
@@ -85,5 +81,4 @@ Use this code: | ||
var sig = new SignedXml(); | ||
sig.addReference("//*[local-name(.)='book']"); | ||
sig.signingKey = fs.readFileSync("client.pem"); | ||
var sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); | ||
sig.addReference({ xpath: "//*[local-name(.)='book']" }); | ||
sig.computeSignature(xml); | ||
@@ -119,14 +114,18 @@ fs.writeFileSync("signed.xml", sig.getSignedXml()); | ||
To generate a `<X509Data></X509Data>` element in the signature you must provide a key info implementation, see [customizing algorithms](#customizing-algorithms) for an example. | ||
If you set the `publicCert` property, a `<X509Data></X509Data>` element with the public certificate will be generated in the signature. | ||
To customize this see [customizing algorithms](#customizing-algorithms) for an example. | ||
## Verifying Xml documents | ||
When verifying a xml document you must specify the following properties on a ``SignedXml` instance: | ||
When verifying a xml document you can pass the following options to the `SignedXml` constructor to customize the verify process: | ||
- `sign.keyInfoProvider` - **[required]** a key info provider instance containing your certificate, see [customizing algorithms](#customizing-algorithms) for an implementation example | ||
- `publicCert` - **[optional]** your certificate as a string, a string of multiple certs in PEM format, or a Buffer | ||
- `privateKey` - **[optional]** your private key as a string or a Buffer - used for verifying symmetrical signatures (HMAC) | ||
You can use any dom parser you want in your code (or none, depending on your usage). This sample uses [xmldom](https://github.com/jindw/xmldom) so you should install it first: | ||
The certificate that will be used to check the signature will first be determined by calling `.getCertFromKeyInfo()`, which function you can customize as you see fit. If that returns `null`, then `publicCert` is used. If that is `null`, then `privateKey` is used (for symmetrical signing applications). | ||
You can use any dom parser you want in your code (or none, depending on your usage). This sample uses [xmldom](https://github.com/xmldom/xmldom), so you should install it first: | ||
```shell | ||
npm install xmldom | ||
npm install @xmldom/xmldom | ||
``` | ||
@@ -140,3 +139,2 @@ | ||
SignedXml = require("xml-crypto").SignedXml, | ||
FileKeyInfo = require("xml-crypto").FileKeyInfo, | ||
fs = require("fs"); | ||
@@ -149,6 +147,5 @@ | ||
doc, | ||
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']" | ||
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", | ||
)[0]; | ||
var sig = new SignedXml(); | ||
sig.keyInfoProvider = new FileKeyInfo("client_public.pem"); | ||
var sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") }); | ||
sig.loadSignature(signature); | ||
@@ -159,3 +156,3 @@ var res = sig.checkSignature(xml); | ||
if the verification process fails `sig.validationErrors` will have the errors. | ||
If the verification process fails `sig.validationErrors` will contain the errors. | ||
@@ -187,5 +184,7 @@ In order to protect from some attacks we must check the content we want to use is the one that has been signed: | ||
```javascript | ||
var option = { implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"] }; | ||
var sig = new SignedXml(null, option); | ||
sig.keyInfoProvider = new FileKeyInfo("client_public.pem"); | ||
var options = { | ||
implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], | ||
publicCert: fs.readFileSync("client_public.pem"), | ||
}; | ||
var sig = new SignedXml(options); | ||
sig.loadSignature(signature); | ||
@@ -211,5 +210,15 @@ var res = sig.checkSignature(xml); | ||
The `SignedXml` constructor provides an abstraction for sign and verify xml documents. The object is constructed using `new SignedXml([idMode])` where: | ||
The `SignedXml` constructor provides an abstraction for sign and verify xml documents. The object is constructed using `new SignedXml(options?: SignedXmlOptions)` where the possible options are: | ||
- `idMode` - if the value of `"wssecurity"` is passed it will create/validate id's with the ws-security namespace. | ||
- `idMode` - default `null` - if the value of `wssecurity` is passed it will create/validate id's with the ws-security namespace. | ||
- `idAttribute` - string - default `Id` or `ID` or `id` - the name of the attribute that contains the id of the element | ||
- `privateKey` - string or Buffer - default `null` - the private key to use for signing | ||
- `publicCert` - string or Buffer - default `null` - the public certificate to use for verifying | ||
- `signatureAlgorithm` - string - default `http://www.w3.org/2000/09/xmldsig#rsa-sha1` - the signature algorithm to use | ||
- `canonicalizationAlgorithm` - string - default `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` - the canonicalization algorithm to use | ||
- `inclusiveNamespacesPrefixList` - string - default `null` - a list of namespace prefixes to include during canonicalization | ||
- `implicitTransforms` - string[] - default `[]` - a list of implicit transforms to use during verification | ||
- `keyInfoAttributes` - object - default `{}` - a hash of attributes and values `attrName: value` to add to the KeyInfo node | ||
- `getKeyInfoContent` - function - default `SignedXml.geTKeyInfoContent` - a function that returns the content of the KeyInfo node | ||
- `getCertFromKeyInfo` - function - default `SignedXml.getCertFromKeyInfo` - a function that returns the certificate from the KeyInfo node | ||
@@ -240,14 +249,6 @@ #### API | ||
- `loadSignature(signatureXml)` - loads the signature where: | ||
- `signatureXml` - a string or node object (like an [xml-dom](https://github.com/jindw/xmldom) node) containing the xml representation of the signature | ||
- `signatureXml` - a string or node object (like an [xmldom](https://github.com/xmldom/xmldom) node) containing the xml representation of the signature | ||
- `checkSignature(xml)` - validates the given xml document and returns true if the validation was successful, `sig.validationErrors` will have the validation errors if any, where: | ||
- `xml` - a string containing a xml document | ||
### FileKeyInfo | ||
A basic key info provider implementation using `fs.readFileSync(file)`, is constructed using `new FileKeyInfo([file])` where: | ||
- `file` - a path to a pem encoded certificate | ||
See [verifying xml documents](#verifying-xml-documents) for an example usage | ||
## Customizing Algorithms | ||
@@ -266,20 +267,11 @@ | ||
A key info provider is used to extract and construct the key and the KeyInfo xml section. | ||
Implement it if you want to create a signature with a KeyInfo section, or you want to read your key in a different way then the default file read option. | ||
To determine the inclusion and contents of a `<KeyInfo />` element, the function | ||
`getKeyInfoContent()` is called. There is a default implementation of this. If you wish to change | ||
this implementation, provide your own function assigned to the property `.getKeyInfoContent`. If | ||
there are no attributes and no contents to the `<KeyInfo />` element, it won't be included in the | ||
generated XML. | ||
```javascript | ||
function MyKeyInfo() { | ||
this.getKeyInfo = function (key, prefix) { | ||
prefix = prefix || ""; | ||
prefix = prefix ? prefix + ":" : prefix; | ||
return "<" + prefix + "X509Data></" + prefix + "X509Data>"; | ||
}; | ||
this.getKey = function (keyInfo) { | ||
//you can use the keyInfo parameter to extract the key in any way you want | ||
return fs.readFileSync("key.pem"); | ||
}; | ||
} | ||
``` | ||
To specify custom attributes on `<KeyInfo />`, add the properties to the `.keyInfoAttributes` property. | ||
A custom hash algorithm is used to calculate digests. Implement it if you want a hash other than the default SHA1. | ||
A custom hash algorithm is used to calculate digests. Implement it if you want a hash other than the built-in methods. | ||
@@ -298,3 +290,3 @@ ```javascript | ||
A custom signing algorithm. The default is RSA-SHA1 | ||
A custom signing algorithm. The default is RSA-SHA1. | ||
@@ -304,3 +296,3 @@ ```javascript | ||
/*sign the given SignedInfo using the key. return base64 signature value*/ | ||
this.getSignature = function (signedInfo, signingKey) { | ||
this.getSignature = function (signedInfo, privateKey) { | ||
return "signature of signedInfo as base64..."; | ||
@@ -352,6 +344,6 @@ }; | ||
SignedXml.CanonicalizationAlgorithms["http://MyTransformation"] = MyTransformation; | ||
SignedXml.CanonicalizationAlgorithms["http://MyCanonicalization"] = MyCanonicalization; | ||
SignedXml.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest; | ||
SignedXml.SignatureAlgorithms["http://mySigningAlgorithm"] = MySignatureAlgorithm; | ||
signedXml.CanonicalizationAlgorithms["http://MyTransformation"] = MyTransformation; | ||
signedXml.CanonicalizationAlgorithms["http://MyCanonicalization"] = MyCanonicalization; | ||
signedXml.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest; | ||
signedXml.SignatureAlgorithms["http://mySigningAlgorithm"] = MySignatureAlgorithm; | ||
``` | ||
@@ -363,16 +355,19 @@ | ||
function signXml(xml, xpath, key, dest) { | ||
var sig = new SignedXml(); | ||
var options = { | ||
publicCert: fs.readFileSync("my_public_cert.pem", "latin1"), | ||
privateKey: fs.readFileSync(key), | ||
/*configure the signature object to use the custom algorithms*/ | ||
signatureAlgorithm: "http://mySignatureAlgorithm", | ||
canonicalizationAlgorithm: "http://MyCanonicalization", | ||
}; | ||
/*configure the signature object to use the custom algorithms*/ | ||
sig.signatureAlgorithm = "http://mySignatureAlgorithm"; | ||
sig.keyInfoProvider = new MyKeyInfo(); | ||
sig.canonicalizationAlgorithm = "http://MyCanonicalization"; | ||
sig.addReference( | ||
"//*[local-name(.)='x']", | ||
["http://MyTransformation"], | ||
"http://myDigestAlgorithm" | ||
); | ||
var sig = new SignedXml(options); | ||
sig.signingKey = fs.readFileSync(key); | ||
sig.addReference(xpath); | ||
sig.addReference({ | ||
xpath: "//*[local-name(.)='x']", | ||
transforms: ["http://MyTransformation"], | ||
digestAlgorithm: "http://myDigestAlgorithm", | ||
}); | ||
sig.addReference({ xpath }); | ||
sig.computeSignature(xml); | ||
@@ -388,14 +383,14 @@ fs.writeFileSync(dest, sig.getSignedXml()); | ||
You can always look at the actual code as a sample (or drop me a [mail](mailto:yaronn01@gmail.com)). | ||
You can always look at the actual code as a sample. | ||
## Asynchronous signing and verification | ||
If the private key is not stored locally and you wish to use a signing server or Hardware Security Module (HSM) to sign documents you can create a custom signing algorithm that uses an asynchronous callback. | ||
If the private key is not stored locally, and you wish to use a signing server or Hardware Security Module (HSM) to sign documents, you can create a custom signing algorithm that uses an asynchronous callback. | ||
```javascript | ||
function AsyncSignatureAlgorithm() { | ||
this.getSignature = function (signedInfo, signingKey, callback) { | ||
this.getSignature = function (signedInfo, privateKey, callback) { | ||
var signer = crypto.createSign("RSA-SHA1"); | ||
signer.update(signedInfo); | ||
var res = signer.sign(signingKey, "base64"); | ||
var res = signer.sign(privateKey, "base64"); | ||
//Do some asynchronous things here | ||
@@ -409,5 +404,4 @@ callback(null, res); | ||
SignedXml.SignatureAlgorithms["http://asyncSignatureAlgorithm"] = AsyncSignatureAlgorithm; | ||
var sig = new SignedXml(); | ||
sig.signatureAlgorithm = "http://asyncSignatureAlgorithm"; | ||
var sig = new SignedXml({ signatureAlgorithm: "http://asyncSignatureAlgorithm" }); | ||
sig.SignatureAlgorithms["http://asyncSignatureAlgorithm"] = AsyncSignatureAlgorithm; | ||
sig.computeSignature(xml, opts, function (err) { | ||
@@ -462,5 +456,4 @@ var signedResponse = sig.getSignedXml(); | ||
var sig = new SignedXml(); | ||
sig.addReference("//*[local-name(.)='book']"); | ||
sig.signingKey = fs.readFileSync("client.pem"); | ||
var sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); | ||
sig.addReference({ xpath: "//*[local-name(.)='book']" }); | ||
sig.computeSignature(xml, { | ||
@@ -487,5 +480,4 @@ prefix: "ds", | ||
var sig = new SignedXml(); | ||
sig.addReference("//*[local-name(.)='book']"); | ||
sig.signingKey = fs.readFileSync("client.pem"); | ||
var sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); | ||
sig.addReference({ xpath: "//*[local-name(.)='book']" }); | ||
sig.computeSignature(xml, { | ||
@@ -500,4 +492,6 @@ location: { reference: "//*[local-name(.)='book']", action: "after" }, //This will place the signature after the book element | ||
The test framework is [nodeunit](https://github.com/caolan/nodeunit). To run tests use: | ||
The testing framework we use is [Mocha](https://github.com/mochajs/mocha) with [Chai](https://github.com/chaijs/chai) as the assertion framework. | ||
To run tests use: | ||
```shell | ||
@@ -504,0 +498,0 @@ npm test |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
254411
30
2290
0
23
490
1
+ Addedxpath@0.0.33(transitive)
- Removedxpath@0.0.32(transitive)
Updated@xmldom/xmldom@^0.8.10
Updatedxpath@0.0.33