asymmetric.xmlsigner.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. /*
  2. * Copyright 2018 Scytl Secure Electronic Voting SA
  3. *
  4. * All rights reserved
  5. *
  6. * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
  7. */
  8. cryptolib.modules.asymmetric = cryptolib.modules.asymmetric || {};
  9. /**
  10. * Defines the asymmetric signers.
  11. *
  12. * @namespace asymmetric/xmlsigner
  13. */
  14. cryptolib.modules.asymmetric.xmlsigner = function(box) {
  15. 'use strict';
  16. box.asymmetric = box.asymmetric || {};
  17. box.asymmetric.xmlsigner = {};
  18. /**
  19. * A module that holds certificates functionalities.
  20. *
  21. * @exports asymmetric/xmlsigner/factory
  22. */
  23. box.asymmetric.xmlsigner.factory = {};
  24. var policies = {
  25. algorithm: box.policies.asymmetric.signer.algorithm,
  26. provider: box.policies.asymmetric.signer.provider,
  27. hash: box.policies.asymmetric.signer.hash
  28. };
  29. var utils, exceptions, converters, parsers, stringUtils;
  30. var f = function(box) {
  31. exceptions = box.commons.exceptions;
  32. utils = box.commons.utils;
  33. converters = new box.commons.utils.Converters();
  34. parsers = new utils.Parsers();
  35. stringUtils = new utils.StringUtils();
  36. };
  37. cryptolib('commons', f);
  38. /**
  39. * A factory class for creating an XML digital signature verifier.
  40. *
  41. * @class
  42. */
  43. box.asymmetric.xmlsigner.factory.XmlSignerFactory = function() {};
  44. box.asymmetric.xmlsigner.factory.XmlSignerFactory.prototype = {
  45. /**
  46. * Gets a {@link asymmetric/xmlsigner.CryptoForgeXmlSigner}.
  47. *
  48. * @function
  49. * @return {asymmetric/xmlsigner.CryptoForgeXmlSigner}
  50. */
  51. getCryptoXmlSigner: function() {
  52. try {
  53. if (policies.provider === Config.asymmetric.signer.provider.FORGE) {
  54. return new CryptoForgeXmlSigner();
  55. } else {
  56. throw new exceptions.CryptoLibException(
  57. 'No suitable provider for the signer was provided.');
  58. }
  59. } catch (error) {
  60. throw new exceptions.CryptoLibException(
  61. 'A CryptoXmlSigner could not be obtained.', error);
  62. }
  63. }
  64. };
  65. /**
  66. * Holds xml signature properties.
  67. *
  68. * @class
  69. * @memberof asymmetric/xmlsigner
  70. */
  71. function XmlSignature(signatureNode) {
  72. // go through all the nodes to load all relevant information
  73. var signedInfoNode;
  74. var signatureValueNode;
  75. var canonicalizationMethodNode;
  76. var signatureMethodNode;
  77. var referenceNode;
  78. var digestMethodNode;
  79. var digestValueNode;
  80. var transformsNode;
  81. try {
  82. signedInfoNode = new utils.XPath(signatureNode.getXml(), 'SignedInfo');
  83. signatureValueNode =
  84. new utils.XPath(signatureNode.getXml(), 'SignatureValue');
  85. canonicalizationMethodNode =
  86. new utils.XPath(signedInfoNode.getXml(), 'CanonicalizationMethod');
  87. signatureMethodNode =
  88. new utils.XPath(signedInfoNode.getXml(), 'SignatureMethod');
  89. referenceNode = new utils.XPath(signedInfoNode.getXml(), 'Reference');
  90. digestMethodNode =
  91. new utils.XPath(referenceNode.getXml(), 'DigestMethod');
  92. digestValueNode = new utils.XPath(referenceNode.getXml(), 'DigestValue');
  93. transformsNode = new utils.XPath(referenceNode.getXml(), 'Transforms');
  94. } catch (error) {
  95. throw new exceptions.CryptoLibException(
  96. 'Could not parse signed XML file', error);
  97. }
  98. // there are signatures without key, so we ignore them
  99. var rsaKeyValueNode;
  100. var modulusNode;
  101. var exponentNode;
  102. try {
  103. rsaKeyValueNode = new utils.XPath(
  104. signatureNode.getXml(), 'KeyInfo/KeyValue/RSAKeyValue');
  105. modulusNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Modulus');
  106. exponentNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Exponent');
  107. } catch (error) {
  108. // ignore if this part fails
  109. }
  110. // build the object structure
  111. /** @property {object} */
  112. this.info = {
  113. method: signatureMethodNode.getAttribute('Algorithm'),
  114. value: signatureValueNode.getValue(),
  115. meta: signatureNode.getAttribute('xmlns')
  116. };
  117. /** @property {object} */
  118. this.canonicalization = {
  119. method: canonicalizationMethodNode.getAttribute('Algorithm')
  120. };
  121. /** @property {object} */
  122. this.reference = {
  123. uri: referenceNode.getAttribute('URI'),
  124. transforms: [],
  125. digest: {
  126. method: digestMethodNode.getAttribute('Algorithm'),
  127. value: digestValueNode.getValue()
  128. }
  129. };
  130. /** @property {object} */
  131. this.rsakey = {
  132. modulus: modulusNode ? modulusNode.getValue() : '',
  133. exponent: exponentNode ? exponentNode.getValue() : ''
  134. };
  135. // update the transforms references
  136. var transforms = transformsNode.getChildren();
  137. for (var i = 0; i < transforms.length; i++) {
  138. if (transforms[i].nodeType === Node.ELEMENT_NODE) {
  139. var transformNode = new utils.XPath(transforms[i]);
  140. this.reference.transforms.push(transformNode.getAttribute('Algorithm'));
  141. }
  142. }
  143. }
  144. /**
  145. * @class
  146. * @memberof asymmetric/xmlsigner
  147. */
  148. function ExclusiveCanonicalization() {}
  149. /**
  150. * Sorts the attributes
  151. *
  152. * @function
  153. * @param xmlNode
  154. * xml node, as a DOM.
  155. */
  156. ExclusiveCanonicalization.prototype.sortAttributes = function(xmlNode) {
  157. // gather all attributes and remove them
  158. var list = [];
  159. var attr;
  160. if (xmlNode.attributes) {
  161. for (var i = 0; i < xmlNode.attributes.length; i++) {
  162. attr = xmlNode.attributes[i];
  163. list.push({id: attr.name, val: xmlNode.getAttribute(attr.name)});
  164. xmlNode.removeAttribute(attr.name);
  165. i--;
  166. }
  167. }
  168. // sort the attributes
  169. list.sort(function(a, b) {
  170. if (a.id < b.id) {
  171. return -1;
  172. }
  173. if (a.id > b.id) {
  174. return 1;
  175. }
  176. return 0;
  177. });
  178. // reinsert the attributes
  179. for (var j = 0; j < list.length; j++) {
  180. attr = list[j];
  181. xmlNode.setAttribute(attr.id, attr.val);
  182. }
  183. };
  184. /**
  185. * Inits the process of canonicalization.
  186. *
  187. * @function
  188. * @param xmlNode
  189. * xml node, as a DOM.
  190. *
  191. * @return xml node processed, as DOM.
  192. */
  193. ExclusiveCanonicalization.prototype.process = function(xml) {
  194. // traverse the tree through all the children
  195. for (var i = 0; i < xml.childNodes.length; i++) {
  196. var child = xml.childNodes[i];
  197. if (child.nodeType === Node.ELEMENT_NODE) {
  198. this.process(child);
  199. }
  200. // if are comments or other stuff, remove them
  201. else if (child.nodeType !== Node.TEXT_NODE) {
  202. child.parentNode.removeChild(child);
  203. --i;
  204. }
  205. }
  206. // sort the attributes
  207. this.sortAttributes(xml);
  208. // return the final object
  209. return xml;
  210. };
  211. /**
  212. * An xml signer.
  213. *
  214. * @class
  215. * @memberof asymmetric/xmlsigner
  216. */
  217. function CryptoForgeXmlSigner() {}
  218. CryptoForgeXmlSigner.prototype = {
  219. /**
  220. * Verifies the digital signature of a selfsigned xml.
  221. *
  222. * @function
  223. * @param publicKey
  224. * Public key, as an object.
  225. * @param signedXml
  226. * XML data, as a string
  227. * @param signatureParentNode
  228. * The node where the signature is, as a string.
  229. *
  230. * @returns Boolean indicating whether signature verification was
  231. * successful.
  232. */
  233. verify: function(publicKey, signedXml, signatureParentNode) {
  234. // clean the comments in the xml
  235. signedXml = this._removeXmlStringComments(signedXml);
  236. // loads the text to an xml
  237. var xml = parsers.xmlFromString(signedXml);
  238. // loads the xml signature structure to json
  239. var signature = this._loadSignature(xml, signatureParentNode);
  240. // check all the algorithms are the expected ones
  241. this._verifyAlgorithmsInSignature(signature);
  242. // check the public key is the expected one
  243. this._verifyPublicKeyInSignature(signature, publicKey);
  244. // check the references are well encoded
  245. this._validateReferences(signature, signedXml);
  246. // validates the signatures are right
  247. this._validateSignature(signature, publicKey);
  248. // if not exception raised, all went good
  249. return true;
  250. },
  251. /**
  252. * Gets the XML signature node from the main XML node
  253. *
  254. * @function
  255. * @private
  256. * @param xml
  257. * XML where the signature is, as a Document Object
  258. * @param signatureParentNode
  259. * The node where the signature is, as a string.
  260. *
  261. * @returns Object with the DOM node of the signature and and the
  262. * signature in json.
  263. */
  264. _loadSignature: function(xml, signatureParentNode) {
  265. // access to the node where the signature is, and take the methods
  266. try {
  267. // get the signature node
  268. var xmlNode = new utils.XPath(xml, signatureParentNode);
  269. // create signature object to load the data from the signature
  270. var data = new XmlSignature(xmlNode);
  271. // return the main object
  272. return {xmlNode: xmlNode, data: data};
  273. } catch (error) {
  274. throw new exceptions.CryptoLibException(
  275. 'Could not load the signature from the XML', error);
  276. }
  277. },
  278. /**
  279. * Verifies that the method algorithms in the EML and the properties
  280. * match.
  281. *
  282. * @function
  283. * @private
  284. * @param signature
  285. * Signature, as a Javascript Object
  286. */
  287. _verifyAlgorithmsInSignature: function(signature) {
  288. try {
  289. if (signature.data.canonicalization.method !==
  290. 'http://www.w3.org/2001/10/xml-exc-c14n#') {
  291. throw new exceptions.CryptoLibException(
  292. 'Invalid canonicalization algorithm');
  293. }
  294. if (!stringUtils.containsSubString(
  295. signature.data.info.method, policies.algorithm) &&
  296. !stringUtils.containsSubString(
  297. signature.data.info.method, policies.hash)) {
  298. throw new exceptions.CryptoLibException(
  299. 'Invalid signature algorithm');
  300. }
  301. if (!stringUtils.containsSubString(
  302. signature.data.reference.digest.method, policies.hash)) {
  303. throw new exceptions.CryptoLibException('Invalid digest algorithm');
  304. }
  305. } catch (error) {
  306. throw new exceptions.CryptoLibException(
  307. 'Could not verify the data in the XML.', error);
  308. }
  309. },
  310. /**
  311. * Verifies the public key is the same than the one in the EML.
  312. *
  313. * @function
  314. * @private
  315. * @param signature
  316. * Signature, as a Javascript Object
  317. * @param publicKey
  318. * Public key, as an object.
  319. */
  320. _verifyPublicKeyInSignature: function(signature, publicKey) {
  321. // it has no key
  322. if (!signature.data.rsakey.exponent || !signature.data.rsakey.modulus) {
  323. return;
  324. }
  325. // if it has key, we validate it
  326. try {
  327. var signatureExponent =
  328. converters.base64ToBigInteger(signature.data.rsakey.exponent);
  329. var signatureModulus =
  330. converters.base64ToBigInteger(signature.data.rsakey.modulus);
  331. if (publicKey.e.compareTo(signatureExponent) !== 0 ||
  332. publicKey.n.compareTo(signatureModulus) !== 0) {
  333. throw new exceptions.CryptoLibException('Invalid public key');
  334. }
  335. } catch (error) {
  336. throw new exceptions.CryptoLibException(
  337. 'Could not verify the data in the XML.', error);
  338. }
  339. },
  340. /**
  341. * Verifies the references of a selfsigned xml.
  342. *
  343. * @function
  344. * @private
  345. * @param signature
  346. * Signature, as a Javascript Object
  347. */
  348. _validateReferences: function(signature, xmlString) {
  349. try {
  350. var xmlCanonString;
  351. // The canonicalization transformations are performed here.
  352. // Due to differences in the behaviour of Internet Explorer,
  353. // these transformations must be applied manually in the case
  354. // of that browser. For all other browsers, the transformations
  355. // are performed by applying a canonicalization algorithm.
  356. if (parsers.isIE()) {
  357. xmlCanonString = this._removeXmlStringHeader(xmlString);
  358. xmlCanonString = this._removeXmlStringSelfClosingTags(xmlCanonString);
  359. xmlCanonString = this._removeXmlStringSignature(xmlCanonString);
  360. xmlCanonString = this._removeXmlStringFirstCharacter(xmlCanonString);
  361. } else {
  362. var xmlCanon = null;
  363. // apply all transforms to the xml
  364. for (var i = 0; i < signature.data.reference.transforms.length; i++) {
  365. // get the algorithm name
  366. var algorithm = signature.data.reference.transforms[i];
  367. // consume it if it exists
  368. if (algorithm in this._transformAlgorithmMap) {
  369. xmlCanon = this._transformAlgorithmMap[algorithm](
  370. signature.xmlNode.getXml());
  371. }
  372. }
  373. xmlCanon = this._removeXmlHeader(xmlCanon);
  374. xmlCanonString = parsers.xmlToString(xmlCanon, true);
  375. }
  376. var digester =
  377. this._getHashMethod(signature.data.reference.digest.method);
  378. digester.start();
  379. digester.update(
  380. parsers.removeCarriageReturnChars(xmlCanonString), 'utf8');
  381. var messageDigest = digester.digest();
  382. // check if the result is valid
  383. var encodedMessage = forge.util.encode64(messageDigest.getBytes());
  384. if (encodedMessage !== signature.data.reference.digest.value) {
  385. throw new exceptions.CryptoLibException(
  386. 'Invalid reference: \'' + encodedMessage +
  387. '\' when was expected \'' +
  388. signature.data.reference.digest.value + '\'');
  389. }
  390. } catch (error) {
  391. throw new exceptions.CryptoLibException(
  392. 'Could not validate references.', error);
  393. }
  394. },
  395. /**
  396. * Verifies the digital signature of a selfsigned xml.
  397. *
  398. * @function
  399. * @private
  400. * @param signature
  401. * Signature, as a Javascript Object
  402. * @param publicKey
  403. * Public key, as an object.
  404. */
  405. _validateSignature: function(signature, publicKey) {
  406. try {
  407. var signedInfoNode =
  408. new utils.XPath(signature.xmlNode.getXml(), '/SignedInfo');
  409. var algorithm = signature.data.canonicalization.method;
  410. var xmlCanon = null;
  411. if (algorithm in this._transformAlgorithmMap) {
  412. xmlCanon =
  413. this._transformAlgorithmMap[algorithm](signedInfoNode.getXml());
  414. }
  415. var digester = this._getHashMethod(signature.data.info.method);
  416. digester.start();
  417. digester.update(parsers.xmlToString(xmlCanon, true), 'utf8');
  418. var signatureValue = forge.util.decode64(signature.data.info.value);
  419. var result =
  420. publicKey.verify(digester.digest().getBytes(), signatureValue);
  421. if (!result) {
  422. throw new exceptions.CryptoLibException(
  423. 'Could not validate the signature.');
  424. }
  425. } catch (error) {
  426. throw new exceptions.CryptoLibException(
  427. 'Could not validate signatures.', error);
  428. }
  429. },
  430. /**
  431. * Creates the hash method
  432. *
  433. * @function
  434. * @private
  435. * @param method
  436. * method type, as string.
  437. *
  438. * @return Object, the disgester of that method.
  439. */
  440. _getHashMethod: function(method) {
  441. if (stringUtils.containsSubString(method, 'sha256')) {
  442. return forge.md.sha256.create();
  443. }
  444. },
  445. /**
  446. * Map with all tarnsform algorithms
  447. *
  448. * @function
  449. * @private
  450. */
  451. _transformAlgorithmMap: {
  452. /**
  453. * Removes the signature from a signed XML
  454. *
  455. * @param xmlSignature
  456. * the xml signature, as DOM.
  457. *
  458. * @return the XML top document, as DOM
  459. */
  460. 'http://www.w3.org/2000/09/xmldsig#enveloped-signature': function(
  461. xmlSignature) {
  462. var topElement = xmlSignature;
  463. // get the top document
  464. while (topElement.parentNode) {
  465. topElement = topElement.parentNode;
  466. }
  467. // remove the signature from the node
  468. xmlSignature.parentNode.removeChild(xmlSignature);
  469. return topElement;
  470. },
  471. /**
  472. * c14n the XML
  473. *
  474. * @param xmlNode
  475. * the xml node, as DOM.
  476. *
  477. * @return the XML top document, as DOM
  478. */
  479. 'http://www.w3.org/2001/10/xml-exc-c14n#': function(xmlNode) {
  480. var transform = new ExclusiveCanonicalization();
  481. return transform.process(xmlNode);
  482. }
  483. },
  484. /**
  485. * Removes the comments in the xml
  486. *
  487. * @function
  488. * @private
  489. * @param xml
  490. * the xml, as string.
  491. *
  492. * @return {string} the xml without comments
  493. */
  494. _removeXmlStringComments: function(xml) {
  495. return xml.replace(/<!--[\s\S]*?-->/g, '');
  496. },
  497. /**
  498. * Removes the header in the xml structure. <?xml version="1.0"
  499. * encoding="UTF-8"?>
  500. *
  501. * @function
  502. * @private
  503. * @param {object}
  504. * xmlDocument the xml, as DOM.
  505. *
  506. * @returns {object} the xml, as DOM without the header
  507. */
  508. _removeXmlHeader: function(xmlDocument) {
  509. return xmlDocument.documentElement ? xmlDocument.documentElement :
  510. xmlDocument;
  511. },
  512. /**
  513. * Removes the header in the XML string.
  514. *
  515. * @param xmlString
  516. * the xml, as a string.
  517. *
  518. * @return the xml string without the header
  519. */
  520. _removeXmlStringHeader: function(xmlString) {
  521. return parsers.removeXmlHeaderFromString(xmlString);
  522. },
  523. /**
  524. * Removes the signature node in the XML string.
  525. *
  526. * @param xmlString
  527. * the xml, as a string.
  528. *
  529. * @return the xml string without the signature tag
  530. */
  531. _removeXmlStringSignature: function(xmlString) {
  532. return parsers.removeXmlSignature(xmlString);
  533. },
  534. /**
  535. * Converts any self-closing tags to opening and closing tags, within an
  536. * XML string.
  537. *
  538. * @param xmlString
  539. * the xml, as a string.
  540. *
  541. * @return the xml string with self-closing tags converted to opening
  542. * and closing tags
  543. */
  544. _removeXmlStringSelfClosingTags: function(xmlString) {
  545. return parsers.xmlRemoveSelfClosingTags(xmlString);
  546. },
  547. /**
  548. * Removes the first character from an XML string if that character is
  549. * not the expected initial character.
  550. *
  551. * @param xmlString
  552. * the xml, as a string.
  553. *
  554. * @return the xml string with the initial character removed if it is
  555. * not wanted
  556. */
  557. _removeXmlStringFirstCharacter: function(xmlString) {
  558. if (xmlString[0] !== '<') {
  559. return xmlString.slice(1, xmlString.length);
  560. } else {
  561. return xmlString;
  562. }
  563. }
  564. };
  565. };