/*
 * 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.products.ov.commons.verify;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.Payload;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.exceptions.ValidationException;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PublicKey;

/**
 * A service that verifies payload signatures based on Scytl's CryptoLib.
 */
public class CryptolibPayloadVerifier implements PayloadVerifier {

    private static final Logger logger = LoggerFactory.getLogger(CryptolibPayloadVerifier.class);

    private final AsymmetricServiceAPI asymmetricService;

    private PayloadSigningCertificateValidator certificateChainValidator;

    /**
     * Initialises the service with the signing key.
     *
     * @param asymmetricService
     *            an instance of the service used for signing
     * @param certificateChainValidator
     *            a service that can validate a certificate chain
     */
    public CryptolibPayloadVerifier(AsymmetricServiceAPI asymmetricService,
            PayloadSigningCertificateValidator certificateChainValidator) {
        this.asymmetricService = asymmetricService;
        this.certificateChainValidator = certificateChainValidator;
    }

    @Override
    public boolean isValid(Payload payload, X509Certificate trustedCertificate) throws PayloadVerificationException {
        logger.info("Verifying signature for payload {}...", payload);

        // Get the signature and the certificate chain.
        PayloadSignature signature = payload.getSignature();
        X509Certificate[] certificateChain = signature.getCertificateChain();

        boolean isSignatureValid = false;
        try {
            // Validate the certificate chain.
            if (isCertificateChainValid(certificateChain, trustedCertificate)) {
                // Extract the public key from the now-validated first
                // certificate.
                PublicKey verificationKey = certificateChain[0].getPublicKey();

                // Verify the signature.
                isSignatureValid = asymmetricService.verifySignature(signature.getSignatureContents(), verificationKey,
                    payload.getSignableContent());

                logger.info("Payload {} signature is {}", payload, isSignatureValid ? "valid" : "not valid");
            } else {
                logger.warn("Cannot verify the payload signature because the certificate chain is not valid");
            }
        } catch (GeneralCryptoLibException | IOException | CertificateChainValidationException e) {
            logger.error("Payload {}'s signature could not be verified", payload, e);
            throw new PayloadVerificationException(e);
        }

        return isSignatureValid;
    }

    /**
     * Ascertain whether a certificate chain can be traced back to a trusted
     * certificate.
     * 
     * @param certificateChain
     *            the certificate chain to test
     * @param trustedCertificate
     *            a trusted certificate
     * @return whether the certificate chain can be trusted
     * @throws CertificateChainValidationException
     * @throws ValidationException
     *             if the validation could not be performed
     */
    private boolean isCertificateChainValid(X509Certificate[] certificateChain, X509Certificate trustedCertificate)
            throws CertificateChainValidationException {
        boolean isValid = certificateChainValidator.isValid(certificateChain, trustedCertificate);
        if (!isValid) {
            logger.error("Invalid payload certificate chain: ",
                certificateChainValidator.getErrors().stream().collect(Collectors.joining(" | ")));
        }

        return isValid;
    }
}
