mitm.common.security.crl.PKIXRevocationChecker.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.crl.PKIXRevocationChecker.java

Source

/*
 * Copyright (c) 2008-2012, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.common.security.crl;

import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CRL;
import java.security.cert.CRLException;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509CRLSelector;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import mitm.common.security.NoSuchProviderRuntimeException;
import mitm.common.security.SecurityFactory;
import mitm.common.security.SecurityFactoryFactory;
import mitm.common.security.asn1.ASN1Utils;
import mitm.common.security.certificate.KeyUsageType;
import mitm.common.security.certificate.X509CertificateInspector;
import mitm.common.security.crlstore.BasicCRLStore;
import mitm.common.security.crlstore.CRLStoreException;
import mitm.common.util.Check;
import mitm.common.util.CloseableIterator;
import mitm.common.util.CloseableIteratorException;
import mitm.common.util.LogUtils;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.IssuingDistributionPoint;
import org.bouncycastle.asn1.x509.ReasonFlags;
import org.bouncycastle.asn1.x509.X509Extension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
 * TODO: we do not yet support indirect CRLs because in such a case the issuer of the
 * CRL can be some other CA than the issuer of the certificate. When we will support
 * indirect CRLs we need to modify this check.
 * 
 * according to RFC 3280 5.1.1.3 it is possible to use a different CA for CRL 
 * signing than for certificate signing. Supporting separate CAs for certificate 
 * signing and CRL signing requires that we build a path for the CRL. 
 * According to RFC 3280 supporting this is a should and we will therefore not 
 * support this yet. Windows does not support CRLs signed by other CAs then
 * the certificate issuer 
 * (see http://www.microsoft.com/technet/security/guidance/cryptographyetc/tshtcrl.mspx)
 * 
 */
public class PKIXRevocationChecker implements RevocationChecker {
    private final static Logger logger = LoggerFactory.getLogger(PKIXRevocationChecker.class);

    /*
     * Status results for Delta CRL checking
     */
    private enum DeltaCRLStatus {
        OK, UNKNOWN, UNSUPPORTED_CRITICAL_EXTENSION
    };

    /*
     * Collection of CRL stores is synchronized to make it thread safe.
     */
    private final Set<BasicCRLStore> crlStores = Collections.synchronizedSet(new HashSet<BasicCRLStore>());

    private final SecurityFactory securityFactory;

    /*
     * all revocation reasons mask. Used to check that all reasons have been covered
     */
    final int allReasons = ReasonFlags.aACompromise | ReasonFlags.affiliationChanged | ReasonFlags.cACompromise
            | ReasonFlags.certificateHold | ReasonFlags.cessationOfOperation | ReasonFlags.keyCompromise
            | ReasonFlags.privilegeWithdrawn | ReasonFlags.unused | ReasonFlags.superseded;

    public PKIXRevocationChecker(BasicCRLStore... stores) {
        securityFactory = SecurityFactoryFactory.getSecurityFactory();

        for (BasicCRLStore store : stores) {
            crlStores.add(store);
        }
    }

    /*
     * Returns the revocation result for the given certPath
     */
    @Override
    public RevocationResult getRevocationStatus(CertPath certPath, TrustAnchor trustAnchor, Date now)
            throws CRLException {
        Check.notNull(certPath, "certPath");
        Check.notNull(trustAnchor, "trustAnchor");

        List<? extends Certificate> certificates = certPath.getCertificates();

        RevocationResult revocationResult = new RevocationResultImpl(certificates.size());

        /* 
         * Step through all the certificates in the path and check the revocation status of all
         * the certificates in the path.
         */
        for (int i = 0; i < certificates.size(); i++) {
            X509Certificate certificate = toX509Certificate(certificates.get(i));

            PublicKey issuerPublicKey;
            X500Principal issuer;
            X509Certificate issuerCertificate;

            /*
             * we need to get the issuer of the current certificate
             * check if there is a next certificate in the path or that we must use the TrustAnchor
             */
            if ((i + 1) == certificates.size()) {
                /* this was the last entry from the path so we must use the trust anchor */
                if (trustAnchor.getTrustedCert() != null) {
                    issuerCertificate = toX509Certificate(trustAnchor.getTrustedCert());
                    issuerPublicKey = issuerCertificate.getPublicKey();
                    issuer = issuerCertificate.getSubjectX500Principal();
                } else {
                    /* the TrustAnchor does not contain a certificate but only an issuer and public key */
                    issuerCertificate = null;
                    issuerPublicKey = trustAnchor.getCAPublicKey();
                    issuer = trustAnchor.getCA();
                }
            } else {
                /* get next entry from path ie. the issuer of the current certificate */
                issuerCertificate = toX509Certificate(certificates.get(i + 1));
                issuerPublicKey = issuerCertificate.getPublicKey();
                issuer = issuerCertificate.getSubjectX500Principal();
            }

            /*
             * sanity check to make sure the CertPath is ordered from end -> final CA
             * ie that the next certificate signed the previous certificate
             */
            verifyCertificate(certificate, issuerPublicKey);

            /* 
             * Sanity check. The issuer principal field of the certificate currently checked should 
             * normally be equal to the issuer principal.
             */
            if (!certificate.getIssuerX500Principal().equals(issuer)) {
                logger.warn("Certificate issuer field is not equal to issuer.");
            }

            if (issuerCertificate != null) {
                Set<KeyUsageType> keyUsage = X509CertificateInspector.getKeyUsage(issuerCertificate);

                /* 
                 * check if issuer is allowed to issue CRLs (only when we have an issuerCertificate, and
                 * a key usage extension) 
                 */
                if (keyUsage != null && !keyUsage.contains(KeyUsageType.CRLSIGN)) {
                    logger.debug("Issuer is not allowed to issue CRLs.");

                    /*
                     * We will return UNKNOWN status.
                     */
                    RevocationDetailImpl detail = new RevocationDetailImpl(RevocationStatus.UNKNOWN);

                    revocationResult.getDetails()[i] = detail;

                    /* there is no need to continue because issuer is not allowed to issue CRLs */
                    break;
                }
            }

            X509CRLSelector crlSelector = new X509CRLSelector();

            /* create a selector to find all relevant CRLs that were issued to the same issuer as the certificate */
            crlSelector.addIssuer(issuer);

            try {
                List<X509CRL> crls = findCRLs(certificate, crlSelector, issuerPublicKey, now);

                RevocationDetail detail = getRevocationDetail(crls, certificate, issuerCertificate, issuerPublicKey,
                        now);

                revocationResult.getDetails()[i] = detail;

                if (detail.getStatus() == RevocationStatus.REVOKED) {
                    logger.warn("Certificate is revoked.");

                    if (logger.isDebugEnabled()) {
                        logger.debug("Revoked certificate: " + certificate);
                    }

                    /* there is no need to continue because the CRL is revoked */
                    break;
                }
            } catch (NoSuchProviderException e) {
                throw new NoSuchProviderRuntimeException(e);
            }
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Revocation status for CertPath " + certPath + " and TrustAnchor " + trustAnchor + " is "
                    + revocationResult);
        }

        return revocationResult;
    }

    private void verifyCertificate(X509Certificate certificate, PublicKey issuerPublicKey) throws CRLException {
        try {
            certificate.verify(issuerPublicKey, securityFactory.getNonSensitiveProvider());
        } catch (InvalidKeyException e) {
            throw new CRLException("Certificate verification failed.", e);
        } catch (CertificateException e) {
            throw new CRLException("Certificate verification failed.", e);
        } catch (NoSuchAlgorithmException e) {
            throw new CRLException("Certificate verification failed.", e);
        } catch (NoSuchProviderException e) {
            throw new CRLException("Certificate verification failed.", e);
        } catch (SignatureException e) {
            throw new CRLException("Certificate verification failed.", e);
        }
    }

    /*
     * Convert certificate to X509Certificate.
     */
    private X509Certificate toX509Certificate(Certificate certificate) {
        if (!(certificate instanceof X509Certificate)) {
            throw new IllegalArgumentException("Only X509Certificates are supported.");
        }

        return (X509Certificate) certificate;
    }

    /*
     * Checks whether the CRL has any unsupported extensions.
     */
    private boolean hasUnsupportedCriticalExtensions(X509CRL crl) {
        Set<String> criticalExtensions = crl.getCriticalExtensionOIDs();

        if (criticalExtensions != null) {
            criticalExtensions.remove(X509Extension.issuingDistributionPoint.getId());
            criticalExtensions.remove(X509Extension.deltaCRLIndicator.getId());
            criticalExtensions.remove(X509Extension.cRLNumber.getId());
            /*
             * Some issuers (Verisign) add a critcal Authority Key Identifier to the CRL. 
             * 
             * RFC 3280 explicitly says: 
             * 
             * 4.2.1.1  Authority Key Identifier
             * ....
             * This extension MUST NOT be marked critical.
             * 
             * We will therefore ignore this extension if it's critical
             * 
             */
            criticalExtensions.remove(X509Extension.authorityKeyIdentifier.getId());
        }

        return criticalExtensions != null && criticalExtensions.size() > 0;
    }

    /*
     * Checks if the stores contain a base CRL for the given delta CRL and if the scope of the 
     * delta CRL is equal to the scope of the base CRL.
     * 
     * For more info see: http://www.ietf.org/rfc/rfc3280.txt. 5.2.4  Delta CRL Indicator
     */
    private DeltaCRLStatus getDeltaCRLStatus(X509Certificate targetCertificate, X509CRL deltaCRL,
            PublicKey issuerPublicKey, Date now) throws NoSuchProviderException {
        DeltaCRLStatus status = DeltaCRLStatus.UNKNOWN;

        BigInteger baseCRLNumber;

        try {
            baseCRLNumber = X509CRLInspector.getDeltaIndicator(deltaCRL);
        } catch (IOException e) {
            logger.error("Error getting base CRL number", e);

            return DeltaCRLStatus.UNKNOWN;
        }

        X509CRLSelector crlSelector = new X509CRLSelector();

        /* We need to find a valid base CRL with the same issuer as the delta CRL */
        crlSelector.addIssuer(deltaCRL.getIssuerX500Principal());

        /*
         * we need to find a baseCRL with at least a CRL number specified by the DeltaCRLIndicator in 
         * the delta CRL
         */
        crlSelector.setMinCRLNumber(baseCRLNumber);

        BigInteger deltaCRLNumber = null;

        try {
            deltaCRLNumber = X509CRLInspector.getCRLNumber(deltaCRL);
        } catch (IOException e) {
            logger.error("Error getting CRLNumber extension from the delta CRL.", e);
        }

        if (deltaCRLNumber != null) {
            /*
             * the base CRL we need to find should have a  CRL number less than the delta CRL
             * otherwise it cannot be a base for this delta CRL
             */
            crlSelector.setMaxCRLNumber(deltaCRLNumber.subtract(BigInteger.valueOf(1)));

            List<X509CRL> crls = findCRLs(targetCertificate, crlSelector, issuerPublicKey, now);

            for (X509CRL baseCRL : crls) {
                try {
                    if (checkDeltaCRL_6_3_3_b(targetCertificate, deltaCRL, baseCRL)) {
                        status = DeltaCRLStatus.OK;
                        break;
                    }
                } catch (IOException e) {
                    logger.error("Error executing checkDeltaCRL_6_3_3_b.", e);
                    continue;
                }

                if (hasUnsupportedCriticalExtensions(baseCRL)) {
                    logger.warn("The base CRL has unsupported critical extensions.");

                    status = DeltaCRLStatus.UNSUPPORTED_CRITICAL_EXTENSION;

                    continue;
                }
            }
        }

        return status;
    }

    /*
     * Converts the relative name to crl issuer to a full name by appending the relative
     * distinguished name contained in distributionPointName to the issuer.
     */
    private X500Name getFullName(X500Principal issuer, DistributionPointName distributionPointName)
            throws IOException {
        ASN1Encodable rdn = distributionPointName.getName();

        X500Name name = null;

        if (rdn != null) {
            ASN1EncodableVector v = ASN1Utils.toASN1EncodableVector(issuer);

            v.add(rdn);

            name = X500Name.getInstance(new DERSequence(v).getEncoded(ASN1Encoding.DER));
        }

        return name;
    }

    /*
     * Returns true if name matches any of the generalNames
     */
    private boolean hasMatchingName(X500Name name, GeneralName[] generalNames) {
        if (name == null || generalNames == null) {
            return false;
        }

        for (GeneralName generalName : generalNames) {
            /* 
             * we only need to compare directoryNames
             */
            if (generalName.getTagNo() == GeneralName.directoryName) {
                if (name.equals(X500Name.getInstance(generalName.getName()))) {
                    return true;
                }
            }
        }

        return false;
    }

    /*
     * Checks if there is a name match between the two DistributionPointNames.
     */
    private boolean hasMatchingName(DistributionPointName dpn1, DistributionPointName dpn2, X500Principal issuer)
            throws IOException {
        if (dpn1 == null && dpn2 == null) {
            return true;
        }

        if (dpn1 == null || dpn2 == null) {
            return false;
        }

        GeneralName[] generalNames1 = null;
        GeneralName[] generalNames2 = null;

        X500Name name1 = null;
        X500Name name2 = null;

        if (dpn1.getType() == DistributionPointName.NAME_RELATIVE_TO_CRL_ISSUER) {
            name1 = getFullName(issuer, dpn1);
        } else {
            generalNames1 = GeneralNames.getInstance(dpn1.getName()).getNames();
        }

        if (dpn2.getType() == DistributionPointName.NAME_RELATIVE_TO_CRL_ISSUER) {
            name2 = getFullName(issuer, dpn2);
        } else {
            generalNames2 = GeneralNames.getInstance(dpn2.getName()).getNames();
        }

        if (generalNames1 != null && generalNames2 != null) {
            return CollectionUtils.containsAny(Arrays.asList(generalNames1), Arrays.asList(generalNames2));
        }

        if (name1 != null && name2 != null) {
            return name1.equals(name2);
        }

        return name1 != null ? hasMatchingName(name1, generalNames2) : hasMatchingName(name2, generalNames1);
    }

    /*
     * Checks whether the CRL is valid for the certificate
     */
    private boolean acceptCRL_6_3_3_b(X509Certificate targetCertificate, X509CRL crl) throws IOException {
        boolean match = false;

        if (X509CRLInspector.isDeltaCRL(crl)) {
            /* CRL is not complete because it's a delta CRL */
            return false;
        }

        if (!crl.getIssuerX500Principal().equals(targetCertificate.getIssuerX500Principal())) {
            logger.debug("CRL issuer and certificate issuer do not match.");

            return false;
        }

        IssuingDistributionPoint idp = X509CRLInspector.getIssuingDistributionPoint(crl);

        /* if there is no IssuingDistributionPoint there is always a match */
        if (idp == null) {
            return true;
        }

        DistributionPointName idpn = idp.getDistributionPoint();

        CRLDistPoint crlDistPoint = X509CertificateInspector.getCRLDistibutionPoints(targetCertificate);

        DistributionPoint[] dps = null;

        if (crlDistPoint != null) {
            dps = crlDistPoint.getDistributionPoints();
        }

        if (dps != null) {
            for (DistributionPoint dp : dps) {
                if (dp == null) {
                    logger.debug("Distributionpoint is null.");
                    continue;
                }

                if (dp.getCRLIssuer() != null) {
                    /* we do not support indirect CRLs */
                    logger.debug("CRL issuer should only be used for indirect CRLs.");

                    continue;
                }

                DistributionPointName dpn = dp.getDistributionPoint();

                if (idp != null) {
                    if (idpn != null && dpn != null) {
                        X500Principal issuer = targetCertificate.getIssuerX500Principal();

                        if (hasMatchingName(idpn, dpn, issuer)) {
                            match = true;
                            break;
                        }
                    }
                }
            }
            if (!match) {
                logger.debug("The CRL did not contain matching DistributionPoint names.");
            }
        } else {
            match = (idpn == null);
        }

        BasicConstraints basicConstraints = X509CertificateInspector.getBasicConstraints(targetCertificate);

        if (idp != null) {
            /* if basicConstraints is null assume it's a user certificate */

            if (idp.onlyContainsCACerts()
                    && ((basicConstraints != null && !basicConstraints.isCA()) | basicConstraints == null)) {
                logger.debug("Certificate is a user certificate but CRL only contains CA certificate.");
                match = false;
            }

            if (idp.onlyContainsUserCerts() && basicConstraints != null && basicConstraints.isCA()) {
                logger.debug("Certificate is a CA but CRL only contains user certificates.");
                match = false;
            }

            if (idp.onlyContainsAttributeCerts()) {
                logger.debug("Certificate only contains attribute certs.");
                match = false;
            }
        }

        return match;
    }

    /*
     * Checks whether the delta CRL and base CRL are valid
     */
    private boolean checkDeltaCRL_6_3_3_b(X509Certificate targetCertificate, X509CRL deltaCRL, X509CRL baseCRL)
            throws IOException {
        if (!X509CRLInspector.isDeltaCRL(deltaCRL)) {
            logger.debug("CRL is not a delta CRL.");
            return false;
        }

        if (X509CRLInspector.isDeltaCRL(baseCRL)) {
            logger.debug("CRL is not a base CRL it's a delta CRL.");
            return false;
        }

        if (!deltaCRL.getIssuerX500Principal().equals(baseCRL.getIssuerX500Principal())) {
            logger.debug("Delta CRL issuer does not match Base CRL issuer.");

            return false;
        }

        IssuingDistributionPoint deltaIDP = X509CRLInspector.getIssuingDistributionPoint(deltaCRL);
        IssuingDistributionPoint baseIDP = X509CRLInspector.getIssuingDistributionPoint(baseCRL);

        if (baseIDP != null) {
            if (!baseIDP.equals(deltaIDP)) {
                logger.debug("The Base CRL has a non matching IssuingDistributionPoint.");
                return false;
            }
        } else {
            if (deltaIDP != null) {
                logger.debug("The Delta CRL has a non matching IssuingDistributionPoint.");
                return false;
            }
        }

        AuthorityKeyIdentifier baseAKI = X509CRLInspector.getAuthorityKeyIdentifier(baseCRL);
        AuthorityKeyIdentifier deltaAKI = X509CRLInspector.getAuthorityKeyIdentifier(deltaCRL);

        if (baseAKI != null) {
            if (!baseAKI.equals(deltaAKI)) {
                logger.debug("Base AuthorityKeyIdentifier does not match Delta AuthorityKeyIdentifier.");
                return false;
            }
        } else {
            if (deltaAKI != null) {
                logger.debug("Delta AuthorityKeyIdentifier does not match Base AuthorityKeyIdentifier.");
                return false;

            }
        }

        return true;
    }

    /*
     * Returns a mask containing the reasons covered by the given CRL.
     */
    private int getInterimReasonsMask(X509Certificate targetCertificate, X509CRL crl) throws IOException {
        IssuingDistributionPoint idp = X509CRLInspector.getIssuingDistributionPoint(crl);

        CRLDistPoint crlDistPoint = X509CertificateInspector.getCRLDistibutionPoints(targetCertificate);

        DistributionPoint[] dps = null;

        if (crlDistPoint != null) {
            dps = crlDistPoint.getDistributionPoints();
        }

        int interimMask = 0;

        if (idp != null && idp.getOnlySomeReasons() != null) {
            ReasonFlags irf = idp.getOnlySomeReasons();

            if (dps != null) {
                for (DistributionPoint dp : dps) {
                    if (dp == null) {
                        logger.debug("Distributionpoint is null.");
                        continue;
                    }

                    /* 6.3.3 (d)(1) */
                    if (dp.getReasons() != null) {
                        ReasonFlags drf = dp.getReasons();

                        int intersection = irf.intValue() & drf.intValue();

                        interimMask = interimMask | intersection;
                    }
                    /* 6.3.3 (d)(2) */
                    else {
                        interimMask = interimMask | irf.intValue();
                    }
                }
            } else {
                /* 6.3.3 (d)(2) */
                interimMask = interimMask | irf.intValue();
            }
        }
        /* 6.3.3 (d)(3) */
        else {
            if (dps != null) {
                for (DistributionPoint dp : dps) {
                    if (dp == null) {
                        logger.debug("Distributionpoint is null.");
                        continue;
                    }

                    if (dp.getReasons() != null) {
                        ReasonFlags drf = dp.getReasons();

                        interimMask = interimMask | drf.intValue();
                    } else {
                        interimMask = interimMask | allReasons;
                    }
                }
            } else {
                interimMask = interimMask | allReasons;
            }
        }

        return interimMask;
    }

    /* 
     * Do some initial filtering like not accepting indirect CRLs.
     */
    private boolean preFilter(X509Certificate targetCertificate, X509CRL crl) throws IOException {
        IssuingDistributionPoint idp = X509CRLInspector.getIssuingDistributionPoint(crl);

        if (idp != null) {
            if (idp.isIndirectCRL()) {
                logger.debug("CRL is indirect.");
                return false;
            }
        }

        if (!crl.getIssuerX500Principal().equals(targetCertificate.getIssuerX500Principal())) {
            logger.debug("CRL issuer and certificate issuer do not match.");
            return false;
        }

        return true;
    }

    /*
     * Filters out CRLs that should not be searched for because they are not correct or corrupt.
     */
    private boolean acceptCRL(X509Certificate targetCertificate, X509CRL crl, PublicKey issuerPublicKey, Date now) {
        boolean accept = false;

        try {
            /* make sure the CRL is signed by the issuer. */
            crl.verify(issuerPublicKey, securityFactory.getNonSensitiveProvider());

            try {
                if (preFilter(targetCertificate, crl)) {
                    accept = true;
                }
            } catch (IOException e) {
                logger.error("IO Error pre-filtering the CRL and certificate.", e);
            }
        } catch (SignatureException e) {
            LogUtils.logWarnStackTraceOnDebug(logger, "CRL could not be verified. Hash not correct", e);
            accept = false;
        } catch (Exception e) {
            LogUtils.logErrorStackTraceOnDebug(logger, "CRL could not be verified.", e);
            accept = false;
        }

        return accept;
    }

    /*
     * Returns a list of all the CRLs matching the selector and which are valid (ie not expired) for the
     * given certificate. The CRLs are verified to see if they are signed by the correct CA (identified 
     * by the public key) 
     */
    private List<X509CRL> findCRLs(X509Certificate targetCertificate, X509CRLSelector crlSelector,
            PublicKey issuerPublicKey, Date now) throws NoSuchProviderException {
        List<X509CRL> crls = new LinkedList<X509CRL>();

        /* step through all the stores and get all the relevant CRLs from the stores */

        for (BasicCRLStore store : crlStores) {
            try {
                CloseableIterator<? extends CRL> crlIterator = store.getCRLIterator(crlSelector);

                try {
                    while (crlIterator.hasNext()) {
                        CRL crl = crlIterator.next();

                        if (!(crl instanceof X509CRL)) {
                            logger.warn("Only X509CRLs are supported. Skipping this CRL.");

                            continue;
                        }

                        X509CRL x509CRL = (X509CRL) crl;

                        if (acceptCRL(targetCertificate, x509CRL, issuerPublicKey, now)) {
                            crls.add(x509CRL);
                        }
                    }
                } finally {
                    crlIterator.close();
                }
            } catch (CRLStoreException e) {
                /* log and continue search */
                logger.error("Error getting CRLs. Skipping this store.", e);

                continue;
            } catch (CloseableIteratorException e) {
                /* log and continue search */
                logger.error("Error stepping through the CRL store. Skipping this store.", e);

                continue;
            }
        }

        return crls;
    }

    /*
     * Do revocation checking for the target Certificate
     */
    private RevocationDetail getRevocationDetail(List<X509CRL> crls, X509Certificate targetCertificate,
            X509Certificate issuerCertificate, PublicKey issuerPublicKey, Date now) throws NoSuchProviderException {
        RevocationDetailImpl detail = new RevocationDetailImpl(RevocationStatus.UNKNOWN);

        boolean validCRLFound = false;

        int reasonMask = 0;

        for (X509CRL crl : crls) {
            BigInteger serialNumber = targetCertificate.getSerialNumber();

            X509CRLEntry crlEntry = crl.getRevokedCertificate(serialNumber);

            Date revocationDate = null;

            if (crlEntry != null) {
                revocationDate = crlEntry.getRevocationDate();

                if (revocationDate == null || !now.before(revocationDate)) {
                    /*
                     * X.509 7.3 NOTE 4  When an implementation processing a certificate revocation list does not 
                     * recognize a critical extension in the crlEntryExtensions field, it shall assume that, 
                     * at a minimum, the identified certificate has been revoked and is no longer valid and 
                     * perform additional actions concerning that revoked certificate as dictated by local policy.
                     *
                     * We do not need to check for unsupported critical extension because if we do not support them
                     * we should assume that the certificate is revoked.
                     */

                    // TODO: add support for onHold/removeFromCRL

                    Integer reasonCode = null;
                    try {
                        reasonCode = X509CRLEntryInspector.getReasonCode(crlEntry);
                    } catch (IOException e) {
                        logger.error("Error retrieving reasonCode.", e);
                    }

                    detail = (reasonCode != null ? new RevocationDetailImpl(RevocationStatus.REVOKED, reasonCode)
                            : new RevocationDetailImpl(RevocationStatus.REVOKED));

                    /* there is no need to continue because certificate is revoked */
                    break;
                } else {
                    if (now.before(revocationDate)) {
                        logger.info("Certificate is revoked in the future.");
                    }
                }
            }

            if (hasUnsupportedCriticalExtensions(crl)) {
                logger.debug("The CRL has unsupported critical extensions.");

                detail = new RevocationDetailImpl(RevocationStatus.UNSUPPORTED_CRITICAL_EXTENSION);

                continue;
            }

            /*
             * check that the start time the CRL is valid is before the time the certificate is 
             * no longer valid. In other words, that the expiration date of the certificate is 
             * later than the date the CRL was issued. It is possible that the certificate was
             * at some point revoked but the CA removed it because the certificate is no longer 
             * valid
             */
            if (crl.getThisUpdate() != null && targetCertificate.getNotAfter().before(crl.getThisUpdate())) {
                logger.info("Certificate has expired before the CRL was valid.");

                continue;
            }

            try {
                if (X509CRLInspector.isDeltaCRL(crl)) {
                    DeltaCRLStatus deltaStatus = getDeltaCRLStatus(targetCertificate, crl, issuerPublicKey, now);

                    if (deltaStatus == DeltaCRLStatus.UNSUPPORTED_CRITICAL_EXTENSION) {
                        detail = new RevocationDetailImpl(RevocationStatus.UNSUPPORTED_CRITICAL_EXTENSION);

                        continue;
                    } else if (deltaStatus == DeltaCRLStatus.UNKNOWN) {
                        continue;
                    }
                } else {
                    if (!acceptCRL_6_3_3_b(targetCertificate, crl)) {
                        logger.debug("CRL not valid according to acceptCRL_6_3_3_b.");
                        continue;
                    }
                }
            } catch (IOException e) {
                logger.error("Error inspecting CRL.", e);

                continue;
            }

            if (crl.getNextUpdate() != null && now.after(crl.getNextUpdate())) {
                /*
                 * an CRL cannot really expire but, when we want at least to log that the 
                 * nextUpdate is overdue
                 */
                logger.debug("The CRL next update is overdue.");

                /* we need to set the nextUpdate if this is a newer CRL */

                if (detail.getStatus() != RevocationStatus.EXPIRED || detail.getNextUpdate() == null) {
                    detail = new RevocationDetailImpl(RevocationStatus.EXPIRED, crl.getNextUpdate());
                } else {
                    if (crl.getNextUpdate().after(detail.getNextUpdate())) {
                        /* the nextUpdate of the current CRL is later so it's longer valid */
                        detail = new RevocationDetailImpl(RevocationStatus.EXPIRED, crl.getNextUpdate());
                    }
                }

                continue;
            }

            try {
                reasonMask = reasonMask | getInterimReasonsMask(targetCertificate, crl);

                /* a valid crl was found. Continue search. */
                validCRLFound = true;
            } catch (IOException e) {
                logger.error("Error getting interim mask.", e);
            }
        }

        /*
         * if one the CRLs was good and the certificate was not revoked we will set the 
         * status to NOT_REVOKED
         */
        if (validCRLFound && detail.getStatus() != RevocationStatus.REVOKED) {
            /* check if all reasons are covered */
            if (reasonMask == allReasons) {
                detail = new RevocationDetailImpl(RevocationStatus.NOT_REVOKED);
            } else {
                logger.debug("Not all reasons were covered.");

                detail = new RevocationDetailImpl(RevocationStatus.UNKNOWN);
            }
        }

        return detail;
    }

    /*
     * Implementation class keeping track of the revocation details of each certificate in
     * the path.
     */
    protected static class RevocationResultImpl implements RevocationResult {
        private final RevocationDetail[] details;

        public RevocationResultImpl(int detailsSize) {
            details = new RevocationDetail[detailsSize];

            for (int i = 0; i < details.length; i++) {
                details[i] = new RevocationDetailImpl();
            }
        }

        /*
         * Returns a positive number for a revocation detail based on the importance of the revocation reason.
         * 0 is least important (ie. not revoked) and the highest number is most important (revoked). This
         * method is used to create an order on the importance.
         */
        private int getImportanceLevel(RevocationDetail detail) {
            switch (detail.getStatus()) {
            case NOT_REVOKED:
                return 0;
            case EXPIRED:
                return 1;
            case UNKNOWN:
                return 2;
            case UNSUPPORTED_CRITICAL_EXTENSION:
                return 3;
            case REVOKED:
                return 4;
            default:
                logger.warn("Unknown revocation status.");

                return Integer.MAX_VALUE;
            }
        }

        /*
         * Returns the accumulated detail. The returned result will be the most important result
         * from one of the details ie. if a certificate in the chain is revoked the result will
         * be revoked etc. The ordering of the revocation result is determined by getImportanceLevel.
         */
        private RevocationDetail getOveralDetail() {
            RevocationDetail mostImportantDetail = null;

            int maxLevel = -1;

            /*
             * step through all entries and update the current based upon the level
             */
            for (RevocationDetail detail : details) {
                int level = getImportanceLevel(detail);

                if (level > maxLevel) {
                    /* this revocation status is more important than the current status */
                    maxLevel = level;

                    mostImportantDetail = detail;
                }
            }

            if (mostImportantDetail == null) {
                mostImportantDetail = new RevocationDetailImpl(RevocationStatus.UNKNOWN);
            }

            return mostImportantDetail;
        }

        /*
         * Returns the accumulated status. If all certificates are NOT_REVOKED the
         * result will be NOT_REVOKED. If one certificate has another status that
         * status will be returned
         */
        @Override
        public RevocationStatus getStatus() {
            return getOveralDetail().getStatus();
        }

        @Override
        public RevocationReason getReason() {
            return getOveralDetail().getReason();
        }

        @Override
        public RevocationDetail[] getDetails() {
            return details;
        }

        @Override
        public String toString() {
            StrBuilder sb = new StrBuilder(1024);

            sb.appendAll(details);

            return sb.toString();
        }
    }

    /*
     * Revocation detail for one certificate in the chain
     */
    protected static class RevocationDetailImpl implements RevocationDetail {
        private final RevocationStatus status;
        private RevocationReason reason;
        private Date nextUpdate;

        public RevocationDetailImpl(RevocationStatus status, int reasonTag) {
            Check.notNull(status, "status");

            this.status = status;
            this.reason = RevocationReason.fromTag(reasonTag);
        }

        public RevocationDetailImpl(RevocationStatus status, Date nextUpdate) {
            Check.notNull(status, "status");

            this.status = status;
            this.reason = null;
            this.nextUpdate = nextUpdate;
        }

        public RevocationDetailImpl(RevocationStatus status) {
            Check.notNull(status, "status");

            this.status = status;
            this.reason = null;
        }

        public RevocationDetailImpl() {
            this.status = RevocationStatus.UNKNOWN;
            this.reason = null;
        }

        @Override
        public RevocationStatus getStatus() {
            return status;
        }

        @Override
        public RevocationReason getReason() {
            return reason;
        }

        @Override
        public Date getNextUpdate() {
            return nextUpdate;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();

            sb.append("Status: " + status.toString());

            if (reason != null) {
                sb.append("; Reason: " + reason.getFriendlyName());
            }

            if (nextUpdate != null) {
                sb.append("; nextUpdate: " + nextUpdate);

            }

            return sb.toString();
        }
    }
}