Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accepting expired CRL with BouncyCastle

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)
    ...
like image 463
Markus Avatar asked May 18 '18 11:05

Markus


1 Answers

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;
}
like image 174
Markus Avatar answered Sep 30 '22 19:09

Markus