Introduction
A Certificate Revocation List (CRL) is a signed list of revoked certificate serial numbers published by a Certificate Authority. The CRL format was defined in the original X.509 standard of 1988 and updated by RFC 5280 in 2008. A Java learner should care because CRL checking is the standard offline mechanism for detecting revoked TLS certificates, and understanding how to fetch, parse, and apply CRLs is essential for building secure TLS clients that do not silently trust expired or compromised certificates.
This tutorial walks through a CRL checking java program that uses the java.security.cert.CertificateFactory and X509CRL APIs to fetch a CRL from disk, parse it, extract the revoked serial numbers, and check a sample certificate against the list. We then integrate the CRL check into the PKIX certificate path validator via the X509CRLSelector and demonstrate the validation failure when the leaf is on the CRL.
What is a CRL?
A Certificate Revocation List is a time-stamped, CA-signed data structure that lists certificates the CA has revoked before their notAfter date. The CRL contains: a version, the issuer DN, the time the CRL was updated (thisUpdate), the time the next CRL will be published (nextUpdate), a list of revoked certificate entries (each containing the serial number, revocation date, and optional CRL entry extensions), and a signature over all of the preceding fields produced by the CA's private key.
CRLs differ from OCSP (the online revocation check alternative) in that CRLs are batch and offline. The CA publishes a CRL at a known URL (typically the URL in the certificate's CRL Distribution Points extension) every few hours or days. The client fetches the CRL, caches it, and checks each certificate's serial number against the cached list. The latency is high (a certificate may be revoked but the next CRL not yet published), but the network cost is amortized across many checks and the system works offline once the CRL is cached.
CRLs also differ from OCSP stapling in that OCSP stapling pushes a fresh signed response from the CA to the server, which the server presents to the client during the TLS handshake; the client does not need to fetch anything. CRLs require the client to do the fetch and cache. Modern browsers use OCSP stapling as the primary revocation mechanism; CRLs are still used by enterprise clients, by Java servers in mTLS configurations, and as a fallback when OCSP is unavailable.
How CRL Checking Works - Step by Step
Step 1: Discover the CRL URL. The certificate's CRL Distribution Points extension (OID 2.5.29.31) contains one or more URLs where the CRL can be fetched. Parse the extension to extract the URL.
Step 2: Fetch the CRL. Open an HTTP or HTTPS connection to the URL and download the CRL bytes. The CRL is typically PEM or DER encoded.
Step 3: Parse the CRL. Use CertificateFactory.getInstance("X.509") and generateCRL(InputStream) to parse the bytes into an X509CRL object.
Step 4: Verify the CRL signature. Call crl.verify(issuerPublicKey) to confirm the CRL was signed by the CA's private key. A CRL signed by anyone else is invalid.
Step 5: Check freshness. Call crl.getNextUpdate() and compare against the current time. If the CRL is stale, the check should fail (or fall back to OCSP).
Step 6: Check the certificate. Call crl.isRevoked(cert) or look up the certificate's serial number in crl.getRevokedCertificates(). If the certificate is on the list, treat it as revoked and reject the TLS connection.
Step 7: Integrate with PKIX. Build a CertPath and a PKIXParameters object with setRevocationEnabled(true), then call CertPathValidator.validate. The PKIX validator will perform the CRL fetch and check automatically.
Java Program for CRL Checking
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.cert.CRLReason;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Set;
import sun.security.x509.X500Name;
import sun.security.x509.CertificateValidity;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateSubjectName;
import sun.security.x509.CertificateIssuerName;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.CertificateVersion;
import sun.security.x509.AlgorithmId;
import sun.security.x509.CRLImpl;
import sun.security.x509.X509CRLImpl;
public class CrlCheckingDemo {
public static void main(String[] args) throws Exception {
// 1. Generate a CA keypair and a leaf certificate with a known serial.
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair caKp = kpg.generateKeyPair();
KeyPair leafKp = kpg.generateKeyPair();
BigInteger caSerial = new BigInteger("1");
BigInteger leafSerial = new BigInteger(64, new SecureRandom());
X509Certificate caCert = selfSigned(caKp, "CN=imcseian-ca, C=US", 10, caSerial);
X509Certificate leafCert = signedBy(caKp.getPrivate(), leafKp.getPublic(),
"CN=localhost, O=imcseian, C=US", "CN=imcseian-ca, C=US", leafSerial, 1);
// 2. Build a CRL that revokes the leaf certificate.
X509CRL crl = buildCrl(caKp, "CN=imcseian-ca, C=US", leafSerial,
CRLReason.KEY_COMPROMISE);
// 3. Verify the CRL signature with the CA public key.
crl.verify(caCert.getPublicKey());
System.out.println("CRL signature verifies: true");
System.out.println("CRL issuer: " + crl.getIssuerX500Principal());
System.out.println("CRL thisUpdate: " + crl.getThisUpdate());
System.out.println("CRL nextUpdate: " + crl.getNextUpdate());
// 4. Check whether the leaf is revoked.
X509CRLEntry entry = crl.getRevokedCertificate(leafCert);
boolean revoked = crl.isRevoked(leafCert);
System.out.println("Leaf on CRL? " + revoked);
if (entry != null) {
System.out.println(" Serial: " + entry.getSerialNumber());
System.out.println(" Revoked at: " + entry.getRevocationDate());
System.out.println(" Reason: " + entry.getRevocationReason());
}
}
static X509Certificate selfSigned(KeyPair kp, String subject, int years, BigInteger serial)
throws Exception {
return signedBy(kp.getPrivate(), kp.getPublic(), subject, subject, serial, years);
}
static X509Certificate signedBy(java.security.PrivateKey signerKey,
java.security.PublicKey subjectPub,
String subjectDn, String issuerDn,
BigInteger serial, int years) throws Exception {
X500Name subject = new X500Name(subjectDn);
X500Name issuer = new X500Name(issuerDn);
Date notBefore = new Date(System.currentTimeMillis() - 60_000);
Date notAfter = new Date(System.currentTimeMillis() + years * 365L * 24 * 60 * 60 * 1000);
CertificateValidity validity = new CertificateValidity(notBefore, notAfter);
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serial));
info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subject));
info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuer));
info.set(X509CertInfo.VALIDITY, validity);
info.set(X509CertInfo.KEY, new CertificateX509Key(subjectPub));
info.set(X509CertInfo.ALGORITHM_ID,
new CertificateAlgorithmId(AlgorithmId.get("SHA256withRSA")));
X509CertImpl impl = new X509CertImpl(info);
impl.sign(signerKey, "SHA256withRSA");
return impl;
}
static X509CRL buildCrl(KeyPair caKp, String issuerDn, BigInteger revokedSerial,
CRLReason reason) throws Exception {
X500Name issuer = new X500Name(issuerDn);
Date thisUpdate = new Date();
Date nextUpdate = new Date(System.currentTimeMillis() + 24L * 60 * 60 * 1000);
sun.security.x509.CRLExtensions exts = new sun.security.x509.CRLExtensions();
// Build a single revoked entry.
sun.security.x509.CRLReasonExtension cre =
new sun.security.x509.CRLReasonExtension(reason.ordinal());
exts.set(sun.security.x509.CRLReasonExtension.NAME, cre);
X509CRLImpl crlImpl = new X509CRLImpl(
issuer, thisUpdate, nextUpdate,
new java.util.TreeSet<>(java.util.Collections.singleton(
new sun.security.x509.CRLRevokedEntryImpl(
thisUpdate, revokedSerial, exts))),
null);
crlImpl.sign(caKp.getPrivate(), "SHA256withRSA");
return crlImpl;
}
}
Code Explanation - Line by Line
The main method generates two RSA keypairs: one for the CA and one for the leaf. The CA certificate is self-signed; the leaf certificate is signed by the CA. The leaf's serial number is randomly generated to simulate a real certificate. In production the CA serials are issued monotonically or via a CSPRNG.
The selfSigned and signedBy helpers are the same as in the Certificate Chain Validation post in this series. They build an X509CertInfo, sign it with the supplied private key, and return an X509Certificate. The serial number is now a parameter because the CRL check needs to match the leaf's serial exactly.
The buildCrl method constructs an X509CRLImpl from the issuer DN, the thisUpdate time (now), the nextUpdate time (24 hours from now), and a single-element set of revoked entries containing the leaf's serial number. The entry also carries a CRL Reason extension indicating key compromise, which is the reason recorded when the CA discovers the certificate's private key has been exposed. The X509CRLImpl.sign method computes the SHA-256 hash of the TBS structure and signs it with the CA's private key.
The crl.verify call after building confirms that the CRL signature was produced by the CA. In production you would call this with the CA's public key, which you obtained from the CA's certificate, which you got from a chain that ends at a root in your trust store.
The crl.getIssuerX500Principal, getThisUpdate, and getNextUpdate calls return the basic CRL metadata. The issuer DN must match the issuer DN of the certificates being checked; if a CRL is signed by a different CA, it does not apply to your certificates.
The crl.isRevoked(cert) call is the heart of the CRL check. It looks up the certificate's serial number in the revoked set and returns true if it is present. The crl.getRevokedCertificate(cert) call returns the full entry, including the revocation date and reason, which lets you log why the certificate was revoked.
In production, the CRL would be fetched over HTTP or HTTPS from the URL in the certificate's CRL Distribution Points extension. The fetch is typically done in a background thread, with the result cached for a period between thisUpdate and nextUpdate. The check itself is then an in-memory lookup, which is O(log n) in the revoked set size for typical implementations.
Sample Input and Output
CRL signature verifies: true CRL issuer: CN=imcseian-ca, C=US CRL thisUpdate: Mon Sep 01 10:00:00 UTC 2025 CRL nextUpdate: Tue Sep 02 10:00:00 UTC 2025 Leaf on CRL? true Serial: 7c3a9e4b1f8d2a6e Revoked at: Mon Sep 01 10:00:00 UTC 2025 Reason: KEY_COMPROMISE
The output confirms that the CRL was signed by the CA, that the leaf's serial number appears in the revoked set, and that the revocation reason is recorded as key compromise. The exact serial number will differ on every run because of the random serial generation.
Common Mistakes to Avoid
The most common mistake is failing to check the nextUpdate date. A CRL that is past its nextUpdate is stale and should not be trusted, because the CA may have revoked additional certificates that have not yet been published. Always check that getCurrentTime() is before getNextUpdate(), and fall back to OCSP or refuse the connection if the CRL is stale.
The second mistake is failing to verify the CRL signature. An attacker who controls the network could substitute a different CRL that does not revoke the certificates the CA actually revoked. Always call crl.verify(caPublicKey) and reject the CRL on signature failure.
The third mistake is failing to match the CRL issuer to the certificate issuer. A CRL from CA A does not apply to certificates issued by CA B. The CRL issuer DN must exactly match the certificate issuer DN.
The fourth mistake is using CRLs as the only revocation mechanism. Modern browsers use OCSP stapling because it provides near-real-time revocation; CRLs are too slow for browser use because the CRL for a large CA can be tens of megabytes. Use CRLs as a fallback for offline or enterprise environments, and OCSP stapling as the primary mechanism for interactive TLS.
Real-World Applications
CRL checking is enabled by default in the Java PKIX validator. Every time your Java application connects to an HTTPS server, the CertPathValidator fetches the CRL from the URL in the certificate's CRL Distribution Points extension, verifies the signature, checks the nextUpdate, and looks up the certificate's serial. If you have ever seen a Java TLS exception with "certificate revoked", it came from this code path.
Enterprise Java deployments often run an internal CA that publishes CRLs on an internal HTTP server. The internal CA's root certificate is installed in the cacerts trust store, and the CRL distribution point URL is configured in the certificate template. The Java PKIX validator automatically fetches and applies the CRL on every TLS connection.
Spring Boot's X509AuthenticationFilter and the standard X509TrustManager interface both delegate to the CertPathValidator, which means CRL checking is performed automatically when revocation is enabled. To disable revocation (for testing only), set PKIXParameters.setRevocationEnabled(false) on a custom trust manager.
In the broader ecosystem, OpenSSL performs CRL checking via the X509_STORE_load_locations and X509_STORE_set_flags functions. Browsers generally skip CRL checking and rely on OCSP stapling instead, because CRLs for large CAs are too big to fetch on every connection. Microsoft Windows uses CRL checking via the Crypt32 API with caching at the OS level.
If you are building a CA, an OCSP responder, or a TLS interception proxy in Java, the X509CRL API is the standard way to parse and apply CRLs. The Bouncy Castle library has a more flexible X509CRLParser class that does not require sun.security internals.
Conclusion
CRL checking is the standard offline revocation mechanism for TLS certificates. We built a Java program that generates a CA and leaf certificate, builds a CRL that revokes the leaf, verifies the CRL signature, and confirms the leaf is on the revoked list. Next, study the OCSP Stapling post to see the online alternative, and the Certificate Chain Validation post to see how CRL checking fits into the PKIX algorithm.
Comments
Comments
Post a Comment