/*
 * 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
 */
package com.scytl.cryptolib.certificates.utils;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationData;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationResult;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationType;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.commons.utils.validations.Validate;

/**
 * Class that validates chains of certificates.
 */
public class X509CertificateChainValidator {

    private final X509Certificate[] _certificateChain;

    private final X509Certificate _leafCertificate;

    private final X509CertificateType _leafCertificateKeyType;

    private final X509DistinguishedName _leafCertificateSubjectDn;

    private final X509DistinguishedName[] _subjectDns;

    private final Date _timeReference;

    private final X509Certificate _trustedCertificate;

    private final int _certificateChainLength;

    private final int _subjectDnsLength;

    /**
     * Creates a certificate chain validator for the provided input arguments.
     * 
     * @param leafCertificate
     *            the leaf certificate.
     * @param leafCertificateKeyType
     *            the leaf certificate type.
     * @param leafCertificateSubjectDn
     *            the leaf certificate subject distinguished name.
     * @param timeReference
     *            a time reference that must be within the dates of validity of
     *            the leaf certificate.
     * @param certificateChain
     *            the certificate chain of the leaf certificate.
     * @param subjectDns
     *            the subject distinguished names of the certificate chain.
     * @param trustedCertificate
     *            the trusted certificate.
     * @throws GeneralCryptoLibException
     *             if the arguments are invalid.
     */
    public X509CertificateChainValidator(
            final X509Certificate leafCertificate,
            final X509CertificateType leafCertificateKeyType,
            final X509DistinguishedName leafCertificateSubjectDn,
            final Date timeReference,
            final X509Certificate[] certificateChain,
            final X509DistinguishedName[] subjectDns,
            final X509Certificate trustedCertificate)
            throws GeneralCryptoLibException {

        super();

        Validate.notNull(leafCertificate, "Leaf certificate");
        Validate.notNull(leafCertificateKeyType,
            "Leaf certificate key type");
        Validate.notNull(leafCertificateSubjectDn,
            "Leaf certificate subject distinguished name");
        Validate.notNull(trustedCertificate, "Trusted certificate");

        if (certificateChain == null || certificateChain.length == 0) {
            _certificateChain = new X509Certificate[0];
        } else {
            _certificateChain =
                Arrays.copyOf(certificateChain, certificateChain.length);
        }

        if (subjectDns == null || subjectDns.length == 0) {
            _subjectDns = new X509DistinguishedName[0];
        } else {
            _subjectDns = Arrays.copyOf(subjectDns, subjectDns.length);
        }

        _certificateChainLength = _certificateChain.length;
        _subjectDnsLength = _subjectDns.length;
        if (_certificateChainLength != _subjectDnsLength) {
            throw new GeneralCryptoLibException(
                "The number of certificates in the chain "
                    + _certificateChainLength
                    + " is different from the number of corresponding subject distinguished names "
                    + _subjectDnsLength);
        }

        _leafCertificate = leafCertificate;
        _leafCertificateKeyType = leafCertificateKeyType;
        _leafCertificateSubjectDn = leafCertificateSubjectDn;
        _timeReference =
            timeReference == null ? null : new Date(
                timeReference.getTime());
        _trustedCertificate = trustedCertificate;
    }

    /**
     * Creates a certificate chain validator for the provided input arguments.
     * 
     * @param leafCertificate
     *            the leaf certificate.
     * @param leafCertificateKeyType
     *            the leaf certificate type.
     * @param leafCertificateSubjectDn
     *            the leaf certificate subject distinguished name.
     * @param certificateChain
     *            the certificate chain of the leaf certificate.
     * @param subjectDns
     *            the subject distinguished names of the certificate chain.
     * @param trustedCertificate
     *            the trusted certificate.
     * @throws GeneralCryptoLibException
     *             if the arguments are invalid.
     */
    public X509CertificateChainValidator(
            final X509Certificate leafCertificate,
            final X509CertificateType leafCertificateKeyType,
            final X509DistinguishedName leafCertificateSubjectDn,
            final X509Certificate[] certificateChain,
            final X509DistinguishedName[] subjectDns,
            final X509Certificate trustedCertificate)
            throws GeneralCryptoLibException {

        this(leafCertificate, leafCertificateKeyType,
            leafCertificateSubjectDn, null, certificateChain, subjectDns,
            trustedCertificate);
    }

    /**
     * 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.
     * 
     * @return a list of failed validations.
     * @throws GeneralCryptoLibException
     *             if a distinguished name could not be parsed or an invalid
     *             validation type was requested for the validation.
     */
    public List<String> validate() throws GeneralCryptoLibException {

        List<String> failedValidations = new ArrayList<>();

        List<X509Certificate> certList = new ArrayList<>();

        certList.add(_leafCertificate);
        certList.addAll(Arrays.asList(_certificateChain));

        certList.add(_trustedCertificate);
        X509Certificate[] allChain =
            certList
                .toArray(new X509Certificate[_certificateChainLength + 2]);

        List<X509DistinguishedName> nameList = new ArrayList<>();
        nameList.add(_leafCertificateSubjectDn);
        nameList.addAll(Arrays.asList(_subjectDns));
        nameList.add(null);
        X509DistinguishedName[] allSubjects =
            nameList
                .toArray(new X509DistinguishedName[_subjectDnsLength + 2]);

        for (int pos = 0; pos < allChain.length - 1; pos++) {

            int nextPos = pos + 1;
            X509Certificate elementX = allChain[pos];
            X509Certificate elementXplus1 = allChain[nextPos];

            X509CertificateType expectedKeyType;
            if (pos == 0) {
                expectedKeyType = _leafCertificateKeyType;
            } else {
                expectedKeyType =
                    X509CertificateType.CERTIFICATE_AUTHORITY;
            }

            X509DistinguishedName issuerSubjectDn =
                new CryptoX509Certificate(allChain[nextPos])
                    .getSubjectDn();

            X509CertificateValidationData validationData =
                new X509CertificateValidationData.Builder()
                    .addSubjectDn(allSubjects[pos])
                    .addIssuerDn(issuerSubjectDn)
                    .addKeyType(expectedKeyType)
                    .addCaPublicKey(elementXplus1.getPublicKey()).build();

            X509CertificateValidator validator =
                new X509CertificateValidator(new CryptoX509Certificate(
                    elementX), validationData,
                    X509CertificateValidationType.SUBJECT,
                    X509CertificateValidationType.ISSUER,
                    X509CertificateValidationType.KEY_TYPE,
                    X509CertificateValidationType.SIGNATURE);

            X509CertificateValidationResult validationResult =
                validator.validate();
            if (!validationResult.isValidated()) {
                List<X509CertificateValidationType> failedValidationTypes =
                    validationResult.getFailedValidationTypes();
                for (X509CertificateValidationType x509CertificateValidationType : failedValidationTypes) {
                    failedValidations.add(String.format("%s_%d",
                        x509CertificateValidationType, pos));
                }
            }

            if (elementX.getNotBefore().after(elementX.getNotAfter())) {
                failedValidations.add(String.format("%s_%d",
                    "ValidityPeriod", pos));
            }
            if (elementX.getNotBefore().before(
                elementXplus1.getNotBefore())) {
                failedValidations.add(String.format("%s_%d", "notBefore",
                    pos));
            }
            if (elementX.getNotAfter().after(elementXplus1.getNotAfter())) {
                failedValidations.add(String.format("%s_%d", "notAfter",
                    pos));
            }
        }

        if (_timeReference != null) {
            CryptoX509Certificate cryptoLeafX509Certificate =
                new CryptoX509Certificate(_leafCertificate);
            failedValidations.addAll(validateTime(_timeReference,
                cryptoLeafX509Certificate));
        }

        return failedValidations;
    }

    private List<String> validateTime(final Date timeReference,
            final CryptoX509Certificate cryptoCertificate)
            throws GeneralCryptoLibException {

        List<String> failedValidations = new ArrayList<>();
        X509CertificateValidationData validationData =
            new X509CertificateValidationData.Builder().addDate(
                timeReference).build();

        X509CertificateValidator validator =
            new X509CertificateValidator(cryptoCertificate,
                validationData, X509CertificateValidationType.DATE);
        X509CertificateValidationResult validationResult =
            validator.validate();
        if (!validationResult.isValidated()) {

            failedValidations.add("Time");
        }

        return failedValidations;
    }

}
