/* * 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 */ /** @namespace certificates */ cryptolib.modules.certificates = function(box) { 'use strict'; if (box.certificates) { return; } box.certificates = {}; /** * Provides an API that can be used to perform operations with certificates. * * @exports certificates/service */ box.certificates.service = {}; var converters; var exceptions; cryptolib('commons', function(box) { converters = new box.commons.utils.Converters(); exceptions = box.commons.exceptions; }); /** * Loads a certificate in pem format. * * @function * @param {string} * certificatePem certificate as string in PEM format. * @returns {certificates.box.certificates.CryptoX509Certificate} the * certificate. */ box.certificates.service.load = function(certificatePem) { return new box.certificates.CryptoX509Certificate(certificatePem); }; /** * Provides methods to access X509 certificate data. * * @class * @param certificatePem * certificate as string in PEM format. * @memberof certificates */ box.certificates.CryptoX509Certificate = function(certificatePem) { /** @property {object} certificate the certificate. * */ this.certificate = forge.pki.certificateFromPem(certificatePem, true); }; box.certificates.CryptoX509Certificate.prototype = { /** * Retrieves the public key of the certificate. * * @function * @returns public key, as string in PEM format. * @memberof certificates.box.certificates.CryptoX509Certificate */ getPublicKey: function() { var publicKey = this.certificate.publicKey; if (publicKey !== null) { return forge.pki.publicKeyToPem(publicKey); } else { throw new exceptions.CryptoLibException( 'Could not find public key in certificate.'); } }, /** * Retrieves the start time of the certificate's validity. * * @function * @returns start time of certificate validity, as Date object. * @memberof certificates.box.certificates.CryptoX509Certificate */ getNotBefore: function() { var notBeforeField = this.certificate.validity.notBefore; if (notBeforeField !== null) { return notBeforeField; } else { throw new exceptions.CryptoLibException( 'Could not find validity start time in certificate.'); } }, /** * Retrieves the end time of the certificate's validity. * * @function * @returns end time of certificate validity, as Date object. * @memberof certificates.box.certificates.CryptoX509Certificate */ getNotAfter: function() { var notAfterField = this.certificate.validity.notAfter; if (notAfterField !== null) { return notAfterField; } else { throw new exceptions.CryptoLibException( 'Could not find validity end time in certificate.'); } }, /** * Retrieves the serial number of the certificate. * * @function * @returns serial number of certificate, as hexadecimal string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSerialNumber: function() { var serialNumber = this.certificate.serialNumber; if (serialNumber !== null) { return serialNumber; } else { return ''; } }, /** * Retrieves the issuer common name of the certificate. * * @function * @returns issuer common name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getIssuerCN: function() { var issuerCNField = this.certificate.issuer.getField({name: 'commonName'}); if (issuerCNField !== null) { return issuerCNField.value; } else { return ''; } }, /** * Retrieves the issuer organizational unit of the certificate. * * @function * @returns issuer organizational unit of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getIssuerOrgUnit: function() { var orgUnitField = this.certificate.issuer.getField({shortName: 'OU'}); if (orgUnitField !== null) { return orgUnitField.value; } else { return ''; } }, /** * Retrieves the issuer organization name of the certificate. * * @function * @returns Issuer organization name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getIssuerOrg: function() { var orgNameField = this.certificate.issuer.getField({name: 'organizationName'}); if (orgNameField !== null) { return orgNameField.value; } else { return ''; } }, /** * Retrieves the issuer locality name of the certificate. * * @function * @returns Locality name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getIssuerLocality: function() { var localityField = this.certificate.issuer.getField({name: 'localityName'}); if (localityField !== null) { return localityField.value; } else { return ''; } }, /** * Retrieves the issuer country name of the certificate. * * @function * @returns Issuer country name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getIssuerCountry: function() { var countryField = this.certificate.issuer.getField({name: 'countryName'}); if (countryField !== null) { return countryField.value; } else { return ''; } }, /** * Retrieves the subject common name of the certificate. * * @function * @returns Subject common name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSubjectCN: function() { var subjectCNField = this.certificate.subject.getField({name: 'commonName'}); if (subjectCNField !== null) { return subjectCNField.value; } else { return ''; } }, /** * Retrieves the subject organizational unit of the certificate. * * @function * @returns Subject organizational unit of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSubjectOrgUnit: function() { var orgUnitField = this.certificate.subject.getField({shortName: 'OU'}); if (orgUnitField !== null) { return orgUnitField.value; } else { return ''; } }, /** * Retrieves the subject organization name of the certificate. * * @function * @return Subject organization name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSubjectOrg: function() { var organizationNameField = this.certificate.subject.getField({name: 'organizationName'}); if (organizationNameField !== null) { return organizationNameField.value; } else { return ''; } }, /** * Retrieves the subject locality name of the certificate. * * @function * @returns Subject locality name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSubjectLocality: function() { var localityField = this.certificate.subject.getField({name: 'localityName'}); if (localityField !== null) { return localityField.value; } else { return ''; } }, /** * Retrieves the subject country name of the certificate. * * @function * @returns Subject country name of certificate, as string. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSubjectCountry: function() { var countryField = this.certificate.subject.getField({name: 'countryName'}); if (countryField !== null) { return countryField.value; } else { return ''; } }, /** * Retrieves the key usage extension of the certificate. * * @function * @return KeyUsageExtension key usage extension, as KeyUsageExtension * object. * @memberof certificates.box.certificates.CryptoX509Certificate */ getKeyUsageExtension: function() { var keyUsageExt = this.certificate.getExtension({name: 'keyUsage'}); if (keyUsageExt !== null) { var keyUsageMap = {}; keyUsageMap.digitalSignature = keyUsageExt.digitalSignature; keyUsageMap.nonRepudiation = keyUsageExt.nonRepudiation; keyUsageMap.keyEncipherment = keyUsageExt.keyEncipherment; keyUsageMap.dataEncipherment = keyUsageExt.dataEncipherment; keyUsageMap.keyAgreement = keyUsageExt.keyAgreement; keyUsageMap.keyCertSign = keyUsageExt.keyCertSign; keyUsageMap.crlSign = keyUsageExt.cRLSign; keyUsageMap.encipherOnly = keyUsageExt.encipherOnly; keyUsageMap.decipherOnly = keyUsageExt.decipherOnly; return new box.certificates.KeyUsageExtension(keyUsageMap); } else { return null; } }, /** * Retrieves the basic constraints of the certificate. * * @function * @returns map containing the basic constraints. * @memberof certificates.box.certificates.CryptoX509Certificate */ getBasicConstraints: function() { var basicConstraints = this.certificate.getExtension({name: 'basicConstraints'}); if (basicConstraints !== null) { var basicConstraintsMap = {}; basicConstraintsMap.ca = basicConstraints.cA; return basicConstraintsMap; } else { return null; } }, /** * Retrieves the digital signature of the certificate. * * @function * @returns digital signature of certificate, as string in Base64 * encoded format. * @memberof certificates.box.certificates.CryptoX509Certificate */ getSignature: function() { var signature = this.certificate.signature; if (signature !== null) { var signatureB64 = converters.base64Encode(signature, box.RSA_LINE_LENGTH); if (signatureB64 !== null) { return signatureB64; } else { throw new exceptions.CryptoLibException( 'Base64 encoding of signature is null.'); } } else { throw new exceptions.CryptoLibException('Signature is null.'); } }, /** * Verifies the digital signature of the certificate provided as input, * using this certificate's public key. * * @function * @param certificatePem * certificate whose signature is to be verified, as string * in PEM format. * @returns boolean indicating whether signature was verified. * @memberof certificates.box.certificates.CryptoX509Certificate */ verify: function(certificatePem) { var certificate = forge.pki.certificateFromPem(certificatePem); if (certificate !== null) { return this.certificate.verify(certificate); } else { throw new exceptions.CryptoLibException('Certificate is null.'); } }, /** * Retrieves the certificate, in PEM format. * * @function * @returns certificate, as string in PEM format * @memberof certificates.box.certificates.CryptoX509Certificate */ toPem: function() { if (this.certificate !== null) { return forge.pki.certificateToPem( this.certificate, box.RSA_LINE_LENGTH); } else { throw new exceptions.CryptoLibException('Certificate is null.'); } } }; /** * Container class for the key usage extension flags of a digital * certificate. * * @class * @param keyUsageMap * map containing name-value pairs of key usage extension flags. * @memberof certificates */ box.certificates.KeyUsageExtension = function(keyUsageMap) { this.keyUsageMap = keyUsageMap; }; box.certificates.KeyUsageExtension.prototype = { /** * Retrieves the digital signature flag of the key usage extension. * * @function * @returns boolean indicating whether digital signature flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ digitalSignature: function() { return this.keyUsageMap.digitalSignature; }, /** * Retrieves the non-repudiation flag of the key usage extension. * * @function * @return boolean indicating whether non-repudiation flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ nonRepudiation: function() { return this.keyUsageMap.nonRepudiation; }, /** * Retrieves the key encipherment flag of the key usage extension. * * @function * @returns boolean indicating whether key encipherment flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ keyEncipherment: function() { return this.keyUsageMap.keyEncipherment; }, /** * Retrieves the data encipherment flag of the key usage extension. * * @function * @returns boolean indicating whether data encipherment flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ dataEncipherment: function() { return this.keyUsageMap.dataEncipherment; }, /** * Retrieves the key agreement flag of the key usage extension. * * @function * @returns boolean indicating whether key agreement flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ keyAgreement: function() { return this.keyUsageMap.keyAgreement; }, /** * Retrieves the key certificate sign flag of the key usage extension. * * @function * @returns Boolean indicating whether key certificate sign flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ keyCertSign: function() { return this.keyUsageMap.keyCertSign; }, /** * Retrieves the CRL sign flag of the key usage extension. * * @function * @returns boolean indicating whether CRL sign flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ crlSign: function() { return this.keyUsageMap.crlSign; }, /** * Retrieves the encipher only flag of the key usage extension. * * @function * @returns boolean indicating whether encipher only flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ encipherOnly: function() { return this.keyUsageMap.encipherOnly; }, /** * Retrieves the decipher only flag of the key usage extension. * * @function * @returns boolean indicating whether decipher only flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ decipherOnly: function() { return this.keyUsageMap.decipherOnly; }, /** * Retrieves the CA flag of the key usage extension. * * @function * @return boolean indicating whether CA flag is set. * @memberof certificates.box.certificates.KeyUsageExtension */ ca: function() { return this.keyUsageMap.ca; } }; /** * Class that validates the content of a CryptoX509Certificate. * * @class * @param validationData * @param {string} * validationData.subject * @param {string} * validationData.subject.commonName * @param {string} * validationData.subject.organization * @param {string} * validationData.subject.organizationUnit * @param {string} * validationData.subject.country * @param {string} * validationData.issuer * @param {string} * validationData.issuer.commonName * @param {string} * validationData.issuer.organization * @param {string} * validationData.issuer.organizationUnit * @param {string} * validationData.issuer.country * @param {string} * validationData.time * @param {string} * validationData.keytype CA, Sign or Encryption * @param {string} * validationData.caCertificatePem * @memberof certificates */ box.certificates.CryptoX509CertificateValidator = function(validationData) { var parseIsoDate = function(isoDate) { var dateChunks = isoDate.split(/\D/); return new Date(Date.UTC( +dateChunks[0], --dateChunks[1], +dateChunks[2], +dateChunks[5], +dateChunks[6], +dateChunks[7], 0)); }; /** * Validates the content of the X.509 certificate. * * @function * @param certificatePem * certificate as string in PEM format. * @return an array containing the errors type, if any. */ this.validate = function(x509CertificatePem) { var cryptoX509Certificate = new box.certificates.CryptoX509Certificate(x509CertificatePem); var failedValidations = []; var validateSubject = function(subject) { if (subject.commonName !== cryptoX509Certificate.getSubjectCN() || subject.organization !== cryptoX509Certificate.getSubjectOrg() || subject.organizationUnit !== cryptoX509Certificate.getSubjectOrgUnit() || subject.country !== cryptoX509Certificate.getSubjectCountry()) { failedValidations.push('SUBJECT'); } }; var validateIssuer = function(issuer) { if (issuer.commonName !== cryptoX509Certificate.getIssuerCN() || issuer.organization !== cryptoX509Certificate.getIssuerOrg() || issuer.organizationUnit !== cryptoX509Certificate.getIssuerOrgUnit() || issuer.country !== cryptoX509Certificate.getIssuerCountry()) { failedValidations.push('ISSUER'); } }; var validateTime = function(isoDate) { var time = parseIsoDate(isoDate); if (time.toString() === 'Invalid Date' || time - cryptoX509Certificate.getNotBefore() < 0 || time - cryptoX509Certificate.getNotAfter() > 0) { failedValidations.push('TIME'); } }; var areBasicConstraintsValid = function(keyType) { var returnValue = true; var basicConstraints = cryptoX509Certificate.getBasicConstraints(); if (keyType === 'CA' && (!basicConstraints || basicConstraints.ca !== true)) { returnValue = false; } return returnValue; }; var isKeyUsageValid = function(keyType) { var returnValue = true; var keyUsageExtension = cryptoX509Certificate.getKeyUsageExtension(); if (!keyUsageExtension) { return false; } switch (keyType) { case 'CA': if (keyUsageExtension.keyCertSign() !== true || keyUsageExtension.crlSign() !== true) { returnValue = false; } break; case 'Sign': if (keyUsageExtension.digitalSignature() !== true || keyUsageExtension.nonRepudiation() !== true) { returnValue = false; } break; case 'Encryption': if (keyUsageExtension.keyEncipherment() !== true || keyUsageExtension.dataEncipherment() !== true) { returnValue = false; } break; default: returnValue = false; } return returnValue; }; var validateKeyType = function(keyType) { if (!areBasicConstraintsValid(keyType) || !isKeyUsageValid(keyType)) { failedValidations.push('KEY_TYPE'); } }; var validateSignature = function(caCertificatePem) { var caCryptoX509Certificate = new box.certificates.CryptoX509Certificate(caCertificatePem); if (caCryptoX509Certificate === null) { failedValidations.push('SIGNATURE'); throw new exceptions.CryptoLibException('CA certificate is null.'); } try { if (!caCryptoX509Certificate.verify(x509CertificatePem)) { failedValidations.push('SIGNATURE'); } } catch (error) { failedValidations.push('SIGNATURE'); throw new exceptions.CryptoLibException( 'Signature verification process failed.'); } }; if (validationData.subject) { validateSubject(validationData.subject); } if (validationData.issuer) { validateIssuer(validationData.issuer); } if (validationData.time) { validateTime(validationData.time); } if (validationData.keyType) { validateKeyType(validationData.keyType); } if (validationData.caCertificatePem) { validateSignature(validationData.caCertificatePem); } return failedValidations; }; }; /** * Class that validates the content of a CryptoX509Certificate. * * @function * @param validationData * {json} * @param {string} * validationData.subject the subject data. * @param {string} * validationData.subject.commonName * @param {string} * validationData.subject.organization * @param {string} * validationData.subject.organizationUnit * @param {string} * validationData.subject.country * @param {string} * validationData.issuer the issuer data. * @param {string} * validationData.issuer.commonName * @param {string} * validationData.issuer.organization * @param {string} * validationData.issuer.organizationUnit * @param {string} * validationData.issuer.country * @param {string} * validationData.time time to be checked. * @param {string} * validationData.keyType CA, Sign or Encryption. * @param {string} * validationData.caCertificatePem * @param certificate * certificate as string in PEM format. * @returns {Array} an array that contains the error types occurred, if any. */ box.certificates.service.validateCertificate = function( validationData, certificate) { if (validationData === null) { throw new exceptions.CryptoLibException('Validation data is null.'); } if (Object.getOwnPropertyNames(validationData).length) { throw new exceptions.CryptoLibException('Validation data is empty.'); } if (certificate === null) { throw new exceptions.CryptoLibException('Certificate is null.'); } return (new box.certificates.CryptoX509CertificateValidator(validationData)) .validate(certificate); }; /** * Validates the certificate information provided to the constructor. The * validation process loops through all certificates, starting with the leaf * certificate, until it reaches the trusted certificate. For each * certificate, except the trusted certificate, it checks that the following * conditions hold: * * In addition, if a non-null value is provided to the constructor 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 list of strings will be returned. If this list * is empty, then the validation was successful. Otherwise, the list will * contain string identifiers for each type of validation that failed. * * @function * @param {json} * chain certificate chain. * @param {json} * chain.leaf leaf certificate. * @param {string} * chain.leaf.pem X.509 certificate. * @param {string} * chain.leaf.keyType keyType. * @param {string} * chain.leaf.subject subject. * @param {string} * chain.leaf.time time reference (Optional). Only mandatory if * the 'Time' validation is going to be performed. * @param {json} * chain.certificates chain of certificates. * @param {Array} * chain.certificates.pems list of X.509 Certificates as string * in pem format. The list starts by the one closer to the root. * @param {Array} * chain.certificates.subjects list of subjects * @param {Array} * chain.root X.509 trusted Certificate. * @returns {Array} a two dimension array where the first dimension is the * index of the element starting by 1 -leaf certificate - and the * second dimension holds the error types. */ box.certificates.service.validateX509CertificateChain = function(chain) { var validateDateRange = function(certificate, failedValidation) { if (certificate.getNotBefore() > certificate.getNotAfter()) { failedValidation.push('VALIDITY_PERIOD'); } }; var validateNotBefore = function( notBefore, previousNotBefore, failedValidation) { if (notBefore < previousNotBefore) { failedValidation.push('NOT_BEFORE'); } }; var validateNotAfter = function( notAfter, previousNotAfter, failedValidation) { if (notAfter > previousNotAfter) { failedValidation.push('NOT_AFTER'); } }; var validateLeafCertificate = function( issuer, signature, previousNotBefore, previousNotAfter) { var validationData = { subject: chain.leaf.subject, issuer: issuer, keyType: chain.leaf.keyType, caCertificatePem: signature }; if (chain.leaf.time) { validationData.time = chain.leaf.time; } var certificateValidator = new box.certificates.CryptoX509CertificateValidator(validationData); var failedValidations = certificateValidator.validate(chain.leaf.pem); var certificate = new box.certificates.CryptoX509Certificate(chain.leaf.pem); validateDateRange(certificate, failedValidations); validateNotBefore( certificate.getNotBefore(), previousNotBefore, failedValidations); validateNotAfter( certificate.getNotAfter(), previousNotAfter, failedValidations); return failedValidations; }; var rootCertificate = new box.certificates.CryptoX509Certificate(chain.root); var issuer = { commonName: rootCertificate.getSubjectCN(), organization: rootCertificate.getSubjectOrg(), organizationUnit: rootCertificate.getSubjectOrgUnit(), country: rootCertificate.getSubjectCountry() }; var signature = chain.root; var failedValidation = []; var previousNotBefore = rootCertificate.getNotBefore(); var previousNotAfter = rootCertificate.getNotAfter(); var certificateValidator; chain.certificates.pems.reverse(); chain.certificates.subjects.reverse(); for (var i = 0; i < chain.certificates.pems.length; i++) { var certificate = new box.certificates.CryptoX509Certificate( chain.certificates.pems[i]); var validationData = { subject: chain.certificates.subjects[i], issuer: issuer, keyType: 'CA', signature: signature }; certificateValidator = new box.certificates.CryptoX509CertificateValidator(validationData); failedValidation[i] = certificateValidator.validate(chain.certificates.pems[i]); validateDateRange(certificate, failedValidation[i]); validateNotBefore( certificate.getNotBefore(), previousNotBefore, failedValidation[i]); validateNotAfter( certificate.getNotAfter(), previousNotAfter, failedValidation[i]); issuer = { commonName: certificate.getSubjectCN(), organization: certificate.getSubjectOrg(), organizationUnit: certificate.getSubjectOrgUnit(), country: certificate.getSubjectCountry() }; signature = chain.certificates.pems[i]; previousNotBefore = certificate.getNotBefore(); previousNotAfter = certificate.getNotAfter(); } failedValidation.push(validateLeafCertificate( issuer, signature, previousNotBefore, previousNotAfter)); failedValidation.reverse(); return failedValidation; }; /** * Flattens failed validations to a one dimensional array to a one * dimensional array. * * @function * @param {array} * failedValidations a two-dimensional failed validations array. * @returns {array} a flat array in which each element contains the error * type message piped with the index of the element, i.e. * ERROR_. */ box.certificates.service.flattenFailedValidations = function( failedValidations) { var flattenFailedValidations = Array(); for (var i = 0; i < failedValidations.length; i++) { if (failedValidations[i] !== undefined) { for (var j = 0; j < failedValidations[i].length; j++) { flattenFailedValidations.push( failedValidations[i][j].toLowerCase() + '_' + (i)); } } } return flattenFailedValidations; }; }; cryptolib.modules.certificates.service = cryptolib.modules.certificates;