/*
 * Copyright 2016 Analytical Graphics, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.keycloak.authentication.authenticators.x509;

import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_POLICY_MODE_ANY;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.SignatureException;
import java.security.cert.CRLException;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CRL;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.security.auth.x500.X500Principal;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.Time;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.crl.CrlStorageProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.utils.CRLUtils;
import org.keycloak.utils.OCSPProvider;

/**
 * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
 * @version $Revision: 1 $
 * @date 7/30/2016
 */

public class CertificateValidator {

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

    enum KeyUsageBits {
        DIGITAL_SIGNATURE(0, "digitalSignature"),
        NON_REPUDIATION(1, "nonRepudiation"),
        KEY_ENCIPHERMENT(2, "keyEncipherment"),
        DATA_ENCIPHERMENT(3, "dataEncipherment"),
        KEY_AGREEMENT(4, "keyAgreement"),
        KEYCERTSIGN(5, "keyCertSign"),
        CRLSIGN(6, "cRLSign"),
        ENCIPHERMENT_ONLY(7, "encipherOnly"),
        DECIPHER_ONLY(8, "decipherOnly");

        private int value;
        private String name;

        KeyUsageBits(int value, String name) {

            if (value < 0 || value > 8)
                throw new IllegalArgumentException("value");
            if (name == null || name.trim().length() == 0)
                throw new IllegalArgumentException("name");
            this.value = value;
            this.name = name.trim();
        }

        public int getInt() { return this.value; }
        public String getName() {  return this.name; }

        static public KeyUsageBits parse(String name) throws IllegalArgumentException, IndexOutOfBoundsException {
            if (name == null || name.trim().length() == 0)
                throw new IllegalArgumentException("name");

            for (KeyUsageBits bit : KeyUsageBits.values()) {
                if (bit.getName().equalsIgnoreCase(name))
                    return bit;
            }
            throw new IndexOutOfBoundsException("name");
        }

        static public KeyUsageBits fromValue(int value) throws IndexOutOfBoundsException {
            if (value < 0 || value > 8)
                throw new IndexOutOfBoundsException("value");
            for (KeyUsageBits bit : KeyUsageBits.values())
                if (bit.getInt() == value)
                    return bit;
            throw new IndexOutOfBoundsException("value");
        }
    }

    public static class LdapContext {
        private final String ldapFactoryClassName;

        public LdapContext() {
            ldapFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory";
        }

        public LdapContext(String ldapFactoryClassName) {
            this.ldapFactoryClassName = ldapFactoryClassName;
        }

        public String getLdapFactoryClassName() {
            return ldapFactoryClassName;
        }
    }

    public abstract static class OCSPChecker {
        /**
         * Requests certificate revocation status using OCSP. The OCSP responder URI
         * is obtained from the certificate's AIA extension.
         * @param cert the certificate to be checked
         * @param issuerCertificate The issuer certificate
         * @return revocation status
         */
        public abstract OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException;
    }

    public abstract static class CRLLoaderImpl {
        /**
         * Returns a collection of {@link X509CRL}
         * @return
         * @throws GeneralSecurityException
         */
        public abstract Collection<X509CRL> getX509CRLs() throws GeneralSecurityException;
    }

    public static class BouncyCastleOCSPChecker extends OCSPChecker {

        private final KeycloakSession session;
        private final String responderUri;
        private final X509Certificate responderCert;

        BouncyCastleOCSPChecker(KeycloakSession session, String responderUri, X509Certificate responderCert) {
            this.session = session;
            this.responderUri = responderUri;
            this.responderCert = responderCert;
        }

        @Override
        public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {

            OCSPProvider.OCSPRevocationStatus ocspRevocationStatus = null;
            if (responderUri == null || responderUri.trim().length() == 0) {
                // Obtains revocation status of a certificate using OCSP and assuming
                // most common defaults. If responderUri is not specified,
                // then OCS responder URI is retrieved from the
                // certificate's AIA extension.
                // OCSP responses must be signed with the issuer certificate
                // or with another certificate that must be:
                // 1) signed by the issuer certificate,
                // 2) Includes the value of OCSPsigning in ExtendedKeyUsage v3 extension
                // 3) Certificate is valid at the time
                OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class);
                ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate);
            }
            else {
                URI uri;
                try {
                    uri = new URI(responderUri);
                } catch (URISyntaxException e) {
                    String message = String.format("Unable to check certificate revocation status using OCSP.\n%s", e.getMessage());
                    throw new CertPathValidatorException(message, e);
                }
                logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP with responderCert=%s",
                        uri.toString(), responderCert);
                // Obtains the revocation status of a certificate using OCSP.
                // OCSP responder's certificate is assumed to be the issuer's certificate
                // certificate.
                // responderUri overrides the contents (if any) of the certificate's AIA extension
                OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class);
                ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate, uri, responderCert, null);
            }
            return ocspRevocationStatus;
        }
    }

    public static class CRLLoaderProxy extends CRLLoaderImpl {
        private final X509CRL _crl;
        public CRLLoaderProxy(X509CRL crl) {
            _crl = crl;
        }
        @Override
        public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
            return Collections.singleton(_crl);
        }
    }

    // Delegate to list of other CRLLoaders
    public static class CRLListLoader extends CRLLoaderImpl {

        private final List<CRLLoaderImpl> delegates;

        public CRLListLoader(KeycloakSession session, String cRLConfigValue, boolean abortIfNonUpdated) {
            String[] delegatePaths = Constants.CFG_DELIMITER_PATTERN.split(cRLConfigValue);
            this.delegates = Arrays.stream(delegatePaths)
                    .map(cRLPath -> new CRLFileLoader(session, cRLPath, abortIfNonUpdated))
                    .collect(Collectors.toList());
        }


        @Override
        public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
            Collection<X509CRL> result = new LinkedList<>();
            for (CRLLoaderImpl delegate : delegates) {
                result.addAll(delegate.getX509CRLs());
            }
            return result;
        }
    }

    public static class CRLFileLoader extends CRLLoaderImpl {

        private final KeycloakSession session;
        private final String cRLPath;
        private final LdapContext ldapContext;
        private final boolean abortIfNonUpdated;

        public CRLFileLoader(KeycloakSession session, String cRLPath, boolean abortIfNonUpdated) {
            this(session, cRLPath, abortIfNonUpdated, new LdapContext());
        }

        public CRLFileLoader(KeycloakSession session, String cRLPath, boolean abortIfNonUpdated, LdapContext ldapContext) {
            this.session = session;
            this.cRLPath = cRLPath;
            this.abortIfNonUpdated = abortIfNonUpdated;
            this.ldapContext = ldapContext;

            if (ldapContext == null)
                throw new NullPointerException("Context cannot be null");
        }

        @Override
        public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
            if (cRLPath == null) {
                throw new GeneralSecurityException("Unable to load CRL because no crl path is defined");
            }

            CrlStorageProvider crlCache = session.getProvider(CrlStorageProvider.class);
            final X509CRL crl = crlCache.get(cRLPath, this::loadCRL);

            if (crl == null) {
                throw new GeneralSecurityException(String.format("Unable to load CRL from \"%s\"", cRLPath));
            }

            if (crl.getNextUpdate() != null && crl.getNextUpdate().compareTo(new Date(Time.currentTimeMillis())) < 0) {
                final String message = String.format("CRL from '%s' is not refreshed. Next update is %s.", cRLPath, crl.getNextUpdate());
                logger.warn(message);
                if (abortIfNonUpdated) {
                    throw new GeneralSecurityException(message);
                }
            }

            return Collections.singletonList(crl);
        }

        private X509CRL loadCRL() throws GeneralSecurityException {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            X509CRL crl = null;
            if (cRLPath.startsWith("http") || cRLPath.startsWith("https")) {
                // load CRL using remote URI
                try {
                    crl = loadFromURI(cf, new URI(cRLPath));
                } catch (URISyntaxException e) {
                    logger.error(e.getMessage());
                }
            } else if (cRLPath.startsWith("ldap")) {
                // load CRL from LDAP
                try {
                    crl = loadCRLFromLDAP(cf, new URI(cRLPath));
                } catch (URISyntaxException e) {
                    logger.error(e.getMessage());
                }
            } else {
                // load CRL from file
                crl = loadCRLFromFile(cf, cRLPath);
            }
            return crl;
        }

        private X509CRL loadFromURI(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
            try {
                logger.debugf("Loading CRL from %s", remoteURI.toString());

                CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
                HttpGet get = new HttpGet(remoteURI);
                get.setHeader("Pragma", "no-cache");
                get.setHeader("Cache-Control", "no-cache, no-store");
                try (CloseableHttpResponse response = httpClient.execute(get)) {
                    try (InputStream content = response.getEntity().getContent()) {
                        return loadFromStream(cf, content);
                    } finally {
                        EntityUtils.consumeQuietly(response.getEntity());
                    }
                }
            }
            catch(IOException ex) {
                logger.errorf(ex.getMessage());
            }
            return null;

        }

        private X509CRL loadCRLFromLDAP(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
            Hashtable<String, String> env = new Hashtable<>(2);
            env.put(Context.INITIAL_CONTEXT_FACTORY, ldapContext.getLdapFactoryClassName());
            env.put(Context.PROVIDER_URL, remoteURI.toString());

            try {
                DirContext ctx = new InitialDirContext(env);
                try {
                    Attributes attrs = ctx.getAttributes("");
                    Attribute cRLAttribute = attrs.get("certificateRevocationList;binary");
                    byte[] data = (byte[])cRLAttribute.get();
                    if (data == null || data.length == 0) {
                        throw new CertificateException(String.format("Failed to download CRL from \"%s\"", remoteURI.toString()));
                    }
                    try (InputStream is = new ByteArrayInputStream(data)) {
                        return loadFromStream(cf, is);
                    }
                } finally {
                    ctx.close();
                }
            } catch (NamingException e) {
                logger.error(e.getMessage());
            } catch(IOException e) {
                logger.error(e.getMessage());
            }

            return null;
        }

        private X509CRL loadCRLFromFile(CertificateFactory cf, String relativePath) throws GeneralSecurityException {
            try {
                String configDir = System.getProperty("jboss.server.config.dir");
                if (configDir != null) {
                    File f = new File(configDir + File.separator + relativePath);
                    if (f.isFile()) {
                        logger.debugf("Loading CRL from %s", f.getAbsolutePath());

                        if (!f.canRead()) {
                            throw new IOException(String.format("Unable to read CRL from \"%s\"", f.getAbsolutePath()));
                        }
                        try (FileInputStream is = new FileInputStream(f.getAbsolutePath())) {
                            return loadFromStream(cf, is);
                        }
                    }
                }
            }
            catch(IOException ex) {
                logger.errorf(ex.getMessage());
            }
            return null;
        }

        private X509CRL loadFromStream(CertificateFactory cf, InputStream is) throws IOException, CRLException {
            return (X509CRL) cf.generateCRL(is);
        }
    }

    KeycloakSession session;
    X509Certificate[] _certChain;
    int _keyUsageBits;
    List<String> _extendedKeyUsage;
    List<String> _certificatePolicy;
    String _certificatePolicyMode;
    boolean _crlCheckingEnabled;
    boolean _crlAbortIfNonUpdated;
    boolean _crldpEnabled;
    CRLLoaderImpl _crlLoader;
    boolean _ocspEnabled;
    boolean _ocspFailOpen;
    OCSPChecker ocspChecker;
    boolean _timestampValidationEnabled;
    boolean _trustValidationEnabled;

    public CertificateValidator() {

    }
    protected CertificateValidator(X509Certificate[] certChain,
                         int keyUsageBits, List<String> extendedKeyUsage,
                                   List<String> certificatePolicy, String certificatePolicyMode,
                                   boolean cRLCheckingEnabled,
                                   boolean cRLAbortIfNonUpdated,
                                   boolean cRLDPCheckingEnabled,
                                   CRLLoaderImpl crlLoader,
                                   boolean oCSPCheckingEnabled,
                                   boolean ocspFailOpen,
                                   OCSPChecker ocspChecker,
                                   KeycloakSession session,
                                   boolean timestampValidationEnabled,
                                   boolean trustValidationEnabled) {
        _certChain = certChain;
        _keyUsageBits = keyUsageBits;
        _extendedKeyUsage = extendedKeyUsage;
        _certificatePolicy = certificatePolicy;
        _certificatePolicyMode = certificatePolicyMode;
        _crlCheckingEnabled = cRLCheckingEnabled;
        _crlAbortIfNonUpdated = cRLAbortIfNonUpdated;
        _crldpEnabled = cRLDPCheckingEnabled;
        _crlLoader = crlLoader;
        _ocspEnabled = oCSPCheckingEnabled;
        _ocspFailOpen = ocspFailOpen;
        this.ocspChecker = ocspChecker;
        this.session = session;
        _timestampValidationEnabled = timestampValidationEnabled;
        _trustValidationEnabled = trustValidationEnabled;

        if (ocspChecker == null)
            throw new IllegalArgumentException("ocspChecker");
    }

    private static void validateKeyUsage(X509Certificate[] certs, int expected) throws GeneralSecurityException {
        boolean[] keyUsageBits = certs[0].getKeyUsage();
        if (keyUsageBits == null) {
            if (expected != 0) {
                String message = "Key usage extension is expected, but unavailable.";
                throw new GeneralSecurityException(message);
            }
            return;
        }

        boolean isCritical = false;
        Set<String> critSet = certs[0].getCriticalExtensionOIDs();
        if (critSet != null) {
            isCritical = critSet.contains("2.5.29.15");
        }

        int n = expected;

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < keyUsageBits.length; i++, n >>= 1) {
            boolean value = keyUsageBits[i];
            if ((n & 1) == 1 && !value) {
                String message = String.format("Key Usage bit \'%s\' is not set.", CertificateValidator.KeyUsageBits.fromValue(i).getName());
                if (sb.length() > 0) sb.append("\n");
                sb.append(message);

                logger.warn(message);
            }
        }
        if (sb.length() > 0) {
            if (isCritical) {
                throw new GeneralSecurityException(sb.toString());
            }
        }
    }

    private static void validateExtendedKeyUsage(X509Certificate[] certs, List<String> expectedEKU) throws GeneralSecurityException {
        if (expectedEKU == null || expectedEKU.size() == 0) {
            logger.debug("Extended Key Usage validation is not enabled.");
            return;
        }
        List<String> extendedKeyUsage = certs[0].getExtendedKeyUsage();
        if (extendedKeyUsage == null) {
            String message = "Extended key usage extension is expected, but unavailable";
            throw new GeneralSecurityException(message);
        }

        boolean isCritical = false;
        Set<String> critSet = certs[0].getCriticalExtensionOIDs();
        if (critSet != null) {
            isCritical = critSet.contains("2.5.29.37");
        }

        List<String> ekuList = new LinkedList<>();
        extendedKeyUsage.forEach(s -> ekuList.add(s.toLowerCase()));

        for (String eku : expectedEKU) {
            if (!ekuList.contains(eku.toLowerCase())) {
                String message = String.format("Extended Key Usage \'%s\' is missing.", eku);
                if (isCritical) {
                    throw new GeneralSecurityException(message);
                }
                logger.warn(message);
            }
        }
    }


    private static void validatePolicy(X509Certificate[] certs, List<String> expectedPolicies, String policyCheckMode) throws GeneralSecurityException {
        if (expectedPolicies == null || expectedPolicies.isEmpty()) {
            logger.debug("Certificate Policy validation is not enabled.");
            return;
        }

        List<String> policyList = CryptoIntegration.getProvider().getCertificateUtils().getCertificatePolicyList(certs[0]);

        logger.debugf("Certificate policies found: %s", String.join(",", policyList));

        if (CERTIFICATE_POLICY_MODE_ANY.equals(policyCheckMode))
        {
            boolean hasMatch = expectedPolicies.stream().anyMatch(p -> policyList.contains(p.toLowerCase()));
            if (!hasMatch) {
                String message = String.format("Certificate Policy check failed: mode = ANY, found = \'%s\', expected = \'%s\'.",
                    String.join(",", policyList), String.join(",", expectedPolicies));
                throw new GeneralSecurityException(message);
            }
        }
        else
        {
            for (String policy : expectedPolicies) {
                if (!policyList.contains(policy.toLowerCase())) {
                    String message = String.format("Certificate Policy check failed: mode = ALL, certificate policy \'%s\' is missing.", policy);
                    throw new GeneralSecurityException(message);
                }
            }
        }
    }

    public CertificateValidator validateKeyUsage() throws GeneralSecurityException {
        validateKeyUsage(_certChain, _keyUsageBits);
        return this;
    }

    public CertificateValidator validateExtendedKeyUsage() throws GeneralSecurityException {
        validateExtendedKeyUsage(_certChain, _extendedKeyUsage);
        return this;
    }

    public CertificateValidator validatePolicy() throws GeneralSecurityException {
        validatePolicy(_certChain, _certificatePolicy, _certificatePolicyMode);
        return this;
    }

    public CertificateValidator validateTimestamps() throws GeneralSecurityException {
        if (!_timestampValidationEnabled)
            return this;

        for (int i = 0; i < _certChain.length; i++)
        {
            X509Certificate x509Certificate = _certChain[i];
            if (x509Certificate.getNotBefore().getTime() > Time.currentTimeMillis()) {
                String serialNumber = x509Certificate.getSerialNumber().toString(16).replaceAll("..(?!$)",
                  "$0 ");
                String message =
                  "certificate with serialnumber '" + serialNumber
                    + "' is not valid yet: " + x509Certificate.getNotBefore().toString();
                throw new GeneralSecurityException(message);
            }
            if (x509Certificate.getNotAfter().getTime() < Time.currentTimeMillis()) {
                String serialNumber = x509Certificate.getSerialNumber().toString(16).replaceAll("..(?!$)",
                  "$0 ");
                String message = "certificate with serialnumber '" + serialNumber
                                   + "' has expired on: " + x509Certificate.getNotAfter().toString();
                throw new GeneralSecurityException(message);
            }
        }

        return this;
    }

    public CertificateValidator validateTrust() throws GeneralSecurityException {
        if (!_trustValidationEnabled)
            return this;

        TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
        if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
            throw new GeneralSecurityException("Cannot validate client certificate trust: Truststore not available. Please make sure to correctly configure truststore provider in order to be able to revalidate certificate trust");
        }
        else
        {
            Set<X509Certificate> trustedRootCerts = truststoreProvider.getRootCertificates().entrySet().stream().flatMap(t -> t.getValue().stream()).collect(Collectors.toSet());
            Set<X509Certificate> trustedIntermediateCerts = truststoreProvider.getIntermediateCertificates().entrySet().stream().flatMap(t -> t.getValue().stream()).collect(Collectors.toSet());

            logger.debugf("Found %d trusted root certs, %d trusted intermediate certs", trustedRootCerts.size(), trustedIntermediateCerts.size());

            verifyCertificateTrust(_certChain, trustedRootCerts, trustedIntermediateCerts);
        }

        return this;
    }

    /**
    * Attempts to build a certification chain for given certificate and to verify
    * it. Relies on a set of root CA certificates (trust anchors) and a set of
    * intermediate certificates (to be used as part of the chain).
    * @param certChain - client chain presented for validation. cert to validate is assumed to be the first in the chain
    * @param trustedRootCerts - set of trusted root CA certificates
    * @param trustedIntermediateCerts - set of intermediate certificates
    * @return the certification chain (if verification is successful)
    * @throws GeneralSecurityException - if the verification is not successful
    *       (e.g. certification path cannot be built or some certificate in the
    *       chain is expired)
    */
    private static PKIXCertPathBuilderResult verifyCertificateTrust(X509Certificate[] certChain, Set<X509Certificate> trustedRootCerts,
        Set<X509Certificate> trustedIntermediateCerts) throws GeneralSecurityException {

        // Create the selector that specifies the starting certificate
        X509CertSelector selector = new X509CertSelector();
        selector.setCertificate(certChain[0]);

        // Create the trust anchors (set of root CA certificates)
        Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
        for (X509Certificate trustedRootCert : trustedRootCerts) {
            trustAnchors.add(new TrustAnchor(trustedRootCert, null));
        }

        // Configure the PKIX certificate builder algorithm parameters
        PKIXBuilderParameters pkixParams =
            new PKIXBuilderParameters(trustAnchors, selector);

        // Disable CRL checks (this is done manually as additional step)
        pkixParams.setRevocationEnabled(false);

        // Specify a list of intermediate certificates
        Set<X509Certificate> intermediateCerts = new HashSet<X509Certificate>();
        for (X509Certificate intermediateCert : trustedIntermediateCerts) {
            intermediateCerts.add(intermediateCert);
        }
        // Client certificates have to be added to the list of intermediate certs
        for (X509Certificate clientCert : certChain) {
            intermediateCerts.add(clientCert);
        }
        CertStore intermediateCertStore = CryptoIntegration.getProvider().getCertStore(new CollectionCertStoreParameters(intermediateCerts));
        pkixParams.addCertStore(intermediateCertStore);

        // Build and verify the certification chain
        CertPathBuilder builder = CryptoIntegration.getProvider().getCertPathBuilder();
        PKIXCertPathBuilderResult result =
            (PKIXCertPathBuilderResult) builder.build(pkixParams);
        return result;
    }

    private X509Certificate findCAInTruststore(X509Certificate cert) throws GeneralSecurityException {
        TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
        if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
            return null;
        }
        Map<X500Principal, List<X509Certificate>> rootCerts = truststoreProvider.getRootCertificates();
        X500Principal issuer = cert.getIssuerX500Principal();
        List<X509Certificate> cas = rootCerts.get(issuer);
        X509Certificate ca = null;
        if (cas == null) {
            // fallback to lookup the issuer from the list of intermediary CAs
            cas = truststoreProvider.getIntermediateCertificates().get(issuer);
        }
        // find the one that signed the cert
        if (cas != null) {
            for (X509Certificate cacert : cas) {
                try {
                    cert.verify(cacert.getPublicKey());
                } catch (InvalidKeyException | SignatureException e) {
                    continue;
                }
                ca = cacert;
                break;
            }
        }
        if (ca != null) {
            ca.checkValidity();
        }
        return ca;
    }

    private void checkRevocationUsingOCSP(X509Certificate[] certs) throws GeneralSecurityException {

        if (logger.isDebugEnabled() && certs != null) {
            for (X509Certificate cert : certs) {
                logger.debugf("Certificate: %s", cert.getSubjectDN().getName());
            }
        }

        X509Certificate cert = null;
        X509Certificate issuer = null;

        if (certs == null || certs.length == 0) {
             throw new GeneralSecurityException("No certificates sent");
        } else if (certs.length > 1) {
            cert = certs[0];
            issuer = certs[1];
        } else {
            // only one cert => find the CA certificate using the truststore SPI
            cert = certs[0];
            issuer = findCAInTruststore(cert);
            if (issuer == null) {
                throw new GeneralSecurityException(
                        String.format("No trusted CA in certificate found: %s. Add it to truststore SPI if valid.",
                                cert.getIssuerX500Principal()));
            }
        }

        try {
            OCSPProvider.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);

            if (rs == null) {
                if (_ocspFailOpen)
                    logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
                else
                    throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
            } else if (rs.getRevocationStatus() == OCSPProvider.RevocationStatus.UNKNOWN) {
                if (_ocspFailOpen)
                    logger.warnf("Unable to determine certificate's revocation status - continuing certificate authentication because of fail-open OCSP configuration setting");
                else
                    throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
            }
            else if (rs.getRevocationStatus() == OCSPProvider.RevocationStatus.REVOKED) {

                StringBuilder sb = new StringBuilder();
                sb.append("Certificate's been revoked.");
                sb.append("\n");
                sb.append(rs.getRevocationReason().toString());
                sb.append("\n");
                sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));

                throw new GeneralSecurityException(sb.toString());
            }
        } catch (CertPathValidatorException e) {
            if (_ocspFailOpen)
                logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
            else
                throw e;
        }
    }

    private static void checkRevocationStatusUsingCRL(X509Certificate[] certs, CRLLoaderImpl crLoader, KeycloakSession session) throws GeneralSecurityException {
        Collection<X509CRL> crlColl = crLoader.getX509CRLs();
        if (crlColl != null && !crlColl.isEmpty()) {
            for (X509CRL it : crlColl) {
                CRLUtils.check(certs, it, session);
            }
        }
    }

    private static List<String> getCRLDistributionPoints(X509Certificate cert) {
        try {
            return CryptoIntegration.getProvider().getCertificateUtils().getCRLDistributionPoints(cert);
        }
        catch(IOException e) {
            logger.error(e.getMessage());
        }
        return new ArrayList<>();
    }

    private static void checkRevocationStatusUsingCRLDistributionPoints(X509Certificate[] certs, KeycloakSession session, boolean abortIfNonUpdated) throws GeneralSecurityException {

        List<String> distributionPoints = getCRLDistributionPoints(certs[0]);
        if (distributionPoints == null || distributionPoints.isEmpty()) {
            throw new GeneralSecurityException("Could not find any CRL distribution points in the certificate, unable to check the certificate revocation status using CRL/DP.");
        }
        for (String dp : distributionPoints) {
            logger.tracef("CRL Distribution point: \"%s\"", dp);
            checkRevocationStatusUsingCRL(certs, new CRLFileLoader(session, dp, abortIfNonUpdated), session);
        }
    }

    public CertificateValidator checkRevocationStatus() throws GeneralSecurityException {
        if (!(_crlCheckingEnabled || _ocspEnabled)) {
            return this;
        }
        if (_crlCheckingEnabled) {
            if (!_crldpEnabled) {
                checkRevocationStatusUsingCRL(_certChain, _crlLoader, session);
            } else {
                checkRevocationStatusUsingCRLDistributionPoints(_certChain, session, _crlAbortIfNonUpdated);
            }
        }
        if (_ocspEnabled) {
            checkRevocationUsingOCSP(_certChain);
        }
        return this;
    }

    /**
     * Configure Certificate validation
     */
    public static class CertificateValidatorBuilder {
        // A hand written DSL that walks through successive steps to configure
        // instances of CertificateValidator type. The design is an adaption of
        // the approach described in http://programmers.stackexchange.com/questions/252067/learning-to-write-dsls-utilities-for-unit-tests-and-am-worried-about-extensablit

        KeycloakSession session;
        int _keyUsageBits;
        List<String> _extendedKeyUsage;
        List<String> _certificatePolicy;
        String _certificatePolicyMode;
        boolean _crlCheckingEnabled;
        boolean _crlAbortIfNonUpdated;
        boolean _crldpEnabled;
        CRLLoaderImpl _crlLoader;
        boolean _ocspEnabled;
        boolean _ocspFailOpen;
        String _responderUri;
        X509Certificate _responderCert;
        boolean _timestampValidationEnabled;
        boolean _trustValidationEnabled;

        public CertificateValidatorBuilder() {
            _extendedKeyUsage = new LinkedList<>();
            _certificatePolicy = new LinkedList<>();
            _keyUsageBits = 0;
        }

        public class KeyUsageValidationBuilder {

            CertificateValidatorBuilder _parent;
            KeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public KeyUsageValidationBuilder enableDigitalSignatureBit() {
                _keyUsageBits |= 1 << KeyUsageBits.DIGITAL_SIGNATURE.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enablecRLSignBit() {
                _keyUsageBits |= 1 << KeyUsageBits.CRLSIGN.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableDataEncriphermentBit() {
                _keyUsageBits |= 1 << KeyUsageBits.DATA_ENCIPHERMENT.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableDecipherOnlyBit() {
                _keyUsageBits |= 1 << KeyUsageBits.DECIPHER_ONLY.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableEnciphermentOnlyBit() {
                _keyUsageBits |= 1 << KeyUsageBits.ENCIPHERMENT_ONLY.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableKeyAgreementBit() {
                _keyUsageBits |= 1 << KeyUsageBits.KEY_AGREEMENT.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableKeyEnciphermentBit() {
                _keyUsageBits |= 1 << KeyUsageBits.KEY_ENCIPHERMENT.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableKeyCertSign() {
                _keyUsageBits |= 1 << KeyUsageBits.KEYCERTSIGN.getInt();
                return this;
            }
            public KeyUsageValidationBuilder enableNonRepudiationBit() {
                _keyUsageBits |= 1 << KeyUsageBits.NON_REPUDIATION.getInt();
                return this;
            }

            public CertificateValidatorBuilder back() {
                return _parent;
            }

            CertificateValidatorBuilder parse(String keyUsage) {
                if (keyUsage == null || keyUsage.trim().length() == 0)
                    return _parent;

                String[] strs = keyUsage.split("[,]");

                for (String s : strs) {
                    try {
                        KeyUsageBits bit = KeyUsageBits.parse(s.trim());
                        switch(bit) {
                            case CRLSIGN: enablecRLSignBit(); break;
                            case DATA_ENCIPHERMENT: enableDataEncriphermentBit(); break;
                            case DECIPHER_ONLY: enableDecipherOnlyBit(); break;
                            case DIGITAL_SIGNATURE: enableDigitalSignatureBit(); break;
                            case ENCIPHERMENT_ONLY: enableEnciphermentOnlyBit(); break;
                            case KEY_AGREEMENT: enableKeyAgreementBit(); break;
                            case KEY_ENCIPHERMENT: enableKeyEnciphermentBit(); break;
                            case KEYCERTSIGN: enableKeyCertSign(); break;
                            case NON_REPUDIATION: enableNonRepudiationBit(); break;
                        }
                    }
                    catch(IllegalArgumentException e) {
                        logger.warnf("Unable to parse key usage bit: \"%s\"", s);
                    }
                    catch(IndexOutOfBoundsException e) {
                        logger.warnf("Invalid key usage bit: \"%s\"", s);
                    }
                }

                return _parent;
            }
        }

        public class ExtendedKeyUsageValidationBuilder {

            CertificateValidatorBuilder _parent;
            protected ExtendedKeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public CertificateValidatorBuilder parse(String extendedKeyUsage) {
                if (extendedKeyUsage == null || extendedKeyUsage.trim().length() == 0)
                    return _parent;

                String[] strs = extendedKeyUsage.split("[,;:]");
                for (String str : strs) {
                    _extendedKeyUsage.add(str.trim());
                }
                return _parent;
            }
        }

        public class CertificatePolicyValidationBuilder {

            CertificateValidatorBuilder _parent;
            protected CertificatePolicyValidationBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public CertificatePolicyValidationBuilder mode(String mode) {
                _certificatePolicyMode = mode;
                return this;
            }

            public CertificateValidatorBuilder parse(String certificatePolicy) {
                if (certificatePolicy == null || certificatePolicy.trim().length() == 0)
                    return _parent;

                String[] strs = certificatePolicy.split("[,;:]");
                for (String str : strs) {
                    _certificatePolicy.add(str.trim());
                }
                return _parent;
            }
        }

        public class RevocationStatusCheckBuilder {

            CertificateValidatorBuilder _parent;
            protected RevocationStatusCheckBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public RevocationStatusCheckBuilder crlAbortIfNonUpdated(boolean abortIfNonUpdated) {
                _crlAbortIfNonUpdated = abortIfNonUpdated;
                return this;
            }

            public GotCRL cRLEnabled(boolean value) {
                _crlCheckingEnabled = value;
                return new GotCRL();
            }

            public class GotCRL {
                public GotCRLDP cRLDPEnabled(boolean value) {
                    _crldpEnabled = value;
                    return new GotCRLDP();
                }
            }

            public class GotCRLRelativePath {
                public GotOCSP oCSPEnabled(boolean value) {
                    _ocspEnabled = value;
                    return new GotOCSP();
                }
            }
            public class GotCRLDP {
                public GotCRLRelativePath cRLrelativePath(String value) {
                    if (value != null)
                        _crlLoader = new CRLListLoader(session, value, _crlAbortIfNonUpdated);
                    return new GotCRLRelativePath();
                }

                public GotCRLRelativePath cRLLoader(CRLLoaderImpl cRLLoader) {
                    if (cRLLoader != null)
                        _crlLoader = cRLLoader;
                    return new GotCRLRelativePath();
                }
            }

            public class GotOCSP {
                public GotOCSPFailOpen oCSPFailOpen(boolean ocspFailOpen) {
                    _ocspFailOpen = ocspFailOpen;
                    return new GotOCSPFailOpen();
                }
            }

            public class GotOCSPFailOpen {
                public GotOCSPFailOpen oCSPResponseCertificate(String responderCert) {
                    if (responderCert != null && !responderCert.isEmpty()) {
                        try {
                            _responderCert = PemUtils.decodeCertificate(responderCert);
                            _responderCert.checkValidity();
                        } catch(CertificateException e) {
                            logger.warnf("Ignoring invalid certificate: %s", _responderCert);
                            _responderCert = null;
                        }
                    }
                    return new GotOCSPFailOpen();
                }

                public CertificateValidatorBuilder oCSPResponderURI(String responderURI) {
                    _responderUri = responderURI;
                    return _parent;
                }
            }
        }

        public class TimestampValidationBuilder {

            CertificateValidatorBuilder _parent;
            protected TimestampValidationBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public CertificateValidatorBuilder enabled(boolean timestampValidationEnabled) {
                _timestampValidationEnabled = timestampValidationEnabled;
                return _parent;
            }
        }

        public class TrustValidationBuilder {

            CertificateValidatorBuilder _parent;
            protected TrustValidationBuilder(CertificateValidatorBuilder parent) {
                _parent = parent;
            }

            public CertificateValidatorBuilder enabled(boolean value) {
                _trustValidationEnabled = value;
                return _parent;
            }
        }

        public CertificateValidatorBuilder session(KeycloakSession session) {
            this.session = session;
            return this;
        }

        public KeyUsageValidationBuilder keyUsage() {
            return new KeyUsageValidationBuilder(this);
        }

        public ExtendedKeyUsageValidationBuilder extendedKeyUsage() {
            return new ExtendedKeyUsageValidationBuilder(this);
        }

        public CertificatePolicyValidationBuilder certificatePolicy() {
            return new CertificatePolicyValidationBuilder(this);
        }

        public RevocationStatusCheckBuilder revocation() {
            return new RevocationStatusCheckBuilder(this);
        }

        public TimestampValidationBuilder timestampValidation() {
            return new TimestampValidationBuilder(this);
        }

        public TrustValidationBuilder trustValidation() {
            return new TrustValidationBuilder(this);
        }

        public CertificateValidator build(X509Certificate[] certs) {
            if (_crlLoader == null) {
                 _crlLoader = new CRLFileLoader(session, "", _crlAbortIfNonUpdated);
            }
            return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
                    _certificatePolicy, _certificatePolicyMode,
                    _crlCheckingEnabled, _crlAbortIfNonUpdated, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen,
                    new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled);
        }
    }


}
