@boxyhq/xml-crypto
Version:
Xml digital signature and encryption library for Node.js
883 lines • 41.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SignedXml = void 0;
const xpath = require("xpath");
const xmldom = 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");
const isDomNode = require("@xmldom/is-dom-node");
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 = undefined;
/**
* Rules used to convert an XML document into its canonical form.
*/
this.canonicalizationAlgorithm = undefined;
/**
* 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
this.id = 0;
this.signedXml = "";
this.signatureXml = "";
this.signatureNode = null;
this.signatureValue = "";
this.originalXmlWithIds = "";
this.keyInfo = null;
/**
* Contains the references that were signed.
* @see {@link Reference}
*/
this.references = [];
/**
* 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 ?? this.signatureAlgorithm;
this.canonicalizationAlgorithm = canonicalizationAlgorithm;
if (typeof inclusiveNamespacesPrefixList === "string") {
this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList.split(" ");
}
else if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) {
this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList;
}
this.implicitTransforms = implicitTransforms ?? this.implicitTransforms;
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
}
/**
* 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 = SignedXml.noop;
}
/**
* 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) => `<${prefix}X509Certificate>${utils
.pemToDer(c)
.toString("base64")}</${prefix}X509Certificate>`)
.join("");
}
return `<${prefix}X509Data>${x509Certs}</${prefix}X509Data>`;
}
/**
* 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 cert = xpath.select1(".//*[local-name(.)='X509Certificate']", keyInfo);
if (isDomNode.isNodeLike(cert)) {
return utils.derToPem(cert.textContent ?? "", "CERTIFICATE");
}
}
return null;
}
checkSignature(xml, callback) {
if (callback != null && typeof callback !== "function") {
throw new Error("Last parameter must be a callback function");
}
this.signedXml = xml;
const doc = new xmldom.DOMParser().parseFromString(xml);
// Reset the references as only references from our re-parsed signedInfo node can be trusted
this.references = [];
// signedInfoCanon is unsigned here, we will show that it is signed in later step (B)
const signedInfoCanon = this.getCanonSignedInfoXml(doc);
if (!signedInfoCanon) {
throw new Error("Canonical signed info not be empty");
}
// unsigned, verify later to keep with consistent callback behavior
const parsedSignedInfo = new xmldom.DOMParser().parseFromString(signedInfoCanon, "text/xml");
const signedInfoDoc = parsedSignedInfo.documentElement;
if (!signedInfoDoc) {
throw new Error("Could not parse signedInfoCanon into a document");
}
const references = utils.findChildren(signedInfoDoc, "Reference");
if (!utils.isArrayHasLength(references)) {
throw new Error("could not find any Reference elements");
}
// TODO: In a future release we'd like to load the Signature and its References at the same time,
// however, in the .loadSignature() method we don't have the entire document,
// which we need to to keep the inclusive namespaces
for (const reference of references) {
this.loadReference(reference);
}
if (!this.getReferences().every((ref) => this.validateReference(ref, doc))) {
if (callback) {
callback(new Error("Could not validate all references"));
return;
}
return false;
}
// Stage B: Take the signature algorithm and key and verify the SignatureValue against the canonicalized SignedInfo
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 (callback) {
signer.verifySignature(signedInfoCanon, key, this.signatureValue, callback);
}
else {
const verified = signer.verifySignature(signedInfoCanon, key, this.signatureValue);
if (verified === false) {
throw new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`);
}
return true;
}
}
getCanonSignedInfoXml(doc) {
if (this.signatureNode == null) {
throw new Error("No signature found.");
}
if (typeof this.canonicalizationAlgorithm !== "string") {
throw new Error("Missing canonicalizationAlgorithm when trying to get signed info for XML");
}
const signedInfo = utils.findChildren(this.signatureNode, "SignedInfo");
if (signedInfo.length === 0) {
throw new Error("could not find SignedInfo element in the message");
}
if (signedInfo.length > 1) {
throw new Error("could not get canonicalized signed info for a signature that contains multiple SignedInfo nodes");
}
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);
}
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);
}
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);
}
}
findSignatureAlgorithm(name) {
if (name == null) {
throw new Error("signatureAlgorithm is required");
}
const algo = this.SignatureAlgorithms[name];
if (algo) {
return new algo();
}
else {
throw new Error(`signature algorithm '${name}' is not supported`);
}
}
findCanonicalizationAlgorithm(name) {
if (name != null) {
const algo = this.CanonicalizationAlgorithms[name];
if (algo) {
return new algo();
}
}
throw new Error(`canonicalization algorithm '${name}' is not supported`);
}
findHashAlgorithm(name) {
const algo = this.HashAlgorithms[name];
if (algo) {
return new algo();
}
else {
throw new Error(`hash algorithm '${name}' is not supported`);
}
}
validateElementAgainstReferences(elemOrXpath, doc) {
let elem;
if (typeof elemOrXpath === "string") {
const firstElem = xpath.select1(elemOrXpath, doc);
isDomNode.assertIsElementNode(firstElem);
elem = firstElem;
}
else {
elem = elemOrXpath;
}
for (const ref of this.getReferences()) {
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
for (const attr of this.idAttributes) {
const elemId = elem.getAttribute(attr);
if (uri === elemId) {
ref.xpath = `//*[@*[local-name(.)='${attr}']='${uri}']`;
break; // found the correct element, no need to check further
}
}
const canonXml = this.getCanonReferenceXml(doc, ref, elem);
const hash = this.findHashAlgorithm(ref.digestAlgorithm);
const digest = hash.getHash(canonXml);
if (utils.validateDigestValue(digest, ref.digestValue)) {
return ref;
}
}
throw new Error("No references passed validation");
}
validateReference(ref, doc) {
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
let elem = null;
if (uri === "") {
elem = xpath.select1("//*", doc);
}
else if (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;
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.");
}
elem = tmp_elem[0];
ref.xpath = tmp_elemXpath;
}
}
}
ref.getValidatedNode = (xpathSelector) => {
xpathSelector = xpathSelector || ref.xpath;
if (typeof xpathSelector !== "string" || ref.validationError != null) {
return null;
}
const selectedValue = xpath.select1(xpathSelector, doc);
return isDomNode.isNodeLike(selectedValue) ? selectedValue : null;
};
if (!isDomNode.isNodeLike(elem)) {
const validationError = new Error(`invalid signature: the signature references an element with uri ${ref.uri} but could not find such element in the xml`);
ref.validationError = validationError;
return false;
}
const canonXml = this.getCanonReferenceXml(doc, ref, elem);
const hash = this.findHashAlgorithm(ref.digestAlgorithm);
const digest = hash.getHash(canonXml);
if (!utils.validateDigestValue(digest, ref.digestValue)) {
const validationError = new Error(`invalid signature: for uri ${ref.uri} calculated digest is ${digest} but the xml to validate supplies digest ${ref.digestValue}`);
ref.validationError = validationError;
return false;
}
return true;
}
findSignatures(doc) {
const nodes = xpath.select("//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc);
return isDomNode.isArrayOfNodes(nodes) ? nodes : [];
}
/**
* 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.DOMParser().parseFromString(signatureNode);
}
else {
this.signatureNode = signatureNode;
}
this.signatureXml = signatureNode.toString();
const node = xpath.select1(".//*[local-name(.)='CanonicalizationMethod']/@Algorithm", signatureNode);
if (!isDomNode.isNodeLike(node)) {
throw new Error("could not find CanonicalizationMethod/@Algorithm element");
}
if (isDomNode.isAttributeNode(node)) {
this.canonicalizationAlgorithm = node.value;
}
const signatureAlgorithm = xpath.select1(".//*[local-name(.)='SignatureMethod']/@Algorithm", signatureNode);
if (isDomNode.isAttributeNode(signatureAlgorithm)) {
this.signatureAlgorithm = signatureAlgorithm.value;
}
const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
if (!utils.isArrayHasLength(signedInfoNodes)) {
throw new Error("no signed info node found");
}
if (signedInfoNodes.length > 1) {
throw new Error("could not load signature that contains multiple SignedInfo nodes");
}
// Try to operate on the c14n version of signedInfo. This forces the initial getReferences()
// API call to always return references that are loaded under the canonical SignedInfo
// in the case that the client access the .references **before** signature verification.
// Ensure canonicalization algorithm is exclusive, otherwise we'd need the entire document
let canonicalizationAlgorithmForSignedInfo = this.canonicalizationAlgorithm;
if (!canonicalizationAlgorithmForSignedInfo ||
canonicalizationAlgorithmForSignedInfo ===
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315" ||
canonicalizationAlgorithmForSignedInfo ===
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments") {
canonicalizationAlgorithmForSignedInfo = "http://www.w3.org/2001/10/xml-exc-c14n#";
}
const temporaryCanonSignedInfo = this.getCanonXml([canonicalizationAlgorithmForSignedInfo], signedInfoNodes[0]);
const temporaryCanonSignedInfoXml = new xmldom.DOMParser().parseFromString(temporaryCanonSignedInfo, "text/xml");
const signedInfoDoc = temporaryCanonSignedInfoXml.documentElement;
this.references = [];
const references = utils.findChildren(signedInfoDoc, "Reference");
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 (isDomNode.isTextNode(signatureValue)) {
this.signatureValue = signatureValue.data.replace(/\r?\n/g, "");
}
const keyInfo = xpath.select1(".//*[local-name(.)='KeyInfo']", signatureNode);
if (isDomNode.isNodeLike(keyInfo)) {
this.keyInfo = keyInfo;
}
}
/**
* Load the reference xml node to a model
*
*/
loadReference(refNode) {
let nodes = utils.findChildren(refNode, "DigestMethod");
if (nodes.length === 0) {
throw new Error(`could not find DigestMethod in reference ${refNode.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.findChildren(refNode, "DigestValue");
if (nodes.length === 0) {
throw new Error(`could not find DigestValue node in reference ${refNode.toString()}`);
}
if (nodes.length > 1) {
throw new Error(`could not load reference for a node that contains multiple DigestValue nodes: ${refNode.toString()}`);
}
const digestValue = nodes[0].textContent;
if (!digestValue) {
throw new Error(`could not find the value of DigestValue in ${refNode.toString()}`);
}
const transforms = [];
let inclusiveNamespacesPrefixList = [];
nodes = utils.findChildren(refNode, "Transforms");
if (nodes.length !== 0) {
const transformsNode = nodes[0];
const transformsAll = utils.findChildren(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.findChildren(transformsAll[transformsAll.length - 1], "InclusiveNamespaces");
if (utils.isArrayHasLength(inclusiveNamespaces)) {
// Should really only be one prefix list, but maybe there's some circumstances where more than one to let's handle it
inclusiveNamespacesPrefixList = inclusiveNamespaces
.flatMap((namespace) => (namespace.getAttribute("PrefixList") ?? "").split(" "))
.filter((value) => value.length > 0);
}
}
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");
}
const refUri = isDomNode.isElementNode(refNode)
? refNode.getAttribute("URI") || undefined
: undefined;
this.addReference({
transforms,
digestAlgorithm: digestAlgo,
uri: refUri,
digestValue,
inclusiveNamespacesPrefixList,
isEmptyUri: false,
});
}
/**
* 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.
* @param digestAlgorithm The digest algorithm to use for computing the digest value.
* @param uri The URI identifier for the reference. If empty, an empty URI will be used.
* @param digestValue The expected digest value for the reference.
* @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization.
* @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`.
*/
addReference({ xpath, transforms, digestAlgorithm, uri = "", digestValue, inclusiveNamespacesPrefixList = [], isEmptyUri = false, }) {
if (digestAlgorithm == null) {
throw new Error("digestAlgorithm is required");
}
if (!utils.isArrayHasLength(transforms)) {
throw new Error("transforms must contain at least one transform algorithm");
}
this.references.push({
xpath,
transforms,
digestAlgorithm,
uri,
digestValue,
inclusiveNamespacesPrefixList,
isEmptyUri,
getValidatedNode: () => {
throw new Error("Reference has not been validated yet; Did you call `sig.checkSignature()`?");
},
});
}
getReferences() {
return this.references;
}
computeSignature(xml, options, callbackParam) {
let callback;
if (typeof options === "function" && callbackParam == null) {
callback = options;
options = {};
}
else {
callback = callbackParam;
options = (options ?? {});
}
const doc = new xmldom.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.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 (!isDomNode.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.findChildren(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();
}
}
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 "";
}
/**
* Generate the Reference nodes (as part of the signature process)
*
*/
createReferences(doc, prefix) {
let res = "";
prefix = prefix || "";
prefix = prefix ? `${prefix}:` : prefix;
for (const ref of this.getReferences()) {
const nodes = xpath.selectWithResolver(ref.xpath ?? "", 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;
}
getCanonXml(transforms, node, options = {}) {
options.defaultNsForPrefix = options.defaultNsForPrefix ?? SignedXml.defaultNsForPrefix;
options.signatureNode = this.signatureNode;
const canonXml = node.cloneNode(true); // Deep clone
let transformedXml = canonXml;
transforms.forEach((transformName) => {
if (isDomNode.isNodeLike(transformedXml)) {
// If, after processing, `transformedNode` is a string, we can't do anymore transforms on it
const transform = this.findCanonicalizationAlgorithm(transformName);
transformedXml = transform.process(transformedXml, 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 transformedXml.toString();
}
/**
* 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;
}
/**
* Create the SignedInfo element
*
*/
createSignedInfo(doc, prefix) {
if (typeof this.canonicalizationAlgorithm !== "string") {
throw new Error("Missing canonicalizationAlgorithm when trying to create signed info for XML");
}
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>`;
}
else {
res += " />";
}
res += `<${currentPrefix}SignatureMethod Algorithm="${algo.getAlgorithmName()}" />`;
res += this.createReferences(doc, prefix);
res += `</${currentPrefix}SignedInfo>`;
return res;
}
/**
* 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.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;
}
/**
* 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#",
};
SignedXml.noop = () => null;
//# sourceMappingURL=signed-xml.js.map