2
0

certificate-validator.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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. /* jshint node:true */
  9. 'use strict';
  10. var X509Certificate = require('./x509-certificate');
  11. var validator = require('./input-validator');
  12. module.exports = CertificateValidator;
  13. /**
  14. * @class CertificateValidator
  15. * @classdesc The certificate validator API. To instantiate this object, use the
  16. * method {@link CertificateService.newValidator}.
  17. * @hideconstructor
  18. */
  19. function CertificateValidator() {}
  20. CertificateValidator.prototype = {
  21. /**
  22. * Validates the content of a certificate and returns an array of strings
  23. * which indicate which types of certificate validation, if any, failed.
  24. *
  25. * @function validate
  26. * @memberof CertificateValidator
  27. * @param {string}
  28. * certificatePem The certificate to validate, in PEM format.
  29. * @param {Object}
  30. * validationData The data against which to validate the
  31. * certificate content.
  32. * @param {string}
  33. * [validationData.subject] The subject validation data.
  34. * @param {string}
  35. * [validationData.subject.commonName] The reference subject
  36. * common name.
  37. * @param {string}
  38. * [validationData.subject.organizationalUnit] The reference
  39. * subject organizational unit.
  40. * @param {string}
  41. * [validationData.subject.organization] The reference subject
  42. * organization.
  43. * @param {string}
  44. * [validationData.subject.country] The reference subject
  45. * country.
  46. * @param {string}
  47. * [validationData.issuer] The issuer validation data.
  48. * @param {string}
  49. * [validationData.issuer.commonName] The reference issuer common
  50. * name.
  51. * @param {string}
  52. * [validationData.issuer.organizationalUnit] The reference
  53. * issuer organizational unit.
  54. * @param {string}
  55. * [validationData.issuer.organization] The reference issuer
  56. * organization.
  57. * @param {string}
  58. * [validationData.issuer.country] The reference issuer country.
  59. * @param {string}
  60. * [validationData.time] The reference time, in ISO format (e.g.
  61. * <code>'2014-10-23\'T\'15:20:11Z'</code>).
  62. * @param {string}
  63. * [validationData.keyType] The reference key type
  64. * (<code>'CA'</code>, <code>'Sign'</code> or <code>'Encryption'</code>).
  65. * @param {string}
  66. * [validationData.issuerCertificatePem] The issuer certificate,
  67. * in PEM format, needed to verify this certificate's signature.
  68. * @returns {string[]} An array of strings indicating which types of
  69. * certificate validation, if any, failed.
  70. * @throws {Error}
  71. * If the input data validation fails.
  72. */
  73. validate: function(certificatePem, validationData) {
  74. validator.checkIsNonEmptyString(
  75. certificatePem, 'Certificate to validate, in PEM format');
  76. validator.checkIsObjectWithProperties(
  77. validationData, 'Certificate validation data');
  78. var certificate = new X509Certificate(certificatePem);
  79. var failedValidations = [];
  80. if (validationData.subject) {
  81. validateSubject(validationData.subject, certificate, failedValidations);
  82. }
  83. if (validationData.issuer) {
  84. validateIssuer(validationData.issuer, certificate, failedValidations);
  85. }
  86. if (validationData.time) {
  87. validateTime(validationData.time, certificate, failedValidations);
  88. }
  89. if (validationData.keyType) {
  90. validateKeyType(validationData.keyType, certificate, failedValidations);
  91. }
  92. if (validationData.issuerCertificatePem) {
  93. validateSignature(
  94. validationData.issuerCertificatePem, certificatePem,
  95. failedValidations);
  96. }
  97. return failedValidations;
  98. },
  99. /**
  100. * Validates a certificate chain provided as input. The validation process
  101. * loops through all certificates in the chain, starting with the leaf
  102. * certificate, until it reaches the root certificate. For each certificate,
  103. * except the root certificate, it checks that the following conditions
  104. * hold:
  105. * <ul>
  106. * <li>Subject DN (distinguished name) is that expected for given
  107. * certificate.</li>
  108. * <li>Issuer DN is same as subject DN of next certificate in chain.</li>
  109. * <li>Key type is that expected: <code>'Sign'</code> or
  110. * <code>'Encryption'</code> for leaf certificate and <code>'CA'</code>
  111. * for rest of certificates in chain.</li>
  112. * <li>Signature can be verified with public key of next certificate in
  113. * chain.</li>
  114. * <li>Starting time is earlier than ending time for given certificate.</li>
  115. * <li>Starting time is equal to or later than starting time of next
  116. * certificate in chain.</li>
  117. * <li>Ending time is equal to or earlier than ending time of next
  118. * certificate in chain.</li>
  119. * </ul>
  120. * In addition, if a non-null value is provided for the time reference, it
  121. * will be checked whether this time reference is within the dates of
  122. * validity of the leaf certificate.
  123. *
  124. * After the validation process has completed, a two dimensional array of
  125. * strings will be returned. If this array is empty, then the validation was
  126. * successful. Otherwise, each element in the first dimension of the array
  127. * will correspond to a single certificate that failed the validation, in
  128. * ascending order of certificate authority. Each element in the second
  129. * dimension will consist of an array of failed validation types for a given
  130. * certificate.
  131. *
  132. * @function validateChain
  133. * @memberof CertificateValidator
  134. * @param {Object}
  135. * chain The chain of certificates to validate.
  136. * @param {Object}
  137. * chain.leaf The data for the leaf certificate of the chain.
  138. * @param {string}
  139. * chain.leaf.pem The leaf certificate, in PEM format.
  140. * @param {string}
  141. * chain.leaf.keyType The key type of the leaf certificate
  142. * (<code>'Sign'</code> or <code>'Encryption'</code>).
  143. * @param {Object}
  144. * chain.leaf.subject The subject DN of the leaf certificate.
  145. * @param {string}
  146. * [chain.leaf.time] The time reference of the leaf certificate,
  147. * in ISO format (e.g. <code>'2014-10-23\'T\'15:20:11Z'</code>).
  148. * @param {Object}
  149. * chain.certificates The data for the intermediate certificates
  150. * of the chain.
  151. * @param {string[]}
  152. * chain.certificates.pems The array of intermediate
  153. * certificates, each in PEM format. The array is in descending
  154. * order of certificate authority.
  155. * @param {Object[]}
  156. * chain.certificates.subjects The array of subject DN's for the
  157. * intermediate certificates. The array is in descending order of
  158. * authority.
  159. * @param {string}
  160. * chain.root The root certificate, in PEM format.
  161. * @returns {string[][]} The two dimensional string array containing
  162. * information about any failed validations.
  163. * @throws {Error}
  164. * If the input data validation fails.
  165. */
  166. validateChain: function(chain) {
  167. var failedValidation = [];
  168. var rootCertificate = new X509Certificate(chain.root.pem);
  169. var issuer = {
  170. commonName: rootCertificate.subjectCommonName,
  171. organizationalUnit: rootCertificate.subjectOrganizationalUnit,
  172. organization: rootCertificate.subjectOrganization,
  173. country: rootCertificate.subjectCountry
  174. };
  175. var issuerCertificatePem = chain.root.pem;
  176. var previousNotBefore = rootCertificate.notBefore;
  177. var previousNotAfter = rootCertificate.notAfter;
  178. chain.intermediates.pems.reverse();
  179. chain.intermediates.subjects.reverse();
  180. for (var i = 0; i < chain.intermediates.pems.length; i++) {
  181. var validationData = {
  182. subject: chain.intermediates.subjects[i],
  183. issuer: issuer,
  184. keyType: 'CA',
  185. issuerCertificatePem: issuerCertificatePem
  186. };
  187. failedValidation[i] =
  188. this.validate(chain.intermediates.pems[i], validationData);
  189. var certificate = new X509Certificate(chain.intermediates.pems[i]);
  190. validateDateRange(certificate, failedValidation[i]);
  191. validateNotBefore(
  192. certificate.notBefore, previousNotBefore, failedValidation[i]);
  193. validateNotAfter(
  194. certificate.notAfter, previousNotAfter, failedValidation[i]);
  195. issuer = {
  196. commonName: certificate.subjectCommonName,
  197. organizationalUnit: certificate.subjectOrganizationalUnit,
  198. organization: certificate.subjectOrganization,
  199. country: certificate.subjectCountry
  200. };
  201. issuerCertificatePem = chain.intermediates.pems[i];
  202. previousNotBefore = certificate.notBefore;
  203. previousNotAfter = certificate.notAfter;
  204. }
  205. failedValidation.push(validateLeafCertificate(
  206. this, chain, issuer, issuerCertificatePem, previousNotBefore,
  207. previousNotAfter));
  208. failedValidation.reverse();
  209. return failedValidation;
  210. },
  211. /**
  212. * Flattens a two-dimensional array of certificate chain failed validations
  213. * into a one-dimensional array containing the same information. Each entry
  214. * in the flattened array will have the form:
  215. * <code>validation-type_certificate-index</code>, where
  216. * <code>validation-type</code> is the type of validation that failed and
  217. * <code>certificate-index</code> is the index in the certificate chain,
  218. * in ascending order of authority, of the given certificate that failed the
  219. * validation.
  220. *
  221. * @function flattenFailedValidations
  222. * @memberof CertificateValidator
  223. * @param {string[][]}
  224. * failedValidations The two-dimensional array containing the
  225. * failed validations.
  226. * @returns {string[]} The one-dimensional array containing the failed
  227. * validations.
  228. * @throws {Error}
  229. * If the input data validation fails.
  230. */
  231. flattenFailedValidations: function(failedValidations) {
  232. validator.checkIsTwoDimensionalArray(
  233. failedValidations, 'Certificate chain failed validation arrays');
  234. var flattenedFailedValidations = [];
  235. for (var i = 0; i < failedValidations.length; i++) {
  236. if (failedValidations[i] !== undefined) {
  237. for (var j = 0; j < failedValidations[i].length; j++) {
  238. flattenedFailedValidations.push(
  239. failedValidations[i][j].toLowerCase() + '_' + (i));
  240. }
  241. }
  242. }
  243. return flattenedFailedValidations;
  244. }
  245. };
  246. function validateSubject(subject, certificate, failedValidations) {
  247. if (subject.commonName !== certificate.subjectCommonName ||
  248. subject.organizationalUnit !== certificate.subjectOrganizationalUnit ||
  249. subject.organization !== certificate.subjectOrganization ||
  250. subject.country !== certificate.subjectCountry) {
  251. failedValidations.push('SUBJECT');
  252. }
  253. }
  254. function validateIssuer(issuer, certificate, failedValidations) {
  255. if (issuer.commonName !== certificate.issuerCommonName ||
  256. issuer.organizationalUnit !== certificate.issuerOrganizationalUnit ||
  257. issuer.organization !== certificate.issuerOrganization ||
  258. issuer.country !== certificate.issuerCountry) {
  259. failedValidations.push('ISSUER');
  260. }
  261. }
  262. function validateTime(isoDate, certificate, failedValidations) {
  263. var time = parseIsoDate(isoDate);
  264. if (time.toString() === 'Invalid Date' ||
  265. time - certificate.notBefore < 0 ||
  266. time - certificate.notAfter > 0) {
  267. failedValidations.push('TIME');
  268. }
  269. }
  270. function validateKeyType(keyType, certificate, failedValidations) {
  271. if (!areBasicConstraintsValid(keyType, certificate) ||
  272. !isKeyUsageValid(keyType, certificate)) {
  273. failedValidations.push('KEY_TYPE');
  274. }
  275. }
  276. function areBasicConstraintsValid(keyType, certificate) {
  277. var basicConstraints = certificate.basicConstraints;
  278. if (keyType === 'CA' && (!basicConstraints || !basicConstraints.ca)) {
  279. return false;
  280. } else {
  281. return true;
  282. }
  283. }
  284. function isKeyUsageValid(keyType, certificate) {
  285. var returnValue = true;
  286. var keyUsageExtension = certificate.keyUsageExtension;
  287. if (!keyUsageExtension) {
  288. return false;
  289. }
  290. switch (keyType) {
  291. case 'CA':
  292. if (!keyUsageExtension.keyCertSign || !keyUsageExtension.crlSign) {
  293. returnValue = false;
  294. }
  295. break;
  296. case 'Sign':
  297. if (!keyUsageExtension.digitalSignature ||
  298. !keyUsageExtension.nonRepudiation) {
  299. returnValue = false;
  300. }
  301. break;
  302. case 'Encryption':
  303. if (!keyUsageExtension.keyEncipherment ||
  304. !keyUsageExtension.dataEncipherment) {
  305. returnValue = false;
  306. }
  307. break;
  308. default:
  309. returnValue = false;
  310. }
  311. return returnValue;
  312. }
  313. function validateSignature(
  314. issuerCertificatePem, certificatePem, failedValidations) {
  315. var issuerCertificate = new X509Certificate(issuerCertificatePem);
  316. try {
  317. if (!issuerCertificate.verify(certificatePem)) {
  318. failedValidations.push('SIGNATURE');
  319. }
  320. } catch (error) {
  321. failedValidations.push('SIGNATURE');
  322. }
  323. }
  324. function parseIsoDate(isoDate) {
  325. var dateChunks = isoDate.split(/\D/);
  326. return new Date(Date.UTC(
  327. +dateChunks[0], --dateChunks[1], +dateChunks[2], +dateChunks[5],
  328. +dateChunks[6], +dateChunks[7], 0));
  329. }
  330. function validateDateRange(certificate, failedValidation) {
  331. if (certificate.notBefore > certificate.notAfter) {
  332. failedValidation.push('VALIDITY_PERIOD');
  333. }
  334. }
  335. function validateNotBefore(notBefore, previousNotBefore, failedValidation) {
  336. if (notBefore < previousNotBefore) {
  337. failedValidation.push('NOT_BEFORE');
  338. }
  339. }
  340. function validateNotAfter(notAfter, previousNotAfter, failedValidation) {
  341. if (notAfter > previousNotAfter) {
  342. failedValidation.push('NOT_AFTER');
  343. }
  344. }
  345. function validateLeafCertificate(
  346. certificateValidator, chain, issuer, issuerCertificatePem,
  347. previousNotBefore, previousNotAfter) {
  348. var validationData = {
  349. subject: chain.leaf.subject,
  350. issuer: issuer,
  351. keyType: chain.leaf.keyType,
  352. issuerCertificatePem: issuerCertificatePem
  353. };
  354. if (chain.leaf.time) {
  355. validationData.time = chain.leaf.time;
  356. }
  357. var failedValidations =
  358. certificateValidator.validate(chain.leaf.pem, validationData);
  359. var certificate = new X509Certificate(chain.leaf.pem);
  360. validateDateRange(certificate, failedValidations);
  361. validateNotBefore(
  362. certificate.notBefore, previousNotBefore, failedValidations);
  363. validateNotAfter(certificate.notAfter, previousNotAfter, failedValidations);
  364. return failedValidations;
  365. }