123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- /*
- * 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
- */
- /* jshint node:true */
- 'use strict';
- var X509Certificate = require('./x509-certificate');
- var validator = require('./input-validator');
- module.exports = CertificateValidator;
- /**
- * @class CertificateValidator
- * @classdesc The certificate validator API. To instantiate this object, use the
- * method {@link CertificateService.newValidator}.
- * @hideconstructor
- */
- function CertificateValidator() {}
- CertificateValidator.prototype = {
- /**
- * Validates the content of a certificate and returns an array of strings
- * which indicate which types of certificate validation, if any, failed.
- *
- * @function validate
- * @memberof CertificateValidator
- * @param {string}
- * certificatePem The certificate to validate, in PEM format.
- * @param {Object}
- * validationData The data against which to validate the
- * certificate content.
- * @param {string}
- * [validationData.subject] The subject validation data.
- * @param {string}
- * [validationData.subject.commonName] The reference subject
- * common name.
- * @param {string}
- * [validationData.subject.organizationalUnit] The reference
- * subject organizational unit.
- * @param {string}
- * [validationData.subject.organization] The reference subject
- * organization.
- * @param {string}
- * [validationData.subject.country] The reference subject
- * country.
- * @param {string}
- * [validationData.issuer] The issuer validation data.
- * @param {string}
- * [validationData.issuer.commonName] The reference issuer common
- * name.
- * @param {string}
- * [validationData.issuer.organizationalUnit] The reference
- * issuer organizational unit.
- * @param {string}
- * [validationData.issuer.organization] The reference issuer
- * organization.
- * @param {string}
- * [validationData.issuer.country] The reference issuer country.
- * @param {string}
- * [validationData.time] The reference time, in ISO format (e.g.
- * <code>'2014-10-23\'T\'15:20:11Z'</code>).
- * @param {string}
- * [validationData.keyType] The reference key type
- * (<code>'CA'</code>, <code>'Sign'</code> or <code>'Encryption'</code>).
- * @param {string}
- * [validationData.issuerCertificatePem] The issuer certificate,
- * in PEM format, needed to verify this certificate's signature.
- * @returns {string[]} An array of strings indicating which types of
- * certificate validation, if any, failed.
- * @throws {Error}
- * If the input data validation fails.
- */
- validate: function(certificatePem, validationData) {
- validator.checkIsNonEmptyString(
- certificatePem, 'Certificate to validate, in PEM format');
- validator.checkIsObjectWithProperties(
- validationData, 'Certificate validation data');
- var certificate = new X509Certificate(certificatePem);
- var failedValidations = [];
- if (validationData.subject) {
- validateSubject(validationData.subject, certificate, failedValidations);
- }
- if (validationData.issuer) {
- validateIssuer(validationData.issuer, certificate, failedValidations);
- }
- if (validationData.time) {
- validateTime(validationData.time, certificate, failedValidations);
- }
- if (validationData.keyType) {
- validateKeyType(validationData.keyType, certificate, failedValidations);
- }
- if (validationData.issuerCertificatePem) {
- validateSignature(
- validationData.issuerCertificatePem, certificatePem,
- failedValidations);
- }
- return failedValidations;
- },
- /**
- * Validates a certificate chain provided as input. The validation process
- * loops through all certificates in the chain, starting with the leaf
- * certificate, until it reaches the root certificate. For each certificate,
- * except the root certificate, it checks that the following conditions
- * hold:
- * <ul>
- * <li>Subject DN (distinguished name) is that expected for given
- * certificate.</li>
- * <li>Issuer DN is same as subject DN of next certificate in chain.</li>
- * <li>Key type is that expected: <code>'Sign'</code> or
- * <code>'Encryption'</code> for leaf certificate and <code>'CA'</code>
- * for rest of certificates in chain.</li>
- * <li>Signature can be verified with public key of next certificate in
- * chain.</li>
- * <li>Starting time is earlier than ending time for given certificate.</li>
- * <li>Starting time is equal to or later than starting time of next
- * certificate in chain.</li>
- * <li>Ending time is equal to or earlier than ending time of next
- * certificate in chain.</li>
- * </ul>
- * In addition, if a non-null value is provided for the time reference, it
- * will be checked whether this time reference is within the dates of
- * validity of the leaf certificate.
- *
- * After the validation process has completed, a two dimensional array of
- * strings will be returned. If this array is empty, then the validation was
- * successful. Otherwise, each element in the first dimension of the array
- * will correspond to a single certificate that failed the validation, in
- * ascending order of certificate authority. Each element in the second
- * dimension will consist of an array of failed validation types for a given
- * certificate.
- *
- * @function validateChain
- * @memberof CertificateValidator
- * @param {Object}
- * chain The chain of certificates to validate.
- * @param {Object}
- * chain.leaf The data for the leaf certificate of the chain.
- * @param {string}
- * chain.leaf.pem The leaf certificate, in PEM format.
- * @param {string}
- * chain.leaf.keyType The key type of the leaf certificate
- * (<code>'Sign'</code> or <code>'Encryption'</code>).
- * @param {Object}
- * chain.leaf.subject The subject DN of the leaf certificate.
- * @param {string}
- * [chain.leaf.time] The time reference of the leaf certificate,
- * in ISO format (e.g. <code>'2014-10-23\'T\'15:20:11Z'</code>).
- * @param {Object}
- * chain.certificates The data for the intermediate certificates
- * of the chain.
- * @param {string[]}
- * chain.certificates.pems The array of intermediate
- * certificates, each in PEM format. The array is in descending
- * order of certificate authority.
- * @param {Object[]}
- * chain.certificates.subjects The array of subject DN's for the
- * intermediate certificates. The array is in descending order of
- * authority.
- * @param {string}
- * chain.root The root certificate, in PEM format.
- * @returns {string[][]} The two dimensional string array containing
- * information about any failed validations.
- * @throws {Error}
- * If the input data validation fails.
- */
- validateChain: function(chain) {
- var failedValidation = [];
- var rootCertificate = new X509Certificate(chain.root.pem);
- var issuer = {
- commonName: rootCertificate.subjectCommonName,
- organizationalUnit: rootCertificate.subjectOrganizationalUnit,
- organization: rootCertificate.subjectOrganization,
- country: rootCertificate.subjectCountry
- };
- var issuerCertificatePem = chain.root.pem;
- var previousNotBefore = rootCertificate.notBefore;
- var previousNotAfter = rootCertificate.notAfter;
- chain.intermediates.pems.reverse();
- chain.intermediates.subjects.reverse();
- for (var i = 0; i < chain.intermediates.pems.length; i++) {
- var validationData = {
- subject: chain.intermediates.subjects[i],
- issuer: issuer,
- keyType: 'CA',
- issuerCertificatePem: issuerCertificatePem
- };
- failedValidation[i] =
- this.validate(chain.intermediates.pems[i], validationData);
- var certificate = new X509Certificate(chain.intermediates.pems[i]);
- validateDateRange(certificate, failedValidation[i]);
- validateNotBefore(
- certificate.notBefore, previousNotBefore, failedValidation[i]);
- validateNotAfter(
- certificate.notAfter, previousNotAfter, failedValidation[i]);
- issuer = {
- commonName: certificate.subjectCommonName,
- organizationalUnit: certificate.subjectOrganizationalUnit,
- organization: certificate.subjectOrganization,
- country: certificate.subjectCountry
- };
- issuerCertificatePem = chain.intermediates.pems[i];
- previousNotBefore = certificate.notBefore;
- previousNotAfter = certificate.notAfter;
- }
- failedValidation.push(validateLeafCertificate(
- this, chain, issuer, issuerCertificatePem, previousNotBefore,
- previousNotAfter));
- failedValidation.reverse();
- return failedValidation;
- },
- /**
- * Flattens a two-dimensional array of certificate chain failed validations
- * into a one-dimensional array containing the same information. Each entry
- * in the flattened array will have the form:
- * <code>validation-type_certificate-index</code>, where
- * <code>validation-type</code> is the type of validation that failed and
- * <code>certificate-index</code> is the index in the certificate chain,
- * in ascending order of authority, of the given certificate that failed the
- * validation.
- *
- * @function flattenFailedValidations
- * @memberof CertificateValidator
- * @param {string[][]}
- * failedValidations The two-dimensional array containing the
- * failed validations.
- * @returns {string[]} The one-dimensional array containing the failed
- * validations.
- * @throws {Error}
- * If the input data validation fails.
- */
- flattenFailedValidations: function(failedValidations) {
- validator.checkIsTwoDimensionalArray(
- failedValidations, 'Certificate chain failed validation arrays');
- var flattenedFailedValidations = [];
- for (var i = 0; i < failedValidations.length; i++) {
- if (failedValidations[i] !== undefined) {
- for (var j = 0; j < failedValidations[i].length; j++) {
- flattenedFailedValidations.push(
- failedValidations[i][j].toLowerCase() + '_' + (i));
- }
- }
- }
- return flattenedFailedValidations;
- }
- };
- function validateSubject(subject, certificate, failedValidations) {
- if (subject.commonName !== certificate.subjectCommonName ||
- subject.organizationalUnit !== certificate.subjectOrganizationalUnit ||
- subject.organization !== certificate.subjectOrganization ||
- subject.country !== certificate.subjectCountry) {
- failedValidations.push('SUBJECT');
- }
- }
- function validateIssuer(issuer, certificate, failedValidations) {
- if (issuer.commonName !== certificate.issuerCommonName ||
- issuer.organizationalUnit !== certificate.issuerOrganizationalUnit ||
- issuer.organization !== certificate.issuerOrganization ||
- issuer.country !== certificate.issuerCountry) {
- failedValidations.push('ISSUER');
- }
- }
- function validateTime(isoDate, certificate, failedValidations) {
- var time = parseIsoDate(isoDate);
- if (time.toString() === 'Invalid Date' ||
- time - certificate.notBefore < 0 ||
- time - certificate.notAfter > 0) {
- failedValidations.push('TIME');
- }
- }
- function validateKeyType(keyType, certificate, failedValidations) {
- if (!areBasicConstraintsValid(keyType, certificate) ||
- !isKeyUsageValid(keyType, certificate)) {
- failedValidations.push('KEY_TYPE');
- }
- }
- function areBasicConstraintsValid(keyType, certificate) {
- var basicConstraints = certificate.basicConstraints;
- if (keyType === 'CA' && (!basicConstraints || !basicConstraints.ca)) {
- return false;
- } else {
- return true;
- }
- }
- function isKeyUsageValid(keyType, certificate) {
- var returnValue = true;
- var keyUsageExtension = certificate.keyUsageExtension;
- if (!keyUsageExtension) {
- return false;
- }
- switch (keyType) {
- case 'CA':
- if (!keyUsageExtension.keyCertSign || !keyUsageExtension.crlSign) {
- returnValue = false;
- }
- break;
- case 'Sign':
- if (!keyUsageExtension.digitalSignature ||
- !keyUsageExtension.nonRepudiation) {
- returnValue = false;
- }
- break;
- case 'Encryption':
- if (!keyUsageExtension.keyEncipherment ||
- !keyUsageExtension.dataEncipherment) {
- returnValue = false;
- }
- break;
- default:
- returnValue = false;
- }
- return returnValue;
- }
- function validateSignature(
- issuerCertificatePem, certificatePem, failedValidations) {
- var issuerCertificate = new X509Certificate(issuerCertificatePem);
- try {
- if (!issuerCertificate.verify(certificatePem)) {
- failedValidations.push('SIGNATURE');
- }
- } catch (error) {
- failedValidations.push('SIGNATURE');
- }
- }
- function parseIsoDate(isoDate) {
- var dateChunks = isoDate.split(/\D/);
- return new Date(Date.UTC(
- +dateChunks[0], --dateChunks[1], +dateChunks[2], +dateChunks[5],
- +dateChunks[6], +dateChunks[7], 0));
- }
- function validateDateRange(certificate, failedValidation) {
- if (certificate.notBefore > certificate.notAfter) {
- failedValidation.push('VALIDITY_PERIOD');
- }
- }
- function validateNotBefore(notBefore, previousNotBefore, failedValidation) {
- if (notBefore < previousNotBefore) {
- failedValidation.push('NOT_BEFORE');
- }
- }
- function validateNotAfter(notAfter, previousNotAfter, failedValidation) {
- if (notAfter > previousNotAfter) {
- failedValidation.push('NOT_AFTER');
- }
- }
- function validateLeafCertificate(
- certificateValidator, chain, issuer, issuerCertificatePem,
- previousNotBefore, previousNotAfter) {
- var validationData = {
- subject: chain.leaf.subject,
- issuer: issuer,
- keyType: chain.leaf.keyType,
- issuerCertificatePem: issuerCertificatePem
- };
- if (chain.leaf.time) {
- validationData.time = chain.leaf.time;
- }
- var failedValidations =
- certificateValidator.validate(chain.leaf.pem, validationData);
- var certificate = new X509Certificate(chain.leaf.pem);
- validateDateRange(certificate, failedValidations);
- validateNotBefore(
- certificate.notBefore, previousNotBefore, failedValidations);
- validateNotAfter(certificate.notAfter, previousNotAfter, failedValidations);
- return failedValidations;
- }
|