/*
 * 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:
   * <ul>
   * <li>Subject DN 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: "signing" or "encryption" for leaf
   * certificate and "CA" 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.</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 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_<element index>.
   */
  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;
