/* * Copyright 2018 Scytl Secure Electronic Voting SA * * All rights reserved * * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package */ cryptolib.modules.asymmetric = cryptolib.modules.asymmetric || {}; /** * Defines the asymmetric signers. * * @namespace asymmetric/xmlsigner */ cryptolib.modules.asymmetric.xmlsigner = function(box) { 'use strict'; box.asymmetric = box.asymmetric || {}; box.asymmetric.xmlsigner = {}; /** * A module that holds certificates functionalities. * * @exports asymmetric/xmlsigner/factory */ box.asymmetric.xmlsigner.factory = {}; var policies = { algorithm: box.policies.asymmetric.signer.algorithm, provider: box.policies.asymmetric.signer.provider, hash: box.policies.asymmetric.signer.hash }; var utils, exceptions, converters, parsers, stringUtils; var f = function(box) { exceptions = box.commons.exceptions; utils = box.commons.utils; converters = new box.commons.utils.Converters(); parsers = new utils.Parsers(); stringUtils = new utils.StringUtils(); }; cryptolib('commons', f); /** * A factory class for creating an XML digital signature verifier. * * @class */ box.asymmetric.xmlsigner.factory.XmlSignerFactory = function() {}; box.asymmetric.xmlsigner.factory.XmlSignerFactory.prototype = { /** * Gets a {@link asymmetric/xmlsigner.CryptoForgeXmlSigner}. * * @function * @return {asymmetric/xmlsigner.CryptoForgeXmlSigner} */ getCryptoXmlSigner: function() { try { if (policies.provider === Config.asymmetric.signer.provider.FORGE) { return new CryptoForgeXmlSigner(); } else { throw new exceptions.CryptoLibException( 'No suitable provider for the signer was provided.'); } } catch (error) { throw new exceptions.CryptoLibException( 'A CryptoXmlSigner could not be obtained.', error); } } }; /** * Holds xml signature properties. * * @class * @memberof asymmetric/xmlsigner */ function XmlSignature(signatureNode) { // go through all the nodes to load all relevant information var signedInfoNode; var signatureValueNode; var canonicalizationMethodNode; var signatureMethodNode; var referenceNode; var digestMethodNode; var digestValueNode; var transformsNode; try { signedInfoNode = new utils.XPath(signatureNode.getXml(), 'SignedInfo'); signatureValueNode = new utils.XPath(signatureNode.getXml(), 'SignatureValue'); canonicalizationMethodNode = new utils.XPath(signedInfoNode.getXml(), 'CanonicalizationMethod'); signatureMethodNode = new utils.XPath(signedInfoNode.getXml(), 'SignatureMethod'); referenceNode = new utils.XPath(signedInfoNode.getXml(), 'Reference'); digestMethodNode = new utils.XPath(referenceNode.getXml(), 'DigestMethod'); digestValueNode = new utils.XPath(referenceNode.getXml(), 'DigestValue'); transformsNode = new utils.XPath(referenceNode.getXml(), 'Transforms'); } catch (error) { throw new exceptions.CryptoLibException( 'Could not parse signed XML file', error); } // there are signatures without key, so we ignore them var rsaKeyValueNode; var modulusNode; var exponentNode; try { rsaKeyValueNode = new utils.XPath( signatureNode.getXml(), 'KeyInfo/KeyValue/RSAKeyValue'); modulusNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Modulus'); exponentNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Exponent'); } catch (error) { // ignore if this part fails } // build the object structure /** @property {object} */ this.info = { method: signatureMethodNode.getAttribute('Algorithm'), value: signatureValueNode.getValue(), meta: signatureNode.getAttribute('xmlns') }; /** @property {object} */ this.canonicalization = { method: canonicalizationMethodNode.getAttribute('Algorithm') }; /** @property {object} */ this.reference = { uri: referenceNode.getAttribute('URI'), transforms: [], digest: { method: digestMethodNode.getAttribute('Algorithm'), value: digestValueNode.getValue() } }; /** @property {object} */ this.rsakey = { modulus: modulusNode ? modulusNode.getValue() : '', exponent: exponentNode ? exponentNode.getValue() : '' }; // update the transforms references var transforms = transformsNode.getChildren(); for (var i = 0; i < transforms.length; i++) { if (transforms[i].nodeType === Node.ELEMENT_NODE) { var transformNode = new utils.XPath(transforms[i]); this.reference.transforms.push(transformNode.getAttribute('Algorithm')); } } } /** * @class * @memberof asymmetric/xmlsigner */ function ExclusiveCanonicalization() {} /** * Sorts the attributes * * @function * @param xmlNode * xml node, as a DOM. */ ExclusiveCanonicalization.prototype.sortAttributes = function(xmlNode) { // gather all attributes and remove them var list = []; var attr; if (xmlNode.attributes) { for (var i = 0; i < xmlNode.attributes.length; i++) { attr = xmlNode.attributes[i]; list.push({id: attr.name, val: xmlNode.getAttribute(attr.name)}); xmlNode.removeAttribute(attr.name); i--; } } // sort the attributes list.sort(function(a, b) { if (a.id < b.id) { return -1; } if (a.id > b.id) { return 1; } return 0; }); // reinsert the attributes for (var j = 0; j < list.length; j++) { attr = list[j]; xmlNode.setAttribute(attr.id, attr.val); } }; /** * Inits the process of canonicalization. * * @function * @param xmlNode * xml node, as a DOM. * * @return xml node processed, as DOM. */ ExclusiveCanonicalization.prototype.process = function(xml) { // traverse the tree through all the children for (var i = 0; i < xml.childNodes.length; i++) { var child = xml.childNodes[i]; if (child.nodeType === Node.ELEMENT_NODE) { this.process(child); } // if are comments or other stuff, remove them else if (child.nodeType !== Node.TEXT_NODE) { child.parentNode.removeChild(child); --i; } } // sort the attributes this.sortAttributes(xml); // return the final object return xml; }; /** * An xml signer. * * @class * @memberof asymmetric/xmlsigner */ function CryptoForgeXmlSigner() {} CryptoForgeXmlSigner.prototype = { /** * Verifies the digital signature of a selfsigned xml. * * @function * @param publicKey * Public key, as an object. * @param signedXml * XML data, as a string * @param signatureParentNode * The node where the signature is, as a string. * * @returns Boolean indicating whether signature verification was * successful. */ verify: function(publicKey, signedXml, signatureParentNode) { // clean the comments in the xml signedXml = this._removeXmlStringComments(signedXml); // loads the text to an xml var xml = parsers.xmlFromString(signedXml); // loads the xml signature structure to json var signature = this._loadSignature(xml, signatureParentNode); // check all the algorithms are the expected ones this._verifyAlgorithmsInSignature(signature); // check the public key is the expected one this._verifyPublicKeyInSignature(signature, publicKey); // check the references are well encoded this._validateReferences(signature, signedXml); // validates the signatures are right this._validateSignature(signature, publicKey); // if not exception raised, all went good return true; }, /** * Gets the XML signature node from the main XML node * * @function * @private * @param xml * XML where the signature is, as a Document Object * @param signatureParentNode * The node where the signature is, as a string. * * @returns Object with the DOM node of the signature and and the * signature in json. */ _loadSignature: function(xml, signatureParentNode) { // access to the node where the signature is, and take the methods try { // get the signature node var xmlNode = new utils.XPath(xml, signatureParentNode); // create signature object to load the data from the signature var data = new XmlSignature(xmlNode); // return the main object return {xmlNode: xmlNode, data: data}; } catch (error) { throw new exceptions.CryptoLibException( 'Could not load the signature from the XML', error); } }, /** * Verifies that the method algorithms in the EML and the properties * match. * * @function * @private * @param signature * Signature, as a Javascript Object */ _verifyAlgorithmsInSignature: function(signature) { try { if (signature.data.canonicalization.method !== 'http://www.w3.org/2001/10/xml-exc-c14n#') { throw new exceptions.CryptoLibException( 'Invalid canonicalization algorithm'); } if (!stringUtils.containsSubString( signature.data.info.method, policies.algorithm) && !stringUtils.containsSubString( signature.data.info.method, policies.hash)) { throw new exceptions.CryptoLibException( 'Invalid signature algorithm'); } if (!stringUtils.containsSubString( signature.data.reference.digest.method, policies.hash)) { throw new exceptions.CryptoLibException('Invalid digest algorithm'); } } catch (error) { throw new exceptions.CryptoLibException( 'Could not verify the data in the XML.', error); } }, /** * Verifies the public key is the same than the one in the EML. * * @function * @private * @param signature * Signature, as a Javascript Object * @param publicKey * Public key, as an object. */ _verifyPublicKeyInSignature: function(signature, publicKey) { // it has no key if (!signature.data.rsakey.exponent || !signature.data.rsakey.modulus) { return; } // if it has key, we validate it try { var signatureExponent = converters.base64ToBigInteger(signature.data.rsakey.exponent); var signatureModulus = converters.base64ToBigInteger(signature.data.rsakey.modulus); if (publicKey.e.compareTo(signatureExponent) !== 0 || publicKey.n.compareTo(signatureModulus) !== 0) { throw new exceptions.CryptoLibException('Invalid public key'); } } catch (error) { throw new exceptions.CryptoLibException( 'Could not verify the data in the XML.', error); } }, /** * Verifies the references of a selfsigned xml. * * @function * @private * @param signature * Signature, as a Javascript Object */ _validateReferences: function(signature, xmlString) { try { var xmlCanonString; // The canonicalization transformations are performed here. // Due to differences in the behaviour of Internet Explorer, // these transformations must be applied manually in the case // of that browser. For all other browsers, the transformations // are performed by applying a canonicalization algorithm. if (parsers.isIE()) { xmlCanonString = this._removeXmlStringHeader(xmlString); xmlCanonString = this._removeXmlStringSelfClosingTags(xmlCanonString); xmlCanonString = this._removeXmlStringSignature(xmlCanonString); xmlCanonString = this._removeXmlStringFirstCharacter(xmlCanonString); } else { var xmlCanon = null; // apply all transforms to the xml for (var i = 0; i < signature.data.reference.transforms.length; i++) { // get the algorithm name var algorithm = signature.data.reference.transforms[i]; // consume it if it exists if (algorithm in this._transformAlgorithmMap) { xmlCanon = this._transformAlgorithmMap[algorithm]( signature.xmlNode.getXml()); } } xmlCanon = this._removeXmlHeader(xmlCanon); xmlCanonString = parsers.xmlToString(xmlCanon, true); } var digester = this._getHashMethod(signature.data.reference.digest.method); digester.start(); digester.update( parsers.removeCarriageReturnChars(xmlCanonString), 'utf8'); var messageDigest = digester.digest(); // check if the result is valid var encodedMessage = forge.util.encode64(messageDigest.getBytes()); if (encodedMessage !== signature.data.reference.digest.value) { throw new exceptions.CryptoLibException( 'Invalid reference: \'' + encodedMessage + '\' when was expected \'' + signature.data.reference.digest.value + '\''); } } catch (error) { throw new exceptions.CryptoLibException( 'Could not validate references.', error); } }, /** * Verifies the digital signature of a selfsigned xml. * * @function * @private * @param signature * Signature, as a Javascript Object * @param publicKey * Public key, as an object. */ _validateSignature: function(signature, publicKey) { try { var signedInfoNode = new utils.XPath(signature.xmlNode.getXml(), '/SignedInfo'); var algorithm = signature.data.canonicalization.method; var xmlCanon = null; if (algorithm in this._transformAlgorithmMap) { xmlCanon = this._transformAlgorithmMap[algorithm](signedInfoNode.getXml()); } var digester = this._getHashMethod(signature.data.info.method); digester.start(); digester.update(parsers.xmlToString(xmlCanon, true), 'utf8'); var signatureValue = forge.util.decode64(signature.data.info.value); var result = publicKey.verify(digester.digest().getBytes(), signatureValue); if (!result) { throw new exceptions.CryptoLibException( 'Could not validate the signature.'); } } catch (error) { throw new exceptions.CryptoLibException( 'Could not validate signatures.', error); } }, /** * Creates the hash method * * @function * @private * @param method * method type, as string. * * @return Object, the disgester of that method. */ _getHashMethod: function(method) { if (stringUtils.containsSubString(method, 'sha256')) { return forge.md.sha256.create(); } }, /** * Map with all tarnsform algorithms * * @function * @private */ _transformAlgorithmMap: { /** * Removes the signature from a signed XML * * @param xmlSignature * the xml signature, as DOM. * * @return the XML top document, as DOM */ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature': function( xmlSignature) { var topElement = xmlSignature; // get the top document while (topElement.parentNode) { topElement = topElement.parentNode; } // remove the signature from the node xmlSignature.parentNode.removeChild(xmlSignature); return topElement; }, /** * c14n the XML * * @param xmlNode * the xml node, as DOM. * * @return the XML top document, as DOM */ 'http://www.w3.org/2001/10/xml-exc-c14n#': function(xmlNode) { var transform = new ExclusiveCanonicalization(); return transform.process(xmlNode); } }, /** * Removes the comments in the xml * * @function * @private * @param xml * the xml, as string. * * @return {string} the xml without comments */ _removeXmlStringComments: function(xml) { return xml.replace(//g, ''); }, /** * Removes the header in the xml structure. * * @function * @private * @param {object} * xmlDocument the xml, as DOM. * * @returns {object} the xml, as DOM without the header */ _removeXmlHeader: function(xmlDocument) { return xmlDocument.documentElement ? xmlDocument.documentElement : xmlDocument; }, /** * Removes the header in the XML string. * * @param xmlString * the xml, as a string. * * @return the xml string without the header */ _removeXmlStringHeader: function(xmlString) { return parsers.removeXmlHeaderFromString(xmlString); }, /** * Removes the signature node in the XML string. * * @param xmlString * the xml, as a string. * * @return the xml string without the signature tag */ _removeXmlStringSignature: function(xmlString) { return parsers.removeXmlSignature(xmlString); }, /** * Converts any self-closing tags to opening and closing tags, within an * XML string. * * @param xmlString * the xml, as a string. * * @return the xml string with self-closing tags converted to opening * and closing tags */ _removeXmlStringSelfClosingTags: function(xmlString) { return parsers.xmlRemoveSelfClosingTags(xmlString); }, /** * Removes the first character from an XML string if that character is * not the expected initial character. * * @param xmlString * the xml, as a string. * * @return the xml string with the initial character removed if it is * not wanted */ _removeXmlStringFirstCharacter: function(xmlString) { if (xmlString[0] !== '<') { return xmlString.slice(1, xmlString.length); } else { return xmlString; } } }; };