UNPKG

xmldsigjs

Version:

XML Digital Signature implementation in TypeScript/JavaScript using Web Crypto API

521 lines (520 loc) 22.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SignedXml = void 0; exports.SelectRootNamespaces = SelectRootNamespaces; const tslib_1 = require("tslib"); const xml_core_1 = require("xml-core"); const pvtsutils_1 = require("pvtsutils"); const Alg = tslib_1.__importStar(require("./algorithms/index.js")); const crypto_config_js_1 = require("./crypto_config.js"); const index_js_1 = require("./xml/index.js"); const index_js_2 = require("./xml/key_infos/index.js"); const KeyInfos = tslib_1.__importStar(require("./xml/key_infos/index.js")); const Transforms = tslib_1.__importStar(require("./xml/transforms/index.js")); const application_js_1 = require("./application.js"); class SignedXml { get XmlSignature() { return this.signature; } get Signature() { return this.XmlSignature.SignatureValue; } constructor(node) { this.signature = new index_js_1.Signature(); this.replaceCanonicalization = false; if (node && node.nodeType === xml_core_1.XmlNodeType.Document) { this.document = node; } else if (node && node.nodeType === xml_core_1.XmlNodeType.Element) { const xmlText = (0, xml_core_1.Stringify)(node); this.document = (0, xml_core_1.Parse)(xmlText); } } async Sign(algorithm, key, data, options = {}) { if ((0, xml_core_1.isDocument)(data)) { data = data.cloneNode(true).documentElement; } else if ((0, xml_core_1.isElement)(data)) { data = data.cloneNode(true); } const signingAlg = (0, xml_core_1.assign)({}, algorithm, key.algorithm); if (key.algorithm['hash']) { signingAlg.hash = key.algorithm['hash']; } const alg = crypto_config_js_1.CryptoConfig.GetSignatureAlgorithm(signingAlg); await this.ApplySignOptions(this.XmlSignature, algorithm, key, options); await this.DigestReferences(data); const signatureMethod = crypto_config_js_1.CryptoConfig.CreateSignatureMethod(alg); this.XmlSignature.SignedInfo.SignatureMethod = signatureMethod; const si = this.TransformSignedInfo(data); const signature = await alg.Sign(si, key, signingAlg); this.Key = key; this.Algorithm = algorithm; this.XmlSignature.SignatureValue = new Uint8Array(signature); if ((0, xml_core_1.isElement)(data)) { this.document = data.ownerDocument; } return this.XmlSignature; } async reimportKey(key, alg) { const spki = await application_js_1.Application.crypto.subtle.exportKey('spki', key); return application_js_1.Application.crypto.subtle.importKey('spki', spki, alg, true, ['verify']); } async Verify(params) { let content; let key; if (params) { if ('algorithm' in params && 'usages' in params && 'type' in params) { key = params; } else { key = params.key; content = params.content; } } if (key && key.type === 'public' && this.Algorithm) { key = await this.reimportKey(key, this.Algorithm); } if (!content) { const xml = this.document; if (!(xml && xml.documentElement)) { throw new xml_core_1.XmlError(xml_core_1.XE.NULL_PARAM, 'SignedXml', 'document'); } content = xml.documentElement; } if ((0, xml_core_1.isDocument)(content) || (0, xml_core_1.isElement)(content)) { content = content.cloneNode(true); } const res = await this.ValidateReferences(content); if (res) { const keys = key ? [key] : await this.GetPublicKeys(); return this.ValidateSignatureValue(keys); } else { return false; } } GetXml() { return this.signature.GetXml(); } LoadXml(value) { this.signature = index_js_1.Signature.LoadXml(value); this.Algorithm = crypto_config_js_1.CryptoConfig.CreateSignatureAlgorithm(this.XmlSignature.SignedInfo.SignatureMethod).algorithm; } toString() { const signature = this.XmlSignature; const enveloped = signature.SignedInfo.References && signature.SignedInfo.References.Some((r) => r.Transforms && r.Transforms.Some((t) => t instanceof Transforms.XmlDsigEnvelopedSignatureTransform)); if (enveloped) { if (!this.document) { throw new xml_core_1.XmlError(xml_core_1.XE.XML_EXCEPTION, 'Document is not defined'); } const doc = this.document.documentElement.cloneNode(true); const node = this.XmlSignature.GetXml(); if (!node) { throw new xml_core_1.XmlError(xml_core_1.XE.XML_EXCEPTION, 'Cannot get Xml element from Signature'); } const sig = node.cloneNode(true); doc.appendChild(sig); return (0, xml_core_1.Stringify)(doc); } return this.XmlSignature.toString(); } async GetPublicKeys() { const keys = []; const alg = crypto_config_js_1.CryptoConfig.CreateSignatureAlgorithm(this.XmlSignature.SignedInfo.SignatureMethod); for (const kic of this.XmlSignature.KeyInfo.GetIterator()) { if (kic instanceof KeyInfos.KeyInfoX509Data) { for (const cert of kic.Certificates) { const key = await cert.exportKey(); keys.push(key); } } else { const key = await kic.exportKey(); keys.push(key); } } if (alg.algorithm.name.startsWith('RSA')) { for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key.algorithm.name.startsWith('RSA')) { const spki = await application_js_1.Application.crypto.subtle.exportKey('spki', key); const updatedKey = await application_js_1.Application.crypto.subtle.importKey('spki', spki, alg.algorithm, true, ['verify']); keys[i] = updatedKey; } } } return keys; } GetSignatureNamespaces() { const namespaces = {}; if (this.XmlSignature.NamespaceURI) { namespaces[this.XmlSignature.Prefix || ''] = this.XmlSignature.NamespaceURI; } return namespaces; } CopyNamespaces(src, dst, ignoreDefault) { this.InjectNamespaces(SelectRootNamespaces(src), dst, ignoreDefault); } InjectNamespaces(namespaces, target, ignoreDefault) { for (const i in namespaces) { const uri = namespaces[i]; if (ignoreDefault && i === '') { continue; } target.setAttribute('xmlns' + (i ? ':' + i : ''), uri); } } async DigestReference(source, reference, _checkHmac) { if (this.contentHandler) { const content = await this.contentHandler(reference, this); if (content) { source = (0, xml_core_1.isDocument)(content) ? content.documentElement : content; } } if (reference.Uri) { let objectName; if (!reference.Uri.indexOf('#xpointer')) { let uri = reference.Uri; uri = uri.substring(9).replace(/[\r\n\t\s]/g, ''); if (uri.length < 2 || uri[0] !== `(` || uri[uri.length - 1] !== `)`) { uri = ''; } else { uri = uri.substring(1, uri.length - 1); } if (uri.length > 6 && uri.indexOf(`id(`) === 0 && uri[uri.length - 1] === `)`) { objectName = uri.substring(4, uri.length - 2); } } else if (reference.Uri[0] === `#`) { objectName = reference.Uri.substring(1); } if (objectName) { let found = null; const xmlSignatureObjects = [this.XmlSignature.KeyInfo.GetXml()]; this.XmlSignature.ObjectList.ForEach((object) => { xmlSignatureObjects.push(object.GetXml()); }); for (const xmlSignatureObject of xmlSignatureObjects) { if (xmlSignatureObject) { found = findById(xmlSignatureObject, objectName || ''); if (found) { const el = found.cloneNode(true); if ((0, xml_core_1.isElement)(source)) { this.CopyNamespaces(source, el, false); } if (this.Parent) { const parentXml = this.Parent instanceof xml_core_1.XmlObject ? this.Parent.GetXml() : this.Parent; if (parentXml) { this.CopyNamespaces(parentXml, el, true); } } this.CopyNamespaces(found, el, false); this.InjectNamespaces(this.GetSignatureNamespaces(), el, true); source = el; break; } } } if (!found && source && (0, xml_core_1.isElement)(source)) { found = xml_core_1.XmlObject.GetElementById(source, objectName); if (found) { const el = found.cloneNode(true); this.CopyNamespaces(found, el, false); this.CopyNamespaces(source, el, false); source = el; } } if (found == null) { throw new xml_core_1.XmlError(xml_core_1.XE.CRYPTOGRAPHIC, `Cannot get object by reference: ${objectName}`); } } } let canonOutput = null; if (reference.Transforms && reference.Transforms.Count) { if (pvtsutils_1.BufferSourceConverter.isBufferSource(source)) { throw new Error(`Transformation for argument 'source' of type BufferSource is not implemented`); } canonOutput = this.ApplyTransforms(reference.Transforms, source); } else { if (reference.Uri && reference.Uri[0] !== `#`) { if ((0, xml_core_1.isElement)(source)) { if (!source.ownerDocument) { throw new Error('Cannot get ownerDocument from the XML document'); } canonOutput = (0, xml_core_1.Stringify)(source.ownerDocument); } else { canonOutput = pvtsutils_1.BufferSourceConverter.toArrayBuffer(source); } } else { const excC14N = new Transforms.XmlDsigC14NTransform(); if (pvtsutils_1.BufferSourceConverter.isBufferSource(source)) { source = (0, xml_core_1.Parse)(pvtsutils_1.Convert.ToUtf8String(source)).documentElement; } excC14N.LoadInnerXml(source); canonOutput = excC14N.GetOutput(); } } if (!reference.DigestMethod.Algorithm) { throw new xml_core_1.XmlError(xml_core_1.XE.NULL_PARAM, 'Reference', 'DigestMethod'); } const digest = crypto_config_js_1.CryptoConfig.CreateHashAlgorithm(reference.DigestMethod.Algorithm); return digest.Digest(canonOutput); } async DigestReferences(data) { for (const ref of this.XmlSignature.SignedInfo.References.GetIterator()) { if (ref.DigestValue) { continue; } if (!ref.DigestMethod.Algorithm) { ref.DigestMethod.Algorithm = new Alg.Sha256().namespaceURI; } const hash = await this.DigestReference(data, ref, false); ref.DigestValue = hash; } } TransformSignedInfo(data) { const t = crypto_config_js_1.CryptoConfig.CreateFromName(this.XmlSignature.SignedInfo.CanonicalizationMethod.Algorithm); const xml = this.XmlSignature.SignedInfo.GetXml(); if (!xml) { throw new xml_core_1.XmlError(xml_core_1.XE.XML_EXCEPTION, 'Cannot get Xml element from SignedInfo'); } const node = xml.cloneNode(true); this.CopyNamespaces(xml, node, false); if (data && !pvtsutils_1.BufferSourceConverter.isBufferSource(data)) { if (data.nodeType === xml_core_1.XmlNodeType.Document) { this.CopyNamespaces(data.documentElement, node, false); } else { this.CopyNamespaces(data, node, false); } } if (this.Parent) { const parentXml = this.Parent instanceof xml_core_1.XmlObject ? this.Parent.GetXml() : this.Parent; if (parentXml) { this.CopyNamespaces(parentXml, node, false); } } const childNamespaces = (0, xml_core_1.SelectNamespaces)(xml); for (const i in childNamespaces) { const uri = childNamespaces[i]; if (i === node.prefix) { continue; } node.setAttribute('xmlns' + (i ? ':' + i : ''), uri); } t.LoadInnerXml(node); const res = t.GetOutput(); return res; } ResolveTransform(transform) { if (typeof transform === 'string') { switch (transform) { case 'enveloped': return new Transforms.XmlDsigEnvelopedSignatureTransform(); case 'c14n': return new Transforms.XmlDsigC14NTransform(); case 'c14n-com': return new Transforms.XmlDsigC14NWithCommentsTransform(); case 'exc-c14n': return new Transforms.XmlDsigExcC14NTransform(); case 'exc-c14n-com': return new Transforms.XmlDsigExcC14NWithCommentsTransform(); case 'base64': return new Transforms.XmlDsigBase64Transform(); default: throw new xml_core_1.XmlError(xml_core_1.XE.CRYPTOGRAPHIC_UNKNOWN_TRANSFORM, transform); } } switch (transform.name) { case 'xpath': { const xpathTransform = new Transforms.XmlDsigXPathTransform(); xpathTransform.XPath = transform.selector; const transformEl = xpathTransform.GetXml(); if (transformEl && transform.namespaces) { for (const [prefix, namespace] of Object.entries(transform.namespaces)) { transformEl.firstChild.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${prefix}`, namespace); } } return xpathTransform; } default: throw new xml_core_1.XmlError(xml_core_1.XE.CRYPTOGRAPHIC_UNKNOWN_TRANSFORM, transform.name); } } ApplyTransforms(transforms, input) { let output = null; transforms .Sort((a, b) => { const c14nTransforms = [ Transforms.XmlDsigC14NTransform, index_js_1.XmlDsigC14NWithCommentsTransform, Transforms.XmlDsigExcC14NTransform, index_js_1.XmlDsigExcC14NWithCommentsTransform, ]; if (c14nTransforms.some((t) => a instanceof t)) { return 1; } if (c14nTransforms.some((t) => b instanceof t)) { return -1; } return 0; }) .ForEach((transform) => { if (this.replaceCanonicalization) { if (transform instanceof Transforms.XmlDsigExcC14NWithCommentsTransform) { transform = new Transforms.XmlDsigExcC14NTransform(); } else if (transform instanceof Transforms.XmlDsigC14NWithCommentsTransform) { transform = new Transforms.XmlDsigC14NTransform(); } } transform.LoadInnerXml(input); if (transform instanceof Transforms.XmlDsigXPathTransform) { transform.GetOutput(); } else { output = transform.GetOutput(); } }); if (transforms.Count === 1 && transforms.Item(0) instanceof Transforms.XmlDsigEnvelopedSignatureTransform) { const c14n = new Transforms.XmlDsigC14NTransform(); c14n.LoadInnerXml(input); output = c14n.GetOutput(); } return output; } async ApplySignOptions(signature, algorithm, key, options) { if (options.id) { this.XmlSignature.Id = options.id; } if (options.keyValue && key.algorithm.name && key.algorithm.name.toUpperCase() !== Alg.HMAC) { if (!signature.KeyInfo) { signature.KeyInfo = new index_js_1.KeyInfo(); } const keyInfo = signature.KeyInfo; const keyValue = new index_js_2.KeyValue(); keyInfo.Add(keyValue); await keyValue.importKey(options.keyValue); } if (options.x509) { if (!signature.KeyInfo) { signature.KeyInfo = new index_js_1.KeyInfo(); } const keyInfo = signature.KeyInfo; options.x509.forEach((x509) => { const raw = pvtsutils_1.BufferSourceConverter.toUint8Array(pvtsutils_1.Convert.FromBase64(x509)); const x509Data = new index_js_2.KeyInfoX509Data(raw); keyInfo.Add(x509Data); }); } if (options.references) { options.references.forEach((item) => { const reference = new index_js_1.Reference(); if (item.id) { reference.Id = item.id; } if (item.uri !== null && item.uri !== undefined) { reference.Uri = item.uri; } if (item.type) { reference.Type = item.type; } const digestAlgorithm = crypto_config_js_1.CryptoConfig.GetHashAlgorithm(item.hash); reference.DigestMethod.Algorithm = digestAlgorithm.namespaceURI; if (item.transforms && item.transforms.length) { const transforms = new index_js_1.Transforms(); item.transforms.forEach((transform) => { transforms.Add(this.ResolveTransform(transform)); }); reference.Transforms = transforms; } if (!signature.SignedInfo.References) { signature.SignedInfo.References = new index_js_1.References(); } signature.SignedInfo.References.Add(reference); }); } if (!signature.SignedInfo.References.Count) { const reference = new index_js_1.Reference(); signature.SignedInfo.References.Add(reference); } } async ValidateReferences(doc) { for (const ref of this.XmlSignature.SignedInfo.References.GetIterator()) { const digest = await this.DigestReference(doc, ref, false); const b64Digest = pvtsutils_1.Convert.ToBase64(digest); const b64DigestValue = pvtsutils_1.Convert.ToString(ref.DigestValue, 'base64'); if (b64Digest !== b64DigestValue) { const errText = `Invalid digest for uri '${ref.Uri}'. Calculated digest is ${b64Digest} but the xml to validate supplies digest ${b64DigestValue}`; throw new xml_core_1.XmlError(xml_core_1.XE.CRYPTOGRAPHIC, errText); } } return true; } async ValidateSignatureValue(keys) { const signedInfoCanon = this.TransformSignedInfo(this.document); const signer = crypto_config_js_1.CryptoConfig.CreateSignatureAlgorithm(this.XmlSignature.SignedInfo.SignatureMethod); for (const key of keys) { if (!this.Signature) { throw new xml_core_1.XmlError(xml_core_1.XE.CRYPTOGRAPHIC, 'Signature is not defined'); } const ok = await signer.Verify(signedInfoCanon, key, this.Signature); if (ok) { return true; } } return false; } } exports.SignedXml = SignedXml; function findById(element, id) { if (element.nodeType !== xml_core_1.XmlNodeType.Element) { return null; } if (element.hasAttribute('Id') && element.getAttribute('Id') === id) { return element; } if (element.childNodes && element.childNodes.length) { for (let i = 0; i < element.childNodes.length; i++) { const el = findById(element.childNodes[i], id); if (el) { return el; } } } return null; } function addNamespace(selectedNodes, name, namespace) { if (!(name in selectedNodes)) { selectedNodes[name] = namespace; } } function _SelectRootNamespaces(node, selectedNodes = {}) { if ((0, xml_core_1.isElement)(node)) { if (node.namespaceURI && node.namespaceURI !== 'http://www.w3.org/XML/1998/namespace') { addNamespace(selectedNodes, node.prefix ? node.prefix : '', node.namespaceURI || ''); } for (let i = 0; i < node.attributes.length; i++) { const attr = node.attributes.item(i); if (attr && attr.prefix === 'xmlns') { addNamespace(selectedNodes, attr.localName ? attr.localName : '', attr.value); } } if (node.parentNode) { _SelectRootNamespaces(node.parentNode, selectedNodes); } } } function SelectRootNamespaces(node) { const attrs = {}; _SelectRootNamespaces(node, attrs); return attrs; }