I'm validating X509 certificates offline with bouncy castle and have run into a problem with older CRLs. I haven't found a possibility yet to accept CRLs which are expired, in my opinion if a certificate was revoked, it should stay revoked after the expiry of the CRL. In addition if the CRL is empty I just want to accept this, I have no way of getting a newer CRL at this point.
Just to clarify, this would be the use case:
Currently I'm setting the revocation checking to false and performing the checks myself. I haven't found anything online about this anywhere.
This is my code:
final X509CertSelector endConstraints = new X509CertSelector();
endConstraints.setSerialNumber(signer.getSID().getSerialNumber());
final PKIXBuilderParameters buildParams = new PKIXBuilderParameters(trustAnchors, endConstraints);
//a CertStore object with Certificates and CRLs
buildParams.addCertStore(certificates);
//currently deactivated
buildParams.setRevocationEnabled(false);
final CertPathBuilder builder = CertPathBuilder.getInstance(SignedFileVerifier.CERTIFICATE_PATH_ALGORITHM, SignedFileVerifier.PROVIDER);
final CertPathBuilderResult result = builder.build(buildParams);
//here I manually check the CRLs, which I don't want to do
checkRevocation(result.getCertPath().getCertificates(), certificates, trustAnchors);
//if this passes I return the found certificate
return (X509Certificate) result.getCertPath().getCertificates().get(0);
The exact exception is:
Caused by: org.bouncycastle.jce.exception.ExtCertPathValidatorException: No CRLs found for issuer "cn=goodOldIssuerCA0,ou=jUnit Test Issuer,o=BOGO Company,c=AT"
at org.bouncycastle.jce.provider.RFC3280CertPathUtilities.processCertA(Unknown Source)
at org.bouncycastle.jce.provider.PKIXCertPathValidatorSpi.engineValidate(Unknown Source)
at org.bouncycastle.jce.provider.PKIXCertPathBuilderSpi.build(Unknown Source)
at org.bouncycastle.jce.provider.PKIXCertPathBuilderSpi.build(Unknown Source)
...
Basically my whole problem happens in the method PKIXCRLUtil#findCRLs
in the package org.bouncycastle.jce.provider
. This is the method is used to load the CRLs, and always checks the date here:
if (crl.getNextUpdate().after(validityDate))
{
X509Certificate cert = crlselect.getCertificateChecking();
if (cert != null)
{
if (crl.getThisUpdate().before(cert.getNotAfter()))
{
finalSet.add(crl);
}
}
else
{
finalSet.add(crl);
}
}
The code I ended up using is below. Basically I'm first combining all public keys into a map by their names (maybe serial number would be better?), then iterating over all certificates I have in the chain. First I get the certificate issuers public key, because I need it to validate the CRL came from the same issuer. Then I create a X509CRLSelector
issuer and load all CRLs by this issuer. Then I iterate over the CRLs I found in the store, verify them by the issuers public key, check if the certificate was revoked and throw an exception if this is the case. In my current implementation it would be OK if no CRL is found, this could be added by checking selectedCRLs
is not empty.
private void checkRevocation(final List<X509Certificate> certificates, final CertStore revocationLists, final Set<TrustAnchor> trustAnchors) throws GeneralSecurityException {
final Map<String, PublicKey> publicKeyMap = extractPublicKeys(certificates, trustAnchors);
//check the whole chain, we don't know if the issuer or the signer was revoked
for(final X509Certificate certificate : certificates){
final X500Principal issuerX500Principal = certificate.getIssuerX500Principal();
//get the issuer of this certificate
final PublicKey issuerPublicKey = publicKeyMap.get(issuerX500Principal.getName());
if(issuerPublicKey == null){
throw new GeneralSecurityException("Unable to find issuer for certificate '" + certificate.getSubjectX500Principal() + "'");
}
final X509CRLSelector crlSelector = new X509CRLSelector();
//we only use the issuer, not the date or time, don't want CRLs to expire
crlSelector.addIssuer(issuerX500Principal);
//get all CRLs that match this issuer
final Collection<? extends CRL> selectedCRLs = revocationLists.getCRLs(crlSelector);
for(final CRL crl : selectedCRLs){
final X509CRL x509CRL = (X509CRL)crl;
//check first if the crl is really published by the issuer
x509CRL.verify(issuerPublicKey);
//check if the current certificate was revoked
final X509CRLEntry revokedCertificate = x509CRL.getRevokedCertificate(certificate);
//if we found a revoked certificate throw an exception
if(revokedCertificate != null){
throw new GeneralSecurityException(String.format("Unable to use certificate '%1$s', revocation after %2$tF %2$tT, reason: %3$s",
certificate.getSubjectX500Principal(), revokedCertificate.getRevocationDate(), revokedCertificate.getRevocationReason()));
}
}
}
}
private Map<String, PublicKey> extractPublicKeys(final List<X509Certificate> certificates, final Set<TrustAnchor> trustAnchors) {
final Map<String, PublicKey> certificateMap = new HashMap<>();
for(final X509Certificate certificate : certificates){
certificateMap.put(certificate.getSubjectX500Principal().getName(), certificate.getPublicKey());
}
for(final TrustAnchor trustAnchor : trustAnchors){
final X509Certificate certificate = trustAnchor.getTrustedCert();
certificateMap.put(certificate.getSubjectX500Principal().getName(), certificate.getPublicKey());
}
return certificateMap;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With