7

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:

  1. Create certificate in 2015, valid 2015-2020
  2. Revoke the certificate with a CRL in 2017, key was stolen, only create CRL for 1 year because I make a mistake or plan on rolling over and never get around to it
  3. Check the certificate in 2019, the CRL is expired, bouncy castle accepts the revoked certificate again - which is obviously not what I want

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)
    ...
Markus
  • 295
  • 4
  • 12
  • 1
    This is no expiration date on a CRL. The exception doesn't give any hint of that either. – President James K. Polk May 18 '18 at 12:36
  • 3
    I know the date is technically called the "next update", but in practice it seems to be used as expiration date. If I change the next update to a date in the future the CRL works without a problem. – Markus May 19 '18 at 14:25
  • You can always ask on the bouncy castle dev mailing list as well. And you would get 100 points from parroting any answer you may get from it here. Do include source if possible. – Maarten Bodewes May 20 '18 at 16:29
  • How are you faring with this? Did you get an answer using other sources? The bounty doesn't seem to attract that much attention... 52 views but nothing remotely like an answer.... – Maarten Bodewes May 23 '18 at 22:42
  • No, unfortunately not. I asked on the mailing list but with no answer. I ended up deactivating CRL checking and am now checking myself. Should I add the code as an answer? Basically that is the checkRevocation() in my question above. One detail I got wrong in the initial question - bouncy does not accept the revoked certificate, the actual problem is that it no longer accepts a non-revoked valid certificate, as it finds no CRL. – Markus May 25 '18 at 11:47
  • Sure you can put the code into an answer, I'll gladly do a quick review and then assign the bounty. Good idea to copy the code to create the alternate revocation check. – Maarten Bodewes May 25 '18 at 23:19
  • Have you tried simply setting the date to something you know is going to be valid against the CRL, like `buildParams.setDate(new Date(0));` ? – mnistic May 26 '18 at 23:29
  • Yes, setting the date to `null` causes the current date to be used, setting it to `new Date(0)` will set the valid date to 1. January 1970. Will be adding the code I'm using in a minute below as my answer. – Markus May 28 '18 at 13:16

2 Answers2

3

Basically my whole problem happens in the method PKIXCRLUtil#findCRLsin 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;
}
Markus
  • 295
  • 4
  • 12
1

Disable CRL checks entirely. No, I'm not kidding. The general consensus in the security community is that revocation is broken. Modern browsers don't even bother checking CRLs. If you could enable strict CRL checking in your browser (many won't even let you anymore), then you'd find much of the web to be inaccessible.

Zenexer
  • 18,788
  • 9
  • 71
  • 77
  • I'd much rather be doing that, but unfortunately won't be allowed to do that - company reasons. – Markus May 28 '18 at 13:17
  • That's an interesting post, which is why I upvoted your answer. However, as it doesn't *directly* answer the question I decided not to assign it the bounty.; CRL checking might just be required. OTOH, I don't see any reason to downvote it. – Maarten Bodewes May 28 '18 at 17:35