blob: 13aae31b4eb7f5eca3a3902a5c28249161befdc5 [file] [log] [blame]
package org.bouncycastle.pkix.jcajce;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.CRL;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLSelector;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.jcajce.PKIXCRLStore;
import org.bouncycastle.jcajce.PKIXExtendedParameters;
import org.bouncycastle.jcajce.util.JcaJceHelper;
import org.bouncycastle.util.CollectionStore;
import org.bouncycastle.util.Iterable;
import org.bouncycastle.util.Selector;
import org.bouncycastle.util.Store;
/**
* X.509 Certificate Revocation Checker - still lacks OCSP support and support for delta CRLs.
*/
public class X509RevocationChecker
extends PKIXCertPathChecker
{
/**
* This is the default PKIX validity model. Actually there are two variants
* of this: The PKIX model and the modified PKIX model. The PKIX model
* verifies that all involved certificates must have been valid at the
* current time. The modified PKIX model verifies that all involved
* certificates were valid at the signing time. Both are indirectly choosen
* with the {@link PKIXParameters#setDate(Date)} method, so this
* methods sets the Date when <em>all</em> certificates must have been
* valid.
*/
public static final int PKIX_VALIDITY_MODEL = 0;
/**
* This model uses the following validity model. Each certificate must have
* been valid at the moment where is was used. That means the end
* certificate must have been valid at the time the signature was done. The
* CA certificate which signed the end certificate must have been valid,
* when the end certificate was signed. The CA (or Root CA) certificate must
* have been valid, when the CA certificate was signed and so on. So the
* {@link PKIXParameters#setDate(Date)} method sets the time, when
* the <em>end certificate</em> must have been valid. It is used e.g.
* in the German signature law.
*/
public static final int CHAIN_VALIDITY_MODEL = 1;
public static class Builder
{
private Set<TrustAnchor> trustAnchors;
private List<CertStore> crlCertStores = new ArrayList<CertStore>();
private List<Store<CRL>> crls = new ArrayList<Store<CRL>>();
private boolean isCheckEEOnly;
private int validityModel = PKIX_VALIDITY_MODEL;
private Provider provider;
private String providerName;
private boolean canSoftFail;
private long failLogMaxTime;
private long failHardMaxTime;
/**
* Base constructor.
*
* @param trustAnchor the trust anchor our chain should start with.
*/
public Builder(TrustAnchor trustAnchor)
{
this.trustAnchors = Collections.singleton(trustAnchor);
}
/**
* Base constructor.
*
* @param trustAnchors a set of potential trust anchors
*/
public Builder(Set<TrustAnchor> trustAnchors)
{
this.trustAnchors = new HashSet<TrustAnchor>(trustAnchors);
}
/**
* Base constructor.
*
* @param trustStore a keystore of potential trust anchors
*/
public Builder(KeyStore trustStore)
throws KeyStoreException
{
this.trustAnchors = new HashSet<TrustAnchor>();
for (Enumeration en = trustStore.aliases(); en.hasMoreElements(); )
{
String alias = (String)en.nextElement();
if (trustStore.isCertificateEntry(alias))
{
trustAnchors.add(new TrustAnchor((X509Certificate)trustStore.getCertificate(alias), null));
}
}
}
/**
* Add a collection of CRLs to the checker.
*
* @param crls CRLs to be examined.
* @return the current builder instance.
*/
public Builder addCrls(CertStore crls)
{
this.crlCertStores.add(crls);
return this;
}
/**
* Add a collection of CRLs to the checker.
*
* @param crls CRLs to be examined.
* @return the current builder instance.
*/
public Builder addCrls(Store<CRL> crls)
{
this.crls.add(crls);
return this;
}
/**
* @param isTrue true if only end-entities should be checked, false otherwise.
* @return the current builder instance.
*/
public Builder setCheckEndEntityOnly(boolean isTrue)
{
this.isCheckEEOnly = isTrue;
return this;
}
/**
* Configure soft failure if CRLs/OCSP not available. If maxTime is greater than zero
* it represents the acceptable downtime for any responders or distribution points we
* are trying to connect to, with downtime measured from the first failure. Initially
* failures will log at Level.WARNING, once maxTime is exceeded any failures will be
* logged as Level.SEVERE. Setting maxTime to zero will mean 1 failure will be allowed
* before failures are logged as severe.
*
* @param isTrue true soft failure should be enabled, false otherwise.
* @param maxTime the time that can pass between the first failure and the most recent.
* @return the current builder instance.
*/
public Builder setSoftFail(boolean isTrue, long maxTime)
{
this.canSoftFail = isTrue;
this.failLogMaxTime = maxTime;
this.failHardMaxTime = -1;
return this;
}
/**
* Configure soft failure with a hard limit if CRLs/OCSP not available. If maxTime is
* greater than zero it represents the acceptable downtime for any responders or
* distribution points we are trying to connect to, with downtime measured from the
* first failure. Initially failures will log at Level.WARNING, once 75% of maxTime is exceeded
* any failures will be logged as Level.SEVERE. At maxTime any failures will be treated as hard,
* setting maxTime to zero will mean 1 failure will be allowed.
*
* @param isTrue true soft failure should be enabled, false otherwise.
* @param maxTime the time that can pass between the first failure and the most recent.
* @return the current builder instance.
*/
public Builder setSoftFailHardLimit(boolean isTrue, long maxTime)
{
this.canSoftFail = isTrue;
this.failLogMaxTime = (maxTime * 3) / 4;
this.failHardMaxTime = maxTime;
return this;
}
/**
* Configure to use the installed provider with name ProviderName.
*
* @param provider provider to use.
* @return the current builder instance.
*/
public Builder usingProvider(Provider provider)
{
this.provider = provider;
return this;
}
/**
* Configure to use the installed provider with name ProviderName.
*
* @param providerName name of the installed provider to use.
* @return the current builder instance.
*/
public Builder usingProvider(String providerName)
{
this.providerName = providerName;
return this;
}
/**
* Build a revocation checker conforming to the current builder.
*
* @return a new X509RevocationChecker.
*/
public X509RevocationChecker build()
{
return new X509RevocationChecker(this);
}
}
private static Logger LOG = Logger.getLogger(X509RevocationChecker.class.getName());
private static final Map<GeneralName, WeakReference<X509CRL>> crlCache = Collections.synchronizedMap(
new WeakHashMap<GeneralName, WeakReference<X509CRL>>());
private final Map<X500Principal, Long> failures = new HashMap<X500Principal, Long>();
private final Set<TrustAnchor> trustAnchors;
private final boolean isCheckEEOnly;
private final List<Store<CRL>> crls;
private final List<CertStore> crlCertStores;
private final PKIXJcaJceHelper helper;
private final boolean canSoftFail;
private final long failLogMaxTime;
private final long failHardMaxTime;
private X500Principal workingIssuerName;
private PublicKey workingPublicKey;
private X509Certificate signingCert;
private X509RevocationChecker(Builder bldr)
{
this.crls = new ArrayList<Store<CRL>>(bldr.crls);
this.crlCertStores = new ArrayList<CertStore>(bldr.crlCertStores);
this.isCheckEEOnly = bldr.isCheckEEOnly;
this.trustAnchors = bldr.trustAnchors;
this.canSoftFail = bldr.canSoftFail;
this.failLogMaxTime = bldr.failLogMaxTime;
this.failHardMaxTime = bldr.failHardMaxTime;
if (bldr.provider != null)
{
this.helper = new PKIXProviderJcaJceHelper(bldr.provider);
}
else if (bldr.providerName != null)
{
helper = new PKIXNamedJcaJceHelper(bldr.providerName);
}
else
{
helper = new PKIXDefaultJcaJceHelper();
}
}
public void init(boolean forward)
throws CertPathValidatorException
{
if (forward)
{
throw new IllegalArgumentException("forward processing not supported");
}
this.workingIssuerName = null;
}
public boolean isForwardCheckingSupported()
{
return false;
}
public Set<String> getSupportedExtensions()
{
return null;
}
public void check(Certificate certificate, Collection<String> collection)
throws CertPathValidatorException
{
X509Certificate cert = (X509Certificate)certificate;
if (isCheckEEOnly && cert.getBasicConstraints() != -1)
{
this.workingIssuerName = cert.getSubjectX500Principal();
this.workingPublicKey = cert.getPublicKey();
this.signingCert = cert;
return;
}
TrustAnchor trustAnchor = null;
if (workingIssuerName == null)
{
this.workingIssuerName = cert.getIssuerX500Principal();
for (Iterator it = trustAnchors.iterator(); it.hasNext(); )
{
TrustAnchor anchor = (TrustAnchor)it.next();
if (workingIssuerName.equals(anchor.getCA())
|| workingIssuerName.equals(anchor.getTrustedCert().getSubjectX500Principal()))
{
trustAnchor = anchor;
}
}
if (trustAnchor == null)
{
throw new CertPathValidatorException("no trust anchor found for " + workingIssuerName);
}
this.signingCert = trustAnchor.getTrustedCert();
this.workingPublicKey = signingCert.getPublicKey();
}
PKIXParameters baseParams;
List<X500Principal> issuerList = new ArrayList<X500Principal>();
try
{
baseParams = new PKIXParameters(trustAnchors);
baseParams.setRevocationEnabled(false);
baseParams.setDate(new Date());
for (int i = 0; i != crlCertStores.size(); i++)
{
if (LOG.isLoggable(Level.INFO))
{
addIssuers(issuerList, crlCertStores.get(i));
}
baseParams.addCertStore(crlCertStores.get(i));
}
}
catch (GeneralSecurityException e)
{
throw new RuntimeException("error setting up baseParams: " + e.getMessage());
}
PKIXExtendedParameters.Builder pkixParamsBldr = new PKIXExtendedParameters.Builder(baseParams);
for (int i = 0; i != crls.size(); i++)
{
if (LOG.isLoggable(Level.INFO))
{
addIssuers(issuerList, crls.get(i));
}
pkixParamsBldr.addCRLStore(new LocalCRLStore(crls.get(i)));
}
if (issuerList.isEmpty())
{
LOG.log(Level.INFO, "configured with 0 pre-loaded CRLs");
}
else
{
if (LOG.isLoggable(Level.FINE))
{
for (int i = 0; i != issuerList.size(); i++)
{
LOG.log(Level.FINE, "configuring with CRL for issuer \"" + issuerList.get(i) + "\"");
}
}
else
{
LOG.log(Level.INFO, "configured with " + issuerList.size() + " pre-loaded CRLs");
}
}
try
{
checkCRLs(pkixParamsBldr.build(), cert, baseParams.getDate(), signingCert, workingPublicKey, new ArrayList(), helper);
}
catch (AnnotatedException e)
{
throw new CertPathValidatorException(e.getMessage(), e.getCause());
}
catch (CRLNotFoundException e)
{
if (cert.getExtensionValue(Extension.cRLDistributionPoints.getId()) != null)
{
CRL crl = null;
try
{
crl = downloadCRLs(cert.getIssuerX500Principal(), baseParams.getDate(), RevocationUtilities.getExtensionValue(cert, Extension.cRLDistributionPoints), helper);
}
catch(AnnotatedException e1)
{
throw new CertPathValidatorException(e.getMessage(), e.getCause());
}
if (crl != null)
{
try
{
pkixParamsBldr.addCRLStore(new LocalCRLStore(
new CollectionStore<CRL>(Collections.singleton(crl))));
checkCRLs(pkixParamsBldr.build(), cert, new Date(), signingCert, workingPublicKey, new ArrayList(), helper);
}
catch(AnnotatedException e1)
{
throw new CertPathValidatorException(e.getMessage(), e.getCause());
}
}
else
{
if (canSoftFail)
{
X500Principal issuer = cert.getIssuerX500Principal();
Long initial = failures.get(issuer);
if (initial != null)
{
long period = System.currentTimeMillis() - initial.longValue();
if (failHardMaxTime != -1 && failHardMaxTime < period)
{
throw e;
}
if (period < failLogMaxTime)
{
LOG.log(Level.WARNING, "soft failing for issuer: \"" + issuer + "\"");
}
else
{
LOG.log(Level.SEVERE, "soft failing for issuer: \"" + issuer + "\"");
}
}
else
{
failures.put(issuer, System.currentTimeMillis());
}
}
else
{
throw e;
}
}
}
else
{
throw e;
}
}
this.signingCert = cert;
this.workingPublicKey = cert.getPublicKey();
this.workingIssuerName = cert.getSubjectX500Principal();
}
private void addIssuers(final List<X500Principal> issuerList, CertStore certStore)
throws CertStoreException
{
certStore.getCRLs(new X509CRLSelector()
{
public boolean match(CRL crl)
{
if (!(crl instanceof X509CRL))
{
return false;
}
issuerList.add(((X509CRL)crl).getIssuerX500Principal());
return false;
}
});
}
private void addIssuers(final List<X500Principal> issuerList, Store<CRL> certStore)
{
certStore.getMatches(new Selector<CRL>()
{
public boolean match(CRL crl)
{
if (!(crl instanceof X509CRL))
{
return false;
}
issuerList.add(((X509CRL)crl).getIssuerX500Principal());
return false;
}
public Object clone()
{
return this;
}
});
}
private CRL downloadCRLs(X500Principal issuer, Date currentDate, ASN1Primitive crlDpPrimitive, JcaJceHelper helper)
{
CRLDistPoint crlDp = CRLDistPoint.getInstance(crlDpPrimitive);
DistributionPoint[] points = crlDp.getDistributionPoints();
for (int i = 0; i != points.length; i++)
{
DistributionPoint dp = points[i];
DistributionPointName dpn = dp.getDistributionPoint();
if (dpn.getType() == DistributionPointName.FULL_NAME)
{
GeneralName[] names = GeneralNames.getInstance(dpn.getName()).getNames();
for (int n = 0; n != names.length; n++)
{
GeneralName name = names[n];
if (name.getTagNo() == GeneralName.uniformResourceIdentifier)
{
X509CRL crl;
WeakReference<X509CRL> crlRef = crlCache.get(name);
if (crlRef != null)
{
crl = crlRef.get();
if (crl != null
&& !currentDate.before(crl.getThisUpdate())
&& !currentDate.after(crl.getNextUpdate()))
{
return crl;
}
crlCache.remove(name); // delete expired/out-of-range entry
}
URL url = null;
try
{
url = new URL(name.getName().toString());
CertificateFactory certFact = helper.createCertificateFactory("X.509");
InputStream urlStream = url.openStream();
crl = (X509CRL)certFact.generateCRL(new BufferedInputStream(urlStream));
urlStream.close();
LOG.log(Level.INFO, "downloaded CRL from CrlDP " + url + " for issuer \"" + issuer + "\"");
crlCache.put(name, new WeakReference<X509CRL>(crl));
return crl;
}
catch (Exception e)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "CrlDP " + url + " ignored: " + e.getMessage(), e);
}
else
{
LOG.log(Level.INFO, "CrlDP " + url + " ignored: " + e.getMessage());
}
}
}
}
}
}
return null;
}
protected static final String[] crlReasons = new String[]{
"unspecified",
"keyCompromise",
"cACompromise",
"affiliationChanged",
"superseded",
"cessationOfOperation",
"certificateHold",
"unknown",
"removeFromCRL",
"privilegeWithdrawn",
"aACompromise"};
static List<PKIXCRLStore> getAdditionalStoresFromCRLDistributionPoint(CRLDistPoint crldp, Map<GeneralName, PKIXCRLStore> namedCRLStoreMap)
throws AnnotatedException
{
if (crldp != null)
{
DistributionPoint dps[] = null;
try
{
dps = crldp.getDistributionPoints();
}
catch (Exception e)
{
throw new AnnotatedException(
"could not read distribution points could not be read", e);
}
List<PKIXCRLStore> stores = new ArrayList<PKIXCRLStore>();
for (int i = 0; i < dps.length; i++)
{
DistributionPointName dpn = dps[i].getDistributionPoint();
// look for URIs in fullName
if (dpn != null)
{
if (dpn.getType() == DistributionPointName.FULL_NAME)
{
GeneralName[] genNames = GeneralNames.getInstance(
dpn.getName()).getNames();
for (int j = 0; j < genNames.length; j++)
{
PKIXCRLStore store = namedCRLStoreMap.get(genNames[j]);
if (store != null)
{
stores.add(store);
}
}
}
}
}
return stores;
}
else
{
return Collections.EMPTY_LIST;
}
}
/**
* Checks a certificate if it is revoked.
*
* @param paramsPKIX PKIX parameters.
* @param cert Certificate to check if it is revoked.
* @param validDate The date when the certificate revocation status should be
* checked.
* @param sign The issuer certificate of the certificate <code>cert</code>.
* @param workingPublicKey The public key of the issuer certificate <code>sign</code>.
* @param certPathCerts The certificates of the certification path.
* @throws AnnotatedException if the certificate is revoked or the status cannot be checked
* or some error occurs.
*/
protected void checkCRLs(
PKIXExtendedParameters paramsPKIX,
X509Certificate cert,
Date validDate,
X509Certificate sign,
PublicKey workingPublicKey,
List certPathCerts,
PKIXJcaJceHelper helper)
throws AnnotatedException, CertPathValidatorException
{
AnnotatedException lastException = null;
CRLDistPoint crldp = null;
try
{
crldp = CRLDistPoint.getInstance(RevocationUtilities.getExtensionValue(cert, Extension.cRLDistributionPoints));
}
catch (Exception e)
{
throw new AnnotatedException("cannot read CRL distribution point extension", e);
}
PKIXExtendedParameters.Builder paramsBldr = new PKIXExtendedParameters.Builder(paramsPKIX);
try
{
List extras = getAdditionalStoresFromCRLDistributionPoint(crldp, paramsPKIX.getNamedCRLStoreMap());
for (Iterator it = extras.iterator(); it.hasNext(); )
{
paramsBldr.addCRLStore((PKIXCRLStore)it.next());
}
}
catch (AnnotatedException e)
{
throw new AnnotatedException(
"no additional CRL locations could be decoded from CRL distribution point extension", e);
}
CertStatus certStatus = new CertStatus();
ReasonsMask reasonsMask = new ReasonsMask();
PKIXExtendedParameters finalParams = paramsBldr.build();
boolean validCrlFound = false;
// for each distribution point
if (crldp != null)
{
DistributionPoint dps[] = null;
try
{
dps = crldp.getDistributionPoints();
}
catch (Exception e)
{
throw new AnnotatedException("cannot read distribution points", e);
}
if (dps != null)
{
for (int i = 0; i < dps.length && certStatus.getCertStatus() == CertStatus.UNREVOKED && !reasonsMask.isAllReasons(); i++)
{
try
{
RFC3280CertPathUtilities.checkCRL(dps[i], finalParams, cert, validDate, sign, workingPublicKey, certStatus, reasonsMask, certPathCerts, helper);
validCrlFound = true;
}
catch (AnnotatedException e)
{
lastException = e;
}
}
}
}
/*
* If the revocation status has not been determined, repeat the process
* above with any available CRLs not specified in a distribution point
* but issued by the certificate issuer.
*/
if (certStatus.getCertStatus() == CertStatus.UNREVOKED && !reasonsMask.isAllReasons())
{
try
{
/*
* assume a DP with both the reasons and the cRLIssuer fields
* omitted and a distribution point name of the certificate
* issuer.
*/
X500Principal issuer = cert.getIssuerX500Principal();
DistributionPoint dp = new DistributionPoint(new DistributionPointName(0, new GeneralNames(
new GeneralName(GeneralName.directoryName, X500Name.getInstance(issuer.getEncoded())))), null, null);
PKIXExtendedParameters paramsPKIXClone = (PKIXExtendedParameters)paramsPKIX.clone();
RFC3280CertPathUtilities.checkCRL(dp, paramsPKIXClone, cert, validDate, sign, workingPublicKey, certStatus, reasonsMask,
certPathCerts, helper);
validCrlFound = true;
}
catch (AnnotatedException e)
{
lastException = e;
}
}
if (!validCrlFound)
{
if (lastException instanceof AnnotatedException)
{
throw new CRLNotFoundException("no valid CRL found", lastException);
}
throw new CRLNotFoundException("no valid CRL found");
}
if (certStatus.getCertStatus() != CertStatus.UNREVOKED)
{
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
String message = "certificate [issuer=\"" + cert.getIssuerX500Principal() + "\",serialNumber="
+ cert.getSerialNumber() + ",subject=\"" + cert.getSubjectX500Principal() + "\"] revoked after " + df.format(certStatus.getRevocationDate());
message += ", reason: " + crlReasons[certStatus.getCertStatus()];
throw new AnnotatedException(message);
}
if (!reasonsMask.isAllReasons() && certStatus.getCertStatus() == CertStatus.UNREVOKED)
{
certStatus.setCertStatus(CertStatus.UNDETERMINED);
}
if (certStatus.getCertStatus() == CertStatus.UNDETERMINED)
{
throw new AnnotatedException("certificate status could not be determined");
}
}
public Object clone()
{
return this;
}
private class LocalCRLStore<T extends CRL>
implements PKIXCRLStore, Iterable<CRL>
{
private Collection<CRL> _local;
/**
* Basic constructor.
*
* @param collection - initial contents for the store, this is copied.
*/
public LocalCRLStore(
Store<CRL> collection)
{
_local = new ArrayList<CRL>(collection.getMatches(null));
}
/**
* Return the matches in the collection for the passed in selector.
*
* @param selector the selector to match against.
* @return a possibly empty collection of matching objects.
*/
public Collection getMatches(Selector selector)
{
if (selector == null)
{
return new ArrayList<CRL>(_local);
}
else
{
List<CRL> col = new ArrayList<CRL>();
Iterator<CRL> iter = _local.iterator();
while (iter.hasNext())
{
CRL obj = iter.next();
if (selector.match(obj))
{
col.add(obj);
}
}
return col;
}
}
public Iterator<CRL> iterator()
{
return getMatches(null).iterator();
}
}
}