/*
 * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.provider.certpath;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.AccessController;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertPathValidatorException.BasicReason;
import java.security.cert.Extension;
import java.security.cert.*;
import java.util.*;
import javax.security.auth.x500.X500Principal;

import static sun.security.provider.certpath.OCSP.*;
import static sun.security.provider.certpath.PKIX.*;
import sun.security.x509.*;
import static sun.security.x509.PKIXExtensions.*;
import sun.security.util.Debug;

class RevocationChecker extends PKIXRevocationChecker {

    private static final Debug debug = Debug.getInstance("certpath");

    private TrustAnchor anchor;
    private ValidatorParams params;
    private boolean onlyEE;
    private boolean softFail;
    private boolean crlDP;
    private URI responderURI;
    private X509Certificate responderCert;
    private List<CertStore> certStores;
    private Map<X509Certificate, byte[]> ocspResponses;
    private List<Extension> ocspExtensions;
    private final boolean legacy;
    private LinkedList<CertPathValidatorException> softFailExceptions =
        new LinkedList<>();

    // state variables
    private OCSPResponse.IssuerInfo issuerInfo;
    private PublicKey prevPubKey;
    private boolean crlSignFlag;
    private int certIndex;

    private enum Mode { PREFER_OCSP, PREFER_CRLS, ONLY_CRLS, ONLY_OCSP };
    private Mode mode = Mode.PREFER_OCSP;

    private static class RevocationProperties {
        boolean onlyEE;
        boolean ocspEnabled;
        boolean crlDPEnabled;
        String ocspUrl;
        String ocspSubject;
        String ocspIssuer;
        String ocspSerial;
    }

    RevocationChecker() {
        legacy = false;
    }

    RevocationChecker(TrustAnchor anchor, ValidatorParams params)
        throws CertPathValidatorException
    {
        legacy = true;
        init(anchor, params);
    }

    void init(TrustAnchor anchor, ValidatorParams params)
        throws CertPathValidatorException
    {
        RevocationProperties rp = getRevocationProperties();
        URI uri = getOcspResponder();
        responderURI = (uri == null) ? toURI(rp.ocspUrl) : uri;
        X509Certificate cert = getOcspResponderCert();
        responderCert = (cert == null)
                        ? getResponderCert(rp, params.trustAnchors(),
                                           params.certStores())
                        : cert;
        Set<Option> options = getOptions();
        for (Option option : options) {
            switch (option) {
            case ONLY_END_ENTITY:
            case PREFER_CRLS:
            case SOFT_FAIL:
            case NO_FALLBACK:
                break;
            default:
                throw new CertPathValidatorException(
                    "Unrecognized revocation parameter option: " + option);
            }
        }
        softFail = options.contains(Option.SOFT_FAIL);

        // set mode, only end entity flag
        if (legacy) {
            mode = (rp.ocspEnabled) ? Mode.PREFER_OCSP : Mode.ONLY_CRLS;
            onlyEE = rp.onlyEE;
        } else {
            if (options.contains(Option.NO_FALLBACK)) {
                if (options.contains(Option.PREFER_CRLS)) {
                    mode = Mode.ONLY_CRLS;
                } else {
                    mode = Mode.ONLY_OCSP;
                }
            } else if (options.contains(Option.PREFER_CRLS)) {
                mode = Mode.PREFER_CRLS;
            }
            onlyEE = options.contains(Option.ONLY_END_ENTITY);
        }
        if (legacy) {
            crlDP = rp.crlDPEnabled;
        } else {
            crlDP = true;
        }
        ocspResponses = getOcspResponses();
        ocspExtensions = getOcspExtensions();

        this.anchor = anchor;
        this.params = params;
        this.certStores = new ArrayList<>(params.certStores());
        try {
            this.certStores.add(CertStore.getInstance("Collection",
                new CollectionCertStoreParameters(params.certificates())));
        } catch (InvalidAlgorithmParameterException |
                 NoSuchAlgorithmException e) {
            // should never occur but not necessarily fatal, so log it,
            // ignore and continue
            if (debug != null) {
                debug.println("RevocationChecker: " +
                              "error creating Collection CertStore: " + e);
            }
        }
    }

    private static URI toURI(String uriString)
        throws CertPathValidatorException
    {
        try {
            if (uriString != null) {
                return new URI(uriString);
            }
            return null;
        } catch (URISyntaxException e) {
            throw new CertPathValidatorException(
                "cannot parse ocsp.responderURL property", e);
        }
    }

    private static RevocationProperties getRevocationProperties() {
        return AccessController.doPrivileged(
            new PrivilegedAction<RevocationProperties>() {
                public RevocationProperties run() {
                    RevocationProperties rp = new RevocationProperties();
                    String onlyEE = Security.getProperty(
                        "com.sun.security.onlyCheckRevocationOfEECert");
                    rp.onlyEE = onlyEE != null
                                && onlyEE.equalsIgnoreCase("true");
                    String ocspEnabled = Security.getProperty("ocsp.enable");
                    rp.ocspEnabled = ocspEnabled != null
                                     && ocspEnabled.equalsIgnoreCase("true");
                    rp.ocspUrl = Security.getProperty("ocsp.responderURL");
                    rp.ocspSubject
                        = Security.getProperty("ocsp.responderCertSubjectName");
                    rp.ocspIssuer
                        = Security.getProperty("ocsp.responderCertIssuerName");
                    rp.ocspSerial
                        = Security.getProperty("ocsp.responderCertSerialNumber");
                    rp.crlDPEnabled
                        = Boolean.getBoolean("com.sun.security.enableCRLDP");
                    return rp;
                }
            }
        );
    }

    private static X509Certificate getResponderCert(RevocationProperties rp,
                                                    Set<TrustAnchor> anchors,
                                                    List<CertStore> stores)
        throws CertPathValidatorException
    {
        if (rp.ocspSubject != null) {
            return getResponderCert(rp.ocspSubject, anchors, stores);
        } else if (rp.ocspIssuer != null && rp.ocspSerial != null) {
            return getResponderCert(rp.ocspIssuer, rp.ocspSerial,
                                    anchors, stores);
        } else if (rp.ocspIssuer != null || rp.ocspSerial != null) {
            throw new CertPathValidatorException(
                "Must specify both ocsp.responderCertIssuerName and " +
                "ocsp.responderCertSerialNumber properties");
        }
        return null;
    }

    private static X509Certificate getResponderCert(String subject,
                                                    Set<TrustAnchor> anchors,
                                                    List<CertStore> stores)
        throws CertPathValidatorException
    {
        X509CertSelector sel = new X509CertSelector();
        try {
            sel.setSubject(new X500Principal(subject));
        } catch (IllegalArgumentException e) {
            throw new CertPathValidatorException(
                "cannot parse ocsp.responderCertSubjectName property", e);
        }
        return getResponderCert(sel, anchors, stores);
    }

    private static X509Certificate getResponderCert(String issuer,
                                                    String serial,
                                                    Set<TrustAnchor> anchors,
                                                    List<CertStore> stores)
        throws CertPathValidatorException
    {
        X509CertSelector sel = new X509CertSelector();
        try {
            sel.setIssuer(new X500Principal(issuer));
        } catch (IllegalArgumentException e) {
            throw new CertPathValidatorException(
                "cannot parse ocsp.responderCertIssuerName property", e);
        }
        try {
            sel.setSerialNumber(new BigInteger(stripOutSeparators(serial), 16));
        } catch (NumberFormatException e) {
            throw new CertPathValidatorException(
                "cannot parse ocsp.responderCertSerialNumber property", e);
        }
        return getResponderCert(sel, anchors, stores);
    }

    private static X509Certificate getResponderCert(X509CertSelector sel,
                                                    Set<TrustAnchor> anchors,
                                                    List<CertStore> stores)
        throws CertPathValidatorException
    {
        // first check TrustAnchors
        for (TrustAnchor anchor : anchors) {
            X509Certificate cert = anchor.getTrustedCert();
            if (cert == null) {
                continue;
            }
            if (sel.match(cert)) {
                return cert;
            }
        }
        // now check CertStores
        for (CertStore store : stores) {
            try {
                Collection<? extends Certificate> certs =
                    store.getCertificates(sel);
                if (!certs.isEmpty()) {
                    return (X509Certificate)certs.iterator().next();
                }
            } catch (CertStoreException e) {
                // ignore and try next CertStore
                if (debug != null) {
                    debug.println("CertStore exception:" + e);
                }
                continue;
            }
        }
        throw new CertPathValidatorException(
            "Cannot find the responder's certificate " +
            "(set using the OCSP security properties).");
    }

    @Override
    public void init(boolean forward) throws CertPathValidatorException {
        if (forward) {
            throw new
                CertPathValidatorException("forward checking not supported");
        }
        if (anchor != null) {
            issuerInfo = new OCSPResponse.IssuerInfo(anchor);
            prevPubKey = issuerInfo.getPublicKey();

        }
        crlSignFlag = true;
        if (params != null && params.certPath() != null) {
            certIndex = params.certPath().getCertificates().size() - 1;
        } else {
            certIndex = -1;
        }
        softFailExceptions.clear();
    }

    @Override
    public boolean isForwardCheckingSupported() {
        return false;
    }

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

    @Override
    public List<CertPathValidatorException> getSoftFailExceptions() {
        return Collections.unmodifiableList(softFailExceptions);
    }

    @Override
    public void check(Certificate cert, Collection<String> unresolvedCritExts)
        throws CertPathValidatorException
    {
        check((X509Certificate)cert, unresolvedCritExts,
              prevPubKey, crlSignFlag);
    }

    private void check(X509Certificate xcert,
                       Collection<String> unresolvedCritExts,
                       PublicKey pubKey, boolean crlSignFlag)
        throws CertPathValidatorException
    {
        if (debug != null) {
            debug.println("RevocationChecker.check: checking cert" +
                "\n  SN: " + Debug.toHexString(xcert.getSerialNumber()) +
                "\n  Subject: " + xcert.getSubjectX500Principal() +
                "\n  Issuer: " + xcert.getIssuerX500Principal());
        }
        try {
            if (onlyEE && xcert.getBasicConstraints() != -1) {
                if (debug != null) {
                    debug.println("Skipping revocation check; cert is not " +
                                  "an end entity cert");
                }
                return;
            }
            switch (mode) {
                case PREFER_OCSP:
                case ONLY_OCSP:
                    checkOCSP(xcert, unresolvedCritExts);
                    break;
                case PREFER_CRLS:
                case ONLY_CRLS:
                    checkCRLs(xcert, unresolvedCritExts, null,
                              pubKey, crlSignFlag);
                    break;
            }
        } catch (CertPathValidatorException e) {
            if (e.getReason() == BasicReason.REVOKED) {
                throw e;
            }
            boolean eSoftFail = isSoftFailException(e);
            if (eSoftFail) {
                if (mode == Mode.ONLY_OCSP || mode == Mode.ONLY_CRLS) {
                    return;
                }
            } else {
                if (mode == Mode.ONLY_OCSP || mode == Mode.ONLY_CRLS) {
                    throw e;
                }
            }
            CertPathValidatorException cause = e;
            // Otherwise, failover
            if (debug != null) {
                debug.println("RevocationChecker.check() " + e.getMessage());
                debug.println("RevocationChecker.check() preparing to failover");
            }
            try {
                switch (mode) {
                    case PREFER_OCSP:
                        checkCRLs(xcert, unresolvedCritExts, null,
                                  pubKey, crlSignFlag);
                        break;
                    case PREFER_CRLS:
                        checkOCSP(xcert, unresolvedCritExts);
                        break;
                }
            } catch (CertPathValidatorException x) {
                if (debug != null) {
                    debug.println("RevocationChecker.check() failover failed");
                    debug.println("RevocationChecker.check() " + x.getMessage());
                }
                if (x.getReason() == BasicReason.REVOKED) {
                    throw x;
                }
                if (!isSoftFailException(x)) {
                    cause.addSuppressed(x);
                    throw cause;
                } else {
                    // only pass if both exceptions were soft failures
                    if (!eSoftFail) {
                        throw cause;
                    }
                }
            }
        } finally {
            updateState(xcert);
        }
    }

    private boolean isSoftFailException(CertPathValidatorException e) {
        if (softFail &&
            e.getReason() == BasicReason.UNDETERMINED_REVOCATION_STATUS)
        {
            // recreate exception with correct index
            CertPathValidatorException e2 = new CertPathValidatorException(
                e.getMessage(), e.getCause(), params.certPath(), certIndex,
                e.getReason());
            softFailExceptions.addFirst(e2);
            return true;
        }
        return false;
    }

    private void updateState(X509Certificate cert)
        throws CertPathValidatorException
    {
        issuerInfo = new OCSPResponse.IssuerInfo(anchor, cert);

        // Make new public key if parameters are missing
        PublicKey pubKey = cert.getPublicKey();
        if (PKIX.isDSAPublicKeyWithoutParams(pubKey)) {
            // pubKey needs to inherit DSA parameters from prev key
            pubKey = BasicChecker.makeInheritedParamsKey(pubKey, prevPubKey);
        }
        prevPubKey = pubKey;
        crlSignFlag = certCanSignCrl(cert);
        if (certIndex > 0) {
            certIndex--;
        }
    }

    // Maximum clock skew in milliseconds (15 minutes) allowed when checking
    // validity of CRLs
    private static final long MAX_CLOCK_SKEW = 900000;
    private void checkCRLs(X509Certificate cert,
                           Collection<String> unresolvedCritExts,
                           Set<X509Certificate> stackedCerts,
                           PublicKey pubKey, boolean signFlag)
        throws CertPathValidatorException
    {
        checkCRLs(cert, pubKey, null, signFlag, true,
                  stackedCerts, params.trustAnchors());
    }

    static boolean isCausedByNetworkIssue(String type, CertStoreException cse) {
        boolean result;
        Throwable t = cse.getCause();

        switch (type) {
            case "LDAP":
                if (t != null) {
                    // These two exception classes are inside java.naming module
                    String cn = t.getClass().getName();
                    result = (cn.equals("javax.naming.ServiceUnavailableException") ||
                        cn.equals("javax.naming.CommunicationException"));
                } else {
                    result = false;
                }
                break;
            case "SSLServer":
                result = (t != null && t instanceof IOException);
                break;
            case "URI":
                result = (t != null && t instanceof IOException);
                break;
            default:
                // we don't know about any other remote CertStore types
                return false;
        }
        return result;
    }

    private void checkCRLs(X509Certificate cert, PublicKey prevKey,
                           X509Certificate prevCert, boolean signFlag,
                           boolean allowSeparateKey,
                           Set<X509Certificate> stackedCerts,
                           Set<TrustAnchor> anchors)
        throws CertPathValidatorException
    {
        if (debug != null) {
            debug.println("RevocationChecker.checkCRLs()" +
                          " ---checking revocation status ...");
        }

        // Reject circular dependencies - RFC 5280 is not explicit on how
        // to handle this, but does suggest that they can be a security
        // risk and can create unresolvable dependencies
        if (stackedCerts != null && stackedCerts.contains(cert)) {
            if (debug != null) {
                debug.println("RevocationChecker.checkCRLs()" +
                              " circular dependency");
            }
            throw new CertPathValidatorException
                 ("Could not determine revocation status", null, null, -1,
                  BasicReason.UNDETERMINED_REVOCATION_STATUS);
        }

        Set<X509CRL> possibleCRLs = new HashSet<>();
        Set<X509CRL> approvedCRLs = new HashSet<>();
        X509CRLSelector sel = new X509CRLSelector();
        sel.setCertificateChecking(cert);
        CertPathHelper.setDateAndTime(sel, params.date(), MAX_CLOCK_SKEW);

        // First, check user-specified CertStores
        CertPathValidatorException networkFailureException = null;
        for (CertStore store : certStores) {
            try {
                for (CRL crl : store.getCRLs(sel)) {
                    possibleCRLs.add((X509CRL)crl);
                }
            } catch (CertStoreException e) {
                if (debug != null) {
                    debug.println("RevocationChecker.checkCRLs() " +
                                  "CertStoreException: " + e.getMessage());
                }
                if (networkFailureException == null &&
                    isCausedByNetworkIssue(store.getType(),e)) {
                    // save this exception, we may need to throw it later
                    networkFailureException = new CertPathValidatorException(
                        "Unable to determine revocation status due to " +
                        "network error", e, null, -1,
                        BasicReason.UNDETERMINED_REVOCATION_STATUS);
                }
            }
        }

        if (debug != null) {
            debug.println("RevocationChecker.checkCRLs() " +
                          "possible crls.size() = " + possibleCRLs.size());
        }
        boolean[] reasonsMask = new boolean[9];
        if (!possibleCRLs.isEmpty()) {
            // Now that we have a list of possible CRLs, see which ones can
            // be approved
            approvedCRLs.addAll(verifyPossibleCRLs(possibleCRLs, cert, prevKey,
                                                   signFlag, reasonsMask,
                                                   anchors));
        }

        if (debug != null) {
            debug.println("RevocationChecker.checkCRLs() " +
                          "approved crls.size() = " + approvedCRLs.size());
        }

        // make sure that we have at least one CRL that _could_ cover
        // the certificate in question and all reasons are covered
        if (!approvedCRLs.isEmpty() &&
            Arrays.equals(reasonsMask, ALL_REASONS))
        {
            checkApprovedCRLs(cert, approvedCRLs);
        } else {
            // Check Distribution Points
            // all CRLs returned by the DP Fetcher have also been verified
            try {
                if (crlDP) {
                    approvedCRLs.addAll(DistributionPointFetcher.getCRLs(
                                        sel, signFlag, prevKey, prevCert,
                                        params.sigProvider(), certStores,
                                        reasonsMask, anchors, null, params.variant()));
                }
            } catch (CertStoreException e) {
                if (e instanceof CertStoreTypeException) {
                    CertStoreTypeException cste = (CertStoreTypeException)e;
                    if (isCausedByNetworkIssue(cste.getType(), e)) {
                        throw new CertPathValidatorException(
                            "Unable to determine revocation status due to " +
                            "network error", e, null, -1,
                            BasicReason.UNDETERMINED_REVOCATION_STATUS);
                    }
                }
                throw new CertPathValidatorException(e);
            }
            if (!approvedCRLs.isEmpty() &&
                Arrays.equals(reasonsMask, ALL_REASONS))
            {
                checkApprovedCRLs(cert, approvedCRLs);
            } else {
                if (allowSeparateKey) {
                    try {
                        verifyWithSeparateSigningKey(cert, prevKey, signFlag,
                                                     stackedCerts);
                        return;
                    } catch (CertPathValidatorException cpve) {
                        if (networkFailureException != null) {
                            // if a network issue previously prevented us from
                            // retrieving a CRL from one of the user-specified
                            // CertStores, throw it now so it can be handled
                            // appropriately
                            throw networkFailureException;
                        }
                        throw cpve;
                    }
                } else {
                    if (networkFailureException != null) {
                        // if a network issue previously prevented us from
                        // retrieving a CRL from one of the user-specified
                        // CertStores, throw it now so it can be handled
                        // appropriately
                        throw networkFailureException;
                    }
                    throw new CertPathValidatorException(
                        "Could not determine revocation status", null, null, -1,
                        BasicReason.UNDETERMINED_REVOCATION_STATUS);
                }
            }
        }
    }

    private void checkApprovedCRLs(X509Certificate cert,
                                   Set<X509CRL> approvedCRLs)
        throws CertPathValidatorException
    {
        // See if the cert is in the set of approved crls.
        if (debug != null) {
            BigInteger sn = cert.getSerialNumber();
            debug.println("RevocationChecker.checkApprovedCRLs() " +
                          "starting the final sweep...");
            debug.println("RevocationChecker.checkApprovedCRLs()" +
                          " cert SN: " + sn.toString());
        }

        CRLReason reasonCode = CRLReason.UNSPECIFIED;
        X509CRLEntryImpl entry = null;
        for (X509CRL crl : approvedCRLs) {
            X509CRLEntry e = crl.getRevokedCertificate(cert);
            if (e != null) {
                try {
                    entry = X509CRLEntryImpl.toImpl(e);
                } catch (CRLException ce) {
                    throw new CertPathValidatorException(ce);
                }
                if (debug != null) {
                    debug.println("RevocationChecker.checkApprovedCRLs()"
                        + " CRL entry: " + entry.toString());
                }

                /*
                 * Abort CRL validation and throw exception if there are any
                 * unrecognized critical CRL entry extensions (see section
                 * 5.3 of RFC 5280).
                 */
                Set<String> unresCritExts = entry.getCriticalExtensionOIDs();
                if (unresCritExts != null && !unresCritExts.isEmpty()) {
                    /* remove any that we will process */
                    unresCritExts.remove(ReasonCode_Id.toString());
                    unresCritExts.remove(CertificateIssuer_Id.toString());
                    if (!unresCritExts.isEmpty()) {
                        throw new CertPathValidatorException(
                            "Unrecognized critical extension(s) in revoked " +
                            "CRL entry");
                    }
                }

                reasonCode = entry.getRevocationReason();
                if (reasonCode == null) {
                    reasonCode = CRLReason.UNSPECIFIED;
                }
                Date revocationDate = entry.getRevocationDate();
                if (revocationDate.before(params.date())) {
                    Throwable t = new CertificateRevokedException(
                        revocationDate, reasonCode,
                        crl.getIssuerX500Principal(), entry.getExtensions());
                    throw new CertPathValidatorException(
                        t.getMessage(), t, null, -1, BasicReason.REVOKED);
                }
            }
        }
    }

    private void checkOCSP(X509Certificate cert,
                           Collection<String> unresolvedCritExts)
        throws CertPathValidatorException
    {
        X509CertImpl currCert = null;
        try {
            currCert = X509CertImpl.toImpl(cert);
        } catch (CertificateException ce) {
            throw new CertPathValidatorException(ce);
        }

        // The algorithm constraints of the OCSP trusted responder certificate
        // does not need to be checked in this code. The constraints will be
        // checked when the responder's certificate is validated.

        OCSPResponse response = null;
        CertId certId = null;
        try {
            certId = new CertId(issuerInfo.getName(), issuerInfo.getPublicKey(),
                    currCert.getSerialNumberObject());

            // check if there is a cached OCSP response available
            byte[] responseBytes = ocspResponses.get(cert);
            if (responseBytes != null) {
                if (debug != null) {
                    debug.println("Found cached OCSP response");
                }
                response = new OCSPResponse(responseBytes);

                // verify the response
                byte[] nonce = null;
                for (Extension ext : ocspExtensions) {
                    if (ext.getId().equals("1.3.6.1.5.5.7.48.1.2")) {
                        nonce = ext.getValue();
                    }
                }
                response.verify(Collections.singletonList(certId), issuerInfo,
                        responderCert, params.date(), nonce, params.variant());

            } else {
                URI responderURI = (this.responderURI != null)
                                   ? this.responderURI
                                   : OCSP.getResponderURI(currCert);
                if (responderURI == null) {
                    throw new CertPathValidatorException(
                        "Certificate does not specify OCSP responder", null,
                        null, -1);
                }

                response = OCSP.check(Collections.singletonList(certId),
                        responderURI, issuerInfo, responderCert, null,
                        ocspExtensions, params.variant());
            }
        } catch (IOException e) {
            throw new CertPathValidatorException(
                "Unable to determine revocation status due to network error",
                e, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS);
        }

        RevocationStatus rs =
            (RevocationStatus)response.getSingleResponse(certId);
        RevocationStatus.CertStatus certStatus = rs.getCertStatus();
        if (certStatus == RevocationStatus.CertStatus.REVOKED) {
            Date revocationTime = rs.getRevocationTime();
            if (revocationTime.before(params.date())) {
                Throwable t = new CertificateRevokedException(
                    revocationTime, rs.getRevocationReason(),
                    response.getSignerCertificate().getSubjectX500Principal(),
                    rs.getSingleExtensions());
                throw new CertPathValidatorException(t.getMessage(), t, null,
                                                     -1, BasicReason.REVOKED);
            }
        } else if (certStatus == RevocationStatus.CertStatus.UNKNOWN) {
            throw new CertPathValidatorException(
                "Certificate's revocation status is unknown", null,
                params.certPath(), -1,
                BasicReason.UNDETERMINED_REVOCATION_STATUS);
        }
    }

    /*
     * Removes any non-hexadecimal characters from a string.
     */
    private static final String HEX_DIGITS = "0123456789ABCDEFabcdef";
    private static String stripOutSeparators(String value) {
        char[] chars = value.toCharArray();
        StringBuilder hexNumber = new StringBuilder();
        for (int i = 0; i < chars.length; i++) {
            if (HEX_DIGITS.indexOf(chars[i]) != -1) {
                hexNumber.append(chars[i]);
            }
        }
        return hexNumber.toString();
    }

    
Checks that a cert can be used to verify a CRL.
Params:
  • cert – an X509Certificate to check
Returns:a boolean specifying if the cert is allowed to vouch for the validity of a CRL
/** * Checks that a cert can be used to verify a CRL. * * @param cert an X509Certificate to check * @return a boolean specifying if the cert is allowed to vouch for the * validity of a CRL */
static boolean certCanSignCrl(X509Certificate cert) { // if the cert doesn't include the key usage ext, or // the key usage ext asserts cRLSigning, return true, // otherwise return false. boolean[] keyUsage = cert.getKeyUsage(); if (keyUsage != null) { return keyUsage[6]; } return false; }
Internal method that verifies a set of possible_crls, and sees if each is approved, based on the cert.
Params:
  • crls – a set of possible CRLs to test for acceptability
  • cert – the certificate whose revocation status is being checked
  • signFlag – true if prevKey was trusted to sign CRLs
  • prevKey – the public key of the issuer of cert
  • reasonsMask – the reason code mask
  • trustAnchors – a Set of TrustAnchors>
Returns:a collection of approved crls (or an empty collection)
/** * Internal method that verifies a set of possible_crls, * and sees if each is approved, based on the cert. * * @param crls a set of possible CRLs to test for acceptability * @param cert the certificate whose revocation status is being checked * @param signFlag <code>true</code> if prevKey was trusted to sign CRLs * @param prevKey the public key of the issuer of cert * @param reasonsMask the reason code mask * @param trustAnchors a <code>Set</code> of <code>TrustAnchor</code>s> * @return a collection of approved crls (or an empty collection) */
private static final boolean[] ALL_REASONS = {true, true, true, true, true, true, true, true, true}; private Collection<X509CRL> verifyPossibleCRLs(Set<X509CRL> crls, X509Certificate cert, PublicKey prevKey, boolean signFlag, boolean[] reasonsMask, Set<TrustAnchor> anchors) throws CertPathValidatorException { try { X509CertImpl certImpl = X509CertImpl.toImpl(cert); if (debug != null) { debug.println("RevocationChecker.verifyPossibleCRLs: " + "Checking CRLDPs for " + certImpl.getSubjectX500Principal()); } CRLDistributionPointsExtension ext = certImpl.getCRLDistributionPointsExtension(); List<DistributionPoint> points = null; if (ext == null) { // assume a DP with reasons and CRLIssuer fields omitted // and a DP name of the cert issuer. // TODO add issuerAltName too X500Name certIssuer = (X500Name)certImpl.getIssuerDN(); DistributionPoint point = new DistributionPoint( new GeneralNames().add(new GeneralName(certIssuer)), null, null); points = Collections.singletonList(point); } else { points = ext.get(CRLDistributionPointsExtension.POINTS); } Set<X509CRL> results = new HashSet<>(); for (DistributionPoint point : points) { for (X509CRL crl : crls) { if (DistributionPointFetcher.verifyCRL( certImpl, point, crl, reasonsMask, signFlag, prevKey, null, params.sigProvider(), anchors, certStores, params.date(), params.variant())) { results.add(crl); } } if (Arrays.equals(reasonsMask, ALL_REASONS)) break; } return results; } catch (CertificateException | CRLException | IOException e) { if (debug != null) { debug.println("Exception while verifying CRL: "+e.getMessage()); e.printStackTrace(); } return Collections.emptySet(); } }
We have a cert whose revocation status couldn't be verified by a CRL issued by the cert that issued the CRL. See if we can find a valid CRL issued by a separate key that can verify the revocation status of this certificate.

Note that this does not provide support for indirect CRLs, only CRLs signed with a different key (but the same issuer name) as the certificate being checked.

Params:
  • currCert – the X509Certificate to be checked
  • prevKey – the PublicKey that failed
  • signFlag – true if that key was trusted to sign CRLs
  • stackedCerts – a Set of X509Certificates> whose revocation status depends on the non-revoked status of this cert. To avoid circular dependencies, we assume they're revoked while checking the revocation status of this cert.
Throws:
/** * We have a cert whose revocation status couldn't be verified by * a CRL issued by the cert that issued the CRL. See if we can * find a valid CRL issued by a separate key that can verify the * revocation status of this certificate. * <p> * Note that this does not provide support for indirect CRLs, * only CRLs signed with a different key (but the same issuer * name) as the certificate being checked. * * @param currCert the <code>X509Certificate</code> to be checked * @param prevKey the <code>PublicKey</code> that failed * @param signFlag <code>true</code> if that key was trusted to sign CRLs * @param stackedCerts a <code>Set</code> of <code>X509Certificate</code>s> * whose revocation status depends on the * non-revoked status of this cert. To avoid * circular dependencies, we assume they're * revoked while checking the revocation * status of this cert. * @throws CertPathValidatorException if the cert's revocation status * cannot be verified successfully with another key */
private void verifyWithSeparateSigningKey(X509Certificate cert, PublicKey prevKey, boolean signFlag, Set<X509Certificate> stackedCerts) throws CertPathValidatorException { String msg = "revocation status"; if (debug != null) { debug.println( "RevocationChecker.verifyWithSeparateSigningKey()" + " ---checking " + msg + "..."); } // Reject circular dependencies - RFC 5280 is not explicit on how // to handle this, but does suggest that they can be a security // risk and can create unresolvable dependencies if ((stackedCerts != null) && stackedCerts.contains(cert)) { if (debug != null) { debug.println( "RevocationChecker.verifyWithSeparateSigningKey()" + " circular dependency"); } throw new CertPathValidatorException ("Could not determine revocation status", null, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS); } // Try to find another key that might be able to sign // CRLs vouching for this cert. // If prevKey wasn't trusted, maybe we just didn't have the right // path to it. Don't rule that key out. if (!signFlag) { buildToNewKey(cert, null, stackedCerts); } else { buildToNewKey(cert, prevKey, stackedCerts); } }
Tries to find a CertPath that establishes a key that can be used to verify the revocation status of a given certificate. Ignores keys that have previously been tried. Throws a CertPathValidatorException if no such key could be found.
Params:
  • currCert – the X509Certificate to be checked
  • prevKey – the PublicKey of the certificate whose key cannot be used to vouch for the CRL and should be ignored
  • stackedCerts – a Set of X509Certificates> whose revocation status depends on the establishment of this path.
Throws:
  • CertPathValidatorException – on failure
/** * Tries to find a CertPath that establishes a key that can be * used to verify the revocation status of a given certificate. * Ignores keys that have previously been tried. Throws a * CertPathValidatorException if no such key could be found. * * @param currCert the <code>X509Certificate</code> to be checked * @param prevKey the <code>PublicKey</code> of the certificate whose key * cannot be used to vouch for the CRL and should be ignored * @param stackedCerts a <code>Set</code> of <code>X509Certificate</code>s> * whose revocation status depends on the * establishment of this path. * @throws CertPathValidatorException on failure */
private static final boolean [] CRL_SIGN_USAGE = { false, false, false, false, false, false, true }; private void buildToNewKey(X509Certificate currCert, PublicKey prevKey, Set<X509Certificate> stackedCerts) throws CertPathValidatorException { if (debug != null) { debug.println("RevocationChecker.buildToNewKey()" + " starting work"); } Set<PublicKey> badKeys = new HashSet<>(); if (prevKey != null) { badKeys.add(prevKey); } X509CertSelector certSel = new RejectKeySelector(badKeys); certSel.setSubject(currCert.getIssuerX500Principal()); certSel.setKeyUsage(CRL_SIGN_USAGE); Set<TrustAnchor> newAnchors = anchor == null ? params.trustAnchors() : Collections.singleton(anchor); PKIXBuilderParameters builderParams; try { builderParams = new PKIXBuilderParameters(newAnchors, certSel); } catch (InvalidAlgorithmParameterException iape) { throw new RuntimeException(iape); // should never occur } builderParams.setInitialPolicies(params.initialPolicies()); builderParams.setCertStores(certStores); builderParams.setExplicitPolicyRequired (params.explicitPolicyRequired()); builderParams.setPolicyMappingInhibited (params.policyMappingInhibited()); builderParams.setAnyPolicyInhibited(params.anyPolicyInhibited()); // Policy qualifiers must be rejected, since we don't have // any way to convey them back to the application. // That's the default, so no need to write code. builderParams.setDate(params.date()); builderParams.setCertPathCheckers(params.certPathCheckers()); builderParams.setSigProvider(params.sigProvider()); // Skip revocation during this build to detect circular // references. But check revocation afterwards, using the // key (or any other that works). builderParams.setRevocationEnabled(false); // check for AuthorityInformationAccess extension if (Builder.USE_AIA == true) { X509CertImpl currCertImpl = null; try { currCertImpl = X509CertImpl.toImpl(currCert); } catch (CertificateException ce) { // ignore but log it if (debug != null) { debug.println("RevocationChecker.buildToNewKey: " + "error decoding cert: " + ce); } } AuthorityInfoAccessExtension aiaExt = null; if (currCertImpl != null) { aiaExt = currCertImpl.getAuthorityInfoAccessExtension(); } if (aiaExt != null) { List<AccessDescription> adList = aiaExt.getAccessDescriptions(); if (adList != null) { for (AccessDescription ad : adList) { CertStore cs = URICertStore.getInstance(ad); if (cs != null) { if (debug != null) { debug.println("adding AIAext CertStore"); } builderParams.addCertStore(cs); } } } } } CertPathBuilder builder = null; try { builder = CertPathBuilder.getInstance("PKIX"); } catch (NoSuchAlgorithmException nsae) { throw new CertPathValidatorException(nsae); } while (true) { try { if (debug != null) { debug.println("RevocationChecker.buildToNewKey()" + " about to try build ..."); } PKIXCertPathBuilderResult cpbr = (PKIXCertPathBuilderResult)builder.build(builderParams); if (debug != null) { debug.println("RevocationChecker.buildToNewKey()" + " about to check revocation ..."); } // Now check revocation of all certs in path, assuming that // the stackedCerts are revoked. if (stackedCerts == null) { stackedCerts = new HashSet<X509Certificate>(); } stackedCerts.add(currCert); TrustAnchor ta = cpbr.getTrustAnchor(); PublicKey prevKey2 = ta.getCAPublicKey(); if (prevKey2 == null) { prevKey2 = ta.getTrustedCert().getPublicKey(); } boolean signFlag = true; List<? extends Certificate> cpList = cpbr.getCertPath().getCertificates(); try { for (int i = cpList.size() - 1; i >= 0; i--) { X509Certificate cert = (X509Certificate) cpList.get(i); if (debug != null) { debug.println("RevocationChecker.buildToNewKey()" + " index " + i + " checking " + cert); } checkCRLs(cert, prevKey2, null, signFlag, true, stackedCerts, newAnchors); signFlag = certCanSignCrl(cert); prevKey2 = cert.getPublicKey(); } } catch (CertPathValidatorException cpve) { // ignore it and try to get another key badKeys.add(cpbr.getPublicKey()); continue; } if (debug != null) { debug.println("RevocationChecker.buildToNewKey()" + " got key " + cpbr.getPublicKey()); } // Now check revocation on the current cert using that key and // the corresponding certificate. // If it doesn't check out, try to find a different key. // And if we can't find a key, then return false. PublicKey newKey = cpbr.getPublicKey(); X509Certificate newCert = cpList.isEmpty() ? null : (X509Certificate) cpList.get(0); try { checkCRLs(currCert, newKey, newCert, true, false, null, params.trustAnchors()); // If that passed, the cert is OK! return; } catch (CertPathValidatorException cpve) { // If it is revoked, rethrow exception if (cpve.getReason() == BasicReason.REVOKED) { throw cpve; } // Otherwise, ignore the exception and // try to get another key. } badKeys.add(newKey); } catch (InvalidAlgorithmParameterException iape) { throw new CertPathValidatorException(iape); } catch (CertPathBuilderException cpbe) { throw new CertPathValidatorException ("Could not determine revocation status", null, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS); } } } /* * This inner class extends the X509CertSelector to add an additional * check to make sure the subject public key isn't on a particular list. * This class is used by buildToNewKey() to make sure the builder doesn't * end up with a CertPath to a public key that has already been rejected. */ private static class RejectKeySelector extends X509CertSelector { private final Set<PublicKey> badKeySet;
Creates a new RejectKeySelector.
Params:
  • badPublicKeys – a Set of PublicKeys that should be rejected (or null if no such check should be done)
/** * Creates a new <code>RejectKeySelector</code>. * * @param badPublicKeys a <code>Set</code> of * <code>PublicKey</code>s that * should be rejected (or <code>null</code> * if no such check should be done) */
RejectKeySelector(Set<PublicKey> badPublicKeys) { this.badKeySet = badPublicKeys; }
Decides whether a Certificate should be selected.
Params:
  • cert – the Certificate to be checked
Returns:true if the Certificate should be selected, false otherwise
/** * Decides whether a <code>Certificate</code> should be selected. * * @param cert the <code>Certificate</code> to be checked * @return <code>true</code> if the <code>Certificate</code> should be * selected, <code>false</code> otherwise */
@Override public boolean match(Certificate cert) { if (!super.match(cert)) return(false); if (badKeySet.contains(cert.getPublicKey())) { if (debug != null) debug.println("RejectKeySelector.match: bad key"); return false; } if (debug != null) debug.println("RejectKeySelector.match: returning true"); return true; }
Return a printable representation of the CertSelector.
Returns:a String describing the contents of the CertSelector
/** * Return a printable representation of the <code>CertSelector</code>. * * @return a <code>String</code> describing the contents of the * <code>CertSelector</code> */
@Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("RejectKeySelector: [\n"); sb.append(super.toString()); sb.append(badKeySet); sb.append("]"); return sb.toString(); } } }