/* * Copyright (c) 2012, 2016, 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)); } } 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); } 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); } } 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. * * @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. * * @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())) { 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. * <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. * * @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()); // CertPathCheckers need to be cloned to start from fresh state builderParams.setCertPathCheckers( params.getPKIXParameters().getCertPathCheckers()); 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); } } } @Override public RevocationChecker clone() { RevocationChecker copy = (RevocationChecker)super.clone(); // we don't deep-copy the exceptions, but that is ok because they // are never modified after they are instantiated copy.softFailExceptions = new LinkedList<>(softFailExceptions); return copy; } /* * 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 <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 <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 <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(); } } }