UNPKG

xml-crypto

Version:

Xml digital signature and encryption library for Node.js

944 lines 45 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SignedXml = void 0; const isDomNode = require("@xmldom/is-dom-node"); const xmldom = require("@xmldom/xmldom"); const util_1 = require("util"); const xpath = require("xpath"); const c14n = require("./c14n-canonicalization"); const envelopedSignatures = require("./enveloped-signature"); const execC14n = require("./exclusive-canonicalization"); const hashAlgorithms = require("./hash-algorithms"); const signatureAlgorithms = require("./signature-algorithms"); const utils = require("./utils"); 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 = []; /** * Contains the canonicalized XML of the references that were validly signed. * * This populates with the canonical XML of the reference only after * verifying the signature is cryptographically authentic. */ this.signedReferences = []; /** * 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, }; // TODO: In v7.x we may consider deprecating sha1 /** * 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, }; // TODO: In v7.x we may consider deprecating sha1 /** * 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 }; /** * @deprecated Use `.getSignedReferences()` instead. * Returns the list of references. */ this.getReferences = (0, util_1.deprecate)(() => this.references, "getReferences() is deprecated. Use `.getSignedReferences()` instead."); 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 = []; const unverifiedSignedInfoCanon = this.getCanonSignedInfoXml(doc); if (!unverifiedSignedInfoCanon) { if (callback) { callback(new Error("Canonical signed info cannot be empty"), false); return; } throw new Error("Canonical signed info cannot be empty"); } // unsigned, verify later to keep with consistent callback behavior const parsedUnverifiedSignedInfo = new xmldom.DOMParser().parseFromString(unverifiedSignedInfoCanon, "text/xml"); const unverifiedSignedInfoDoc = parsedUnverifiedSignedInfo.documentElement; if (!unverifiedSignedInfoDoc) { if (callback) { callback(new Error("Could not parse unverifiedSignedInfoCanon into a document"), false); return; } throw new Error("Could not parse unverifiedSignedInfoCanon into a document"); } const references = utils.findChildren(unverifiedSignedInfoDoc, "Reference"); if (!utils.isArrayHasLength(references)) { if (callback) { callback(new Error("could not find any Reference elements"), false); return; } 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); } /* eslint-disable-next-line deprecation/deprecation */ if (!this.getReferences().every((ref) => this.validateReference(ref, doc))) { /* Trustworthiness can only be determined if SignedInfo's (which holds References' DigestValue(s) which were validated at this stage) signature is valid. Execution does not proceed to validate signature phase thus each References' DigestValue must be considered to be untrusted (attacker might have injected any data with new new references and/or recalculated new DigestValue for altered Reference(s)). Returning any content via `signedReferences` would give false sense of trustworthiness if/when SignedInfo's (which holds references' DigestValues) signature is not valid(ated). Put simply: if one fails, they are all not trustworthy. */ this.signedReferences = []; if (callback) { callback(new Error("Could not validate all references"), false); return; } // We return false because some references validated, but not all // We should actually be throwing an error here, but that would be a breaking change // See https://www.w3.org/TR/xmldsig-core/#sec-CoreValidation return false; } // (Stage B authentication step, show that the `signedInfoCanon` is signed) // First find the key & signature algorithm, these should match // 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"); } // Check the signature verification to know whether to reset signature value or not. const sigRes = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue); if (sigRes === true) { if (callback) { callback(null, true); } else { return true; } } else { // Ideally, we would start by verifying the `signedInfoCanon` first, // but that may cause some breaking changes, so we'll handle that in v7.x. // If we were validating `signedInfoCanon` first, we wouldn't have to reset this array. this.signedReferences = []; if (callback) { callback(new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`)); return; // return early } else { throw new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`); } } } 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; } /* eslint-disable-next-line deprecation/deprecation */ 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; } // This step can only be done after we have verified the `signedInfo`. // We verified that they have same hash, // thus the `canonXml` and _only_ the `canonXml` can be trusted. // Append this to `signedReferences`. this.signedReferences.push(canonXml); 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()`?"); }, }); } getSignedReferences() { return [...this.signedReferences]; } 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; /* eslint-disable-next-line deprecation/deprecation */ 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