/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.util.validation;

import java.lang.reflect.Method;
import java.security.cert.CertPath;
import java.security.cert.CertPathParameters;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorException.BasicReason;
import java.security.cert.Certificate;
import java.security.cert.PKIXParameters;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.collect.Sets.newHashSet;

public class OpcUaCertificateRevocationChecker extends PKIXRevocationChecker {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(OpcUaCertificateRevocationChecker.class);

    private final PKIXRevocationChecker checker;

    private final CertPath certPath;
    private final TrustAnchor trustAnchor;
    private final PKIXParameters parameters;
    private final Set<ValidationCheck> validationChecks;

    public OpcUaCertificateRevocationChecker(
        CertPath certPath,
        TrustAnchor trustAnchor,
        PKIXParameters parameters,
        Set<ValidationCheck> validationChecks
    ) throws Exception {

        this.certPath = certPath;
        this.trustAnchor = trustAnchor;
        this.parameters = parameters;
        this.validationChecks = validationChecks;

        HashSet<Option> options = newHashSet(
            Option.NO_FALLBACK,
            Option.PREFER_CRLS
        );

        // If the REVOCATION_LIST_FOUND check is suppressed, enable SOFT_FAIL.
        // Unfortunately these means we won't be able to log a warning when
        // a CRL is not found, but without SOFT_FAIL *every* CRL in the chain
        // must be available, or the check fails with
        // BasicReason.UNDETERMINED_REVOCATION_STATUS, which could mask a revoked
        // certificate in a CRL we *do* have.
        if (!validationChecks.contains(ValidationCheck.REVOCATION_LISTS)) {
            options.add(Option.SOFT_FAIL);
        }

        CertPathValidator validator = CertPathValidator.getInstance("PKIX", "SUN");
        checker = (PKIXRevocationChecker) validator.getRevocationChecker();
        checker.setOptions(options);

        // bail in constructor if access via reflection will fail.
        initRevocationChecker();
    }

    private void initRevocationChecker() throws Exception {
        Class<?> pkixClass = Class.forName("sun.security.provider.certpath.PKIX");
        Class<?> validatorParamsClass = Class.forName("sun.security.provider.certpath.PKIX$ValidatorParams");
        Class<?> revocationCheckerClass = Class.forName("sun.security.provider.certpath.RevocationChecker");

        Method checkParams = pkixClass.getDeclaredMethod(
            "checkParams",
            CertPath.class,
            CertPathParameters.class
        );
        checkParams.setAccessible(true);
        Object validatorParams = checkParams.invoke(null, certPath, parameters);

        Method init = revocationCheckerClass.getDeclaredMethod(
            "init",
            TrustAnchor.class,
            validatorParamsClass
        );
        init.setAccessible(true);
        init.invoke(checker, trustAnchor, validatorParams);
    }

    @Override
    public List<CertPathValidatorException> getSoftFailExceptions() {
        return checker.getSoftFailExceptions();
    }

    @Override
    public void init(boolean forward) throws CertPathValidatorException {
        checker.init(forward);
    }

    @Override
    public boolean isForwardCheckingSupported() {
        return checker.isForwardCheckingSupported();
    }

    @Override
    public Set<String> getSupportedExtensions() {
        return checker.getSupportedExtensions();
    }

    @Override
    public void check(Certificate cert, Collection<String> unresolvedCritExts) throws CertPathValidatorException {
        try {
            initRevocationChecker();

            checker.check(cert, unresolvedCritExts);
        } catch (CertPathValidatorException e) {
            CertPath certPath = e.getCertPath();
            CertPathValidatorException.Reason reason = e.getReason();
            int failedAtIndex = e.getIndex();

            if (failedAtIndex < 0) {
                throw e;
            } else {
                X509Certificate failed = (X509Certificate) certPath.getCertificates().get(failedAtIndex);

                if (reason == BasicReason.REVOKED) {
                    if (validationChecks.contains(ValidationCheck.REVOCATION)) {
                        throw e;
                    } else {
                        LOGGER.warn(
                            "check suppressed: certificate failed revocation check: {}",
                            failed.getSubjectX500Principal().getName()
                        );
                    }
                } else if (reason == BasicReason.UNDETERMINED_REVOCATION_STATUS) {
                    // sanity check: this can only happen if we didn't include
                    // the Option.SOFT_FAIL option, which only happens when
                    // ValidationCheck.REVOCATION_LIST_FOUND is present.
                    assert !getOptions().contains(Option.SOFT_FAIL);
                    assert validationChecks.contains(ValidationCheck.REVOCATION_LISTS);

                    throw e;
                } else {
                    throw e;
                }
            }
        } catch (Exception e) {
            throw new CertPathValidatorException("revocation checker initialization failed", e);
        }
    }

}
