UNPKG

xmldsigjs

Version:

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

515 lines (514 loc) 21.4 kB
import { XmlObject, XmlNodeType, isDocument, isElement, assign, XmlError, XE, Parse, SelectNamespaces, Stringify, } from 'xml-core'; import { BufferSourceConverter, Convert } from 'pvtsutils'; import * as Alg from './algorithms/index.js'; import { CryptoConfig } from './crypto_config.js'; import { KeyInfo, Reference, References, Signature, Transforms as XmlTransforms, XmlDsigC14NWithCommentsTransform, XmlDsigExcC14NWithCommentsTransform, } from './xml/index.js'; import { KeyInfoX509Data, KeyValue } from './xml/key_infos/index.js'; import * as KeyInfos from './xml/key_infos/index.js'; import * as Transforms from './xml/transforms/index.js'; import { Application } from './application.js'; export class SignedXml { get XmlSignature() { return this.signature; } get Signature() { return this.XmlSignature.SignatureValue; } constructor(node) { this.signature = new Signature(); this.replaceCanonicalization = false; if (node && node.nodeType === XmlNodeType.Document) { this.document = node; } else if (node && node.nodeType === XmlNodeType.Element) { const xmlText = Stringify(node); this.document = Parse(xmlText); } } async Sign(algorithm, key, data, options = {}) { if (isDocument(data)) { data = data.cloneNode(true).documentElement; } else if (isElement(data)) { data = data.cloneNode(true); } const signingAlg = assign({}, algorithm, key.algorithm); if (key.algorithm['hash']) { signingAlg.hash = key.algorithm['hash']; } const alg = CryptoConfig.GetSignatureAlgorithm(signingAlg); await this.ApplySignOptions(this.XmlSignature, algorithm, key, options); await this.DigestReferences(data); const signatureMethod = 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 (isElement(data)) { this.document = data.ownerDocument; } return this.XmlSignature; } async reimportKey(key, alg) { const spki = await Application.crypto.subtle.exportKey('spki', key); return 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 XmlError(XE.NULL_PARAM, 'SignedXml', 'document'); } content = xml.documentElement; } if (isDocument(content) || 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 = Signature.LoadXml(value); this.Algorithm = 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 XmlError(XE.XML_EXCEPTION, 'Document is not defined'); } const doc = this.document.documentElement.cloneNode(true); const node = this.XmlSignature.GetXml(); if (!node) { throw new XmlError(XE.XML_EXCEPTION, 'Cannot get Xml element from Signature'); } const sig = node.cloneNode(true); doc.appendChild(sig); return Stringify(doc); } return this.XmlSignature.toString(); } async GetPublicKeys() { const keys = []; const alg = 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.crypto.subtle.exportKey('spki', key); const updatedKey = await 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 = 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 (isElement(source)) { this.CopyNamespaces(source, el, false); } if (this.Parent) { const parentXml = this.Parent instanceof 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 && isElement(source)) { found = 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 XmlError(XE.CRYPTOGRAPHIC, `Cannot get object by reference: ${objectName}`); } } } let canonOutput = null; if (reference.Transforms && reference.Transforms.Count) { if (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 (isElement(source)) { if (!source.ownerDocument) { throw new Error('Cannot get ownerDocument from the XML document'); } canonOutput = Stringify(source.ownerDocument); } else { canonOutput = BufferSourceConverter.toArrayBuffer(source); } } else { const excC14N = new Transforms.XmlDsigC14NTransform(); if (BufferSourceConverter.isBufferSource(source)) { source = Parse(Convert.ToUtf8String(source)).documentElement; } excC14N.LoadInnerXml(source); canonOutput = excC14N.GetOutput(); } } if (!reference.DigestMethod.Algorithm) { throw new XmlError(XE.NULL_PARAM, 'Reference', 'DigestMethod'); } const digest = 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 = CryptoConfig.CreateFromName(this.XmlSignature.SignedInfo.CanonicalizationMethod.Algorithm); const xml = this.XmlSignature.SignedInfo.GetXml(); if (!xml) { throw new XmlError(XE.XML_EXCEPTION, 'Cannot get Xml element from SignedInfo'); } const node = xml.cloneNode(true); this.CopyNamespaces(xml, node, false); if (data && !BufferSourceConverter.isBufferSource(data)) { if (data.nodeType === XmlNodeType.Document) { this.CopyNamespaces(data.documentElement, node, false); } else { this.CopyNamespaces(data, node, false); } } if (this.Parent) { const parentXml = this.Parent instanceof XmlObject ? this.Parent.GetXml() : this.Parent; if (parentXml) { this.CopyNamespaces(parentXml, node, false); } } const childNamespaces = 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 XmlError(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 XmlError(XE.CRYPTOGRAPHIC_UNKNOWN_TRANSFORM, transform.name); } } ApplyTransforms(transforms, input) { let output = null; transforms .Sort((a, b) => { const c14nTransforms = [ Transforms.XmlDsigC14NTransform, XmlDsigC14NWithCommentsTransform, Transforms.XmlDsigExcC14NTransform, 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 KeyInfo(); } const keyInfo = signature.KeyInfo; const keyValue = new KeyValue(); keyInfo.Add(keyValue); await keyValue.importKey(options.keyValue); } if (options.x509) { if (!signature.KeyInfo) { signature.KeyInfo = new KeyInfo(); } const keyInfo = signature.KeyInfo; options.x509.forEach((x509) => { const raw = BufferSourceConverter.toUint8Array(Convert.FromBase64(x509)); const x509Data = new KeyInfoX509Data(raw); keyInfo.Add(x509Data); }); } if (options.references) { options.references.forEach((item) => { const reference = new 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 = CryptoConfig.GetHashAlgorithm(item.hash); reference.DigestMethod.Algorithm = digestAlgorithm.namespaceURI; if (item.transforms && item.transforms.length) { const transforms = new XmlTransforms(); item.transforms.forEach((transform) => { transforms.Add(this.ResolveTransform(transform)); }); reference.Transforms = transforms; } if (!signature.SignedInfo.References) { signature.SignedInfo.References = new References(); } signature.SignedInfo.References.Add(reference); }); } if (!signature.SignedInfo.References.Count) { const reference = new 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 = Convert.ToBase64(digest); const b64DigestValue = 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 XmlError(XE.CRYPTOGRAPHIC, errText); } } return true; } async ValidateSignatureValue(keys) { const signedInfoCanon = this.TransformSignedInfo(this.document); const signer = CryptoConfig.CreateSignatureAlgorithm(this.XmlSignature.SignedInfo.SignatureMethod); for (const key of keys) { if (!this.Signature) { throw new XmlError(XE.CRYPTOGRAPHIC, 'Signature is not defined'); } const ok = await signer.Verify(signedInfoCanon, key, this.Signature); if (ok) { return true; } } return false; } } function findById(element, id) { if (element.nodeType !== 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 (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); } } } export function SelectRootNamespaces(node) { const attrs = {}; _SelectRootNamespaces(node, attrs); return attrs; }