xmldsigjs
Version:
XML Digital Signature implementation in TypeScript/JavaScript using Web Crypto API
521 lines (520 loc) • 22.4 kB
JavaScript
"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;
}