/*
* 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.
* '2014-10-23\'T\'15:20:11Z'
).
* @param {string}
* [validationData.keyType] The reference key type
* ('CA'
, 'Sign'
or 'Encryption'
).
* @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:
*
'Sign'
or
* 'Encryption'
for leaf certificate and 'CA'
* for rest of certificates in chain.'Sign'
or 'Encryption'
).
* @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. '2014-10-23\'T\'15:20:11Z'
).
* @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:
* validation-type_certificate-index
, where
* validation-type
is the type of validation that failed and
* certificate-index
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;
}