Author: Svetlin Nakov
December 1, 2009
For one of my recent projects I needed to implement X.509 certificate validation library that validates a certificate across given set of trusted root certificated and a set of intermediate certificate. Initially I thought this is a problem that has already out-of-the-box solution in BouncyCastle but the CRL verification was found to be unpleasant to implement and not available out-of-the-box.
The task was formulated as follows: given a X.509 certificate and a set of trusted root certificates and a set of intermediate certificates to build a certification chain (if possible) and to extract the CRL distribution point from the certificate (if available) and to check whether the certificate is not revoked. It was required to support HTTP, HTPS, FTP and LDAP based distribution points.
I will not get into more details because I don’t heve too much time but I think the code is clear enough, so most developers will be able to read, understand and use it:
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Set;
/**
* Class for building a certification chain for given certificate and verifying
* it. Relies on a set of root CA certificates and intermediate certificates
* that will be used for building the certification chain. The verification
* process assumes that all self-signed certificates in the set are trusted
* root CA certificates and all other certificates in the set are intermediate
* certificates.
*
* @author Svetlin Nakov
*/
public class CertificateVerifier {
/**
* Attempts to build a certification chain for given certificate and to verify
* it. Relies on a set of root CA certificates and intermediate certificates
* that will be used for building the certification chain. The verification
* process assumes that all self-signed certificates in the set are trusted
* root CA certificates and all other certificates in the set are intermediate
* certificates.
*
* @param cert - certificate for validation
* @param additionalCerts - set of trusted root CA certificates that will be
* used as "trust anchors" and intermediate CA certificates that will be
* used as part of the certification chain. All self-signed certificates
* are considered to be trusted root CA certificates. All the rest are
* considered to be intermediate CA certificates.
* @return the certification chain (if verification is successful)
* @throws CertificateVerificationException - if the certification is not
* successful (e.g. certification path cannot be built or some
* certificate in the chain is expired or CRL checks are failed)
*/
public static PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert,
Set<x509certificate> additionalCerts)
throws CertificateVerificationException {
try {
// Check for self-signed certificate
if (isSelfSigned(cert)) {
throw new CertificateVerificationException(
"The certificate is self-signed.");
}
// Prepare a set of trusted root CA certificates
// and a set of intermediate certificates
Set</x509certificate><x509certificate> trustedRootCerts = new HashSet</x509certificate><x509certificate>();
Set</x509certificate><x509certificate> intermediateCerts = new HashSet</x509certificate><x509certificate>();
for (X509Certificate additionalCert : additionalCerts) {
if (isSelfSigned(additionalCert)) {
trustedRootCerts.add(additionalCert);
} else {
intermediateCerts.add(additionalCert);
}
}
// Attempt to build the certification chain and verify it
PKIXCertPathBuilderResult verifiedCertChain =
verifyCertificate(cert, trustedRootCerts, intermediateCerts);
// Check whether the certificate is revoked by the CRL
// given in its CRL distribution point extension
CRLVerifier.verifyCertificateCRLs(cert);
// The chain is built and verified. Return it as a result
return verifiedCertChain;
} catch (CertPathBuilderException certPathEx) {
throw new CertificateVerificationException(
"Error building certification path: " +
cert.getSubjectX500Principal(), certPathEx);
} catch (CertificateVerificationException cvex) {
throw cvex;
} catch (Exception ex) {
throw new CertificateVerificationException(
"Error verifying the certificate: " +
cert.getSubjectX500Principal(), ex);
}
}
/**
* Checks whether given X.509 certificate is self-signed.
*/
public static boolean isSelfSigned(X509Certificate cert)
throws CertificateException, NoSuchAlgorithmException,
NoSuchProviderException {
try {
// Try to verify certificate signature with its own public key
PublicKey key = cert.getPublicKey();
cert.verify(key);
return true;
} catch (SignatureException sigEx) {
// Invalid signature --> not self-signed
return false;
} catch (InvalidKeyException keyEx) {
// Invalid key --> not self-signed
return false;
}
}
/**
* Attempts to build a certification chain for given certificate and to verify
* it. Relies on a set of root CA certificates (trust anchors) and a set of
* intermediate certificates (to be used as part of the chain).
* @param cert - certificate for validation
* @param trustedRootCerts - set of trusted root CA certificates
* @param intermediateCerts - set of intermediate certificates
* @return the certification chain (if verification is successful)
* @throws GeneralSecurityException - if the verification is not successful
* (e.g. certification path cannot be built or some certificate in the
* chain is expired)
*/
private static PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, Set</x509certificate><x509certificate> trustedRootCerts,
Set</x509certificate><x509certificate> intermediateCerts) throws GeneralSecurityException {
// Create the selector that specifies the starting certificate
X509CertSelector selector = new X509CertSelector();
selector.setCertificate(cert);
// Create the trust anchors (set of root CA certificates)
Set<trustanchor> trustAnchors = new HashSet</trustanchor><trustanchor>();
for (X509Certificate trustedRootCert : trustedRootCerts) {
trustAnchors.add(new TrustAnchor(trustedRootCert, null));
}
// Configure the PKIX certificate builder algorithm parameters
PKIXBuilderParameters pkixParams =
new PKIXBuilderParameters(trustAnchors, selector);
// Disable CRL checks (this is done manually as additional step)
pkixParams.setRevocationEnabled(false);
// Specify a list of intermediate certificates
CertStore intermediateCertStore = CertStore.getInstance("Collection",
new CollectionCertStoreParameters(intermediateCerts), "BC");
pkixParams.addCertStore(intermediateCertStore);
// Build and verify the certification chain
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC");
PKIXCertPathBuilderResult result =
(PKIXCertPathBuilderResult) builder.build(pkixParams);
return result;
}
}
------------------------
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509Extensions;
/**
* Class that verifies CRLs for given X509 certificate. Extracts the CRL
* distribution points from the certificate (if available) and checks the
* certificate revocation status against the CRLs coming from the
* distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
*
* @author Svetlin Nakov
*/
public class CRLVerifier {
/**
* Extracts the CRL distribution points from the certificate (if available)
* and checks the certificate revocation status against the CRLs coming from
* the distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
*
* @param cert the certificate to be checked for revocation
* @throws CertificateVerificationException if the certificate is revoked
*/
public static void verifyCertificateCRLs(X509Certificate cert)
throws CertificateVerificationException {
try {
List<string> crlDistPoints = getCrlDistributionPoints(cert);
for (String crlDP : crlDistPoints) {
X509CRL crl = downloadCRL(crlDP);
if (crl.isRevoked(cert)) {
throw new CertificateVerificationException(
"The certificate is revoked by CRL: " + crlDP);
}
}
} catch (Exception ex) {
if (ex instanceof CertificateVerificationException) {
throw (CertificateVerificationException) ex;
} else {
throw new CertificateVerificationException(
"Can not verify CRL for certificate: " +
cert.getSubjectX500Principal());
}
}
}
/**
* Downloads CRL from given URL. Supports http, https, ftp and ldap based URLs.
*/
private static X509CRL downloadCRL(String crlURL) throws IOException,
CertificateException, CRLException,
CertificateVerificationException, NamingException {
if (crlURL.startsWith("http://") || crlURL.startsWith("https://")
|| crlURL.startsWith("ftp://")) {
X509CRL crl = downloadCRLFromWeb(crlURL);
return crl;
} else if (crlURL.startsWith("ldap://")) {
X509CRL crl = downloadCRLFromLDAP(crlURL);
return crl;
} else {
throw new CertificateVerificationException(
"Can not download CRL from certificate " +
"distribution point: " + crlURL);
}
}
/**
* Downloads a CRL from given LDAP url, e.g.
* ldap://ldap.infonotary.com/dc=identity-ca,dc=infonotary,dc=com
*/
private static X509CRL downloadCRLFromLDAP(String ldapURL)
throws CertificateException, NamingException, CRLException,
CertificateVerificationException {
Hashtable</string><string , String> env = new Hashtable</string><string , String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, ldapURL);
DirContext ctx = new InitialDirContext(env);
Attributes avals = ctx.getAttributes("");
Attribute aval = avals.get("certificateRevocationList;binary");
byte[] val = (byte[])aval.get();
if ((val == null) || (val.length == 0)) {
throw new CertificateVerificationException(
"Can not download CRL from: " + ldapURL);
} else {
InputStream inStream = new ByteArrayInputStream(val);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509CRL crl = (X509CRL)cf.generateCRL(inStream);
return crl;
}
}
/**
* Downloads a CRL from given HTTP/HTTPS/FTP URL, e.g.
* http://crl.infonotary.com/crl/identity-ca.crl
*/
private static X509CRL downloadCRLFromWeb(String crlURL)
throws MalformedURLException, IOException, CertificateException,
CRLException {
URL url = new URL(crlURL);
InputStream crlStream = url.openStream();
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509CRL crl = (X509CRL) cf.generateCRL(crlStream);
return crl;
} finally {
crlStream.close();
}
}
/**
* Extracts all CRL distribution point URLs from the "CRL Distribution Point"
* extension in a X.509 certificate. If CRL distribution point extension is
* unavailable, returns an empty list.
*/
public static List</string><string> getCrlDistributionPoints(
X509Certificate cert) throws CertificateParsingException, IOException {
byte[] crldpExt = cert.getExtensionValue(
X509Extensions.CRLDistributionPoints.getId());
if (crldpExt == null) {
List</string><string> emptyList = new ArrayList</string><string>();
return emptyList;
}
ASN1InputStream oAsnInStream = new ASN1InputStream(
new ByteArrayInputStream(crldpExt));
DERObject derObjCrlDP = oAsnInStream.readObject();
DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP;
byte[] crldpExtOctets = dosCrlDP.getOctets();
ASN1InputStream oAsnInStream2 = new ASN1InputStream(
new ByteArrayInputStream(crldpExtOctets));
DERObject derObj2 = oAsnInStream2.readObject();
CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
List</string><string> crlUrls = new ArrayList</string><string>();
for (DistributionPoint dp : distPoint.getDistributionPoints()) {
DistributionPointName dpn = dp.getDistributionPoint();
// Look for URIs in fullName
if (dpn != null) {
if (dpn.getType() == DistributionPointName.FULL_NAME) {
GeneralName[] genNames = GeneralNames.getInstance(
dpn.getName()).getNames();
// Look for an URI
for (int j = 0; j < genNames.length; j++) {
if (genNames[j].getTagNo() == GeneralName.uniformResourceIdentifier) {
String url = DERIA5String.getInstance(
genNames[j].getName()).getString();
crlUrls.add(url);
}
}
}
}
}
return crlUrls;
}
}
------------------------
import java.security.cert.PKIXCertPathBuilderResult;
/**
* This class keeps the result from the certificate verification
* process. If the the certificate is verified as valid, the built
* certification chain is stored in the Result property. If the
* certificate is invalid, the problem is stored in the Exception
* property.
*
* @author Svetlin Nakov
*/
public class CertificateVerificationResult {
private boolean valid;
private PKIXCertPathBuilderResult result;
private Throwable exception;
/**
* Constructs a certificate verification result for valid
* certificate by given certification path.
*/
public CertificateVerificationResult(
PKIXCertPathBuilderResult result) {
this.valid = true;
this.result = result;
}
/**
* Constructs a certificate verification result for invalid
* certificate by given exception that keeps the problem
* occurred during the verification process.
*/
public CertificateVerificationResult(Throwable exception) {
this.valid = false;
this.exception = exception;
}
public boolean isValid() {
return valid;
}
public PKIXCertPathBuilderResult getResult() {
return result;
}
public Throwable getException() {
return exception;
}
}
------------------------
/**
* This class wraps an exception that could be thrown during
* the certificate verification process.
*
* @author Svetlin Nakov
*/
public class CertificateVerificationException extends Exception {
private static final long serialVersionUID = 1L;
public CertificateVerificationException(String message, Throwable cause) {
super(message, cause);
}
public CertificateVerificationException(String message) {
super(message);
}
}
I hope the above coude could be useful to anybody trying to build and validate X.509 certificate chain and check the CRL revocation status.
Tags: crl distribution point, crlURL, intermediate certificates, java security, org, return, root ca certificates, root certificates, security, set
Great example, well written and complete. Many thanks …the code answered many questions I had regarding CRL’s.
Comment by Rod — December 22, 2010 @ 17:20
Great Great Example. I second Rod’s comment. I have one question though. Do we need to add the certificate in question or the certificate to be validated in the intermediate cert list? I will really appreciate if you help me with this question.
Thank you so much anyway for this.
Regards,
Dpak
Comment by Dpak — March 17, 2011 @ 05:47
In the additional set of certificates you need to add the trusted Root CA certificates and all intermediate certificates. The certificate that need to be validated does not need to be in this set.
Regards, Svetlin
Comment by nakov — March 24, 2011 @ 00:35
Are you sure that the certificate that need to be validated does not need to be in set of intermediate certificates? If it is correct I have probably something wrong and I don’t understand what. Can you explain it to me please?
I set up this testing scenario:
RCA (CN=ca.org) created as root CA
ICA1 (CN=ica.org) created as intermediate CA signed by RCA
ICA2 (CN=ica2.org) created as intermediate CA signed by ICA1
UC (CN=user.org) created as server certificate signed ICA2
When I don’t add UC (CN=user.org) certificate to intermediate certificates set (aC) I get
CertificateVerificationException: Error building certification path: CN=user.org, O=Internet Widgits Pty Ltd, ST=Some-State, C=AU
When I add also user.org certificate to intermediate certificates set everything is ok.
I verified user.org with openssl and it is OK:
$ (cat rca.pem ; cat ica1.pem ; cat ica2.pem ) > ca.pem
$ openssl verify -CAfile ca.pem user.pem
user.pem: OK
Code is:
Set aC = new HashSet();
aC.add(RCA);
aC.add(ICA1);
aC.add(ICA2);
PKIXCertPathBuilderResult ver = CertificateVerifier.verifyCertificate(UC, aC);
/* ^ here I get CertificateVerificationException */
vs.
Set aC = new HashSet();
aC.add(RCA);
aC.add(ICA1);
aC.add(ICA2);
aC.add(UC);
PKIXCertPathBuilderResult ver = CertificateVerifier.verifyCertificate(UC, aC);
/* ^ here it is OK */
Thank you very much.
Comment by Tomas — May 1, 2011 @ 19:50
Thank you, Nakov. I work in a CA, and I ws searching for validation using BC (we have our framework), just out of sheer curiosity…
It is a great example. Covers, from what I’ve seen, pretty much all of it.
Grand job!
Comment by NoproblemBabe — February 7, 2012 @ 20:09
[...] will check client certificate against available CA certificate stored in JKS file. There is a good tutorial on this case, but still, the code is not working on my computer. So, I tried to manually develop my [...]
Pingback by X.509 Certificate Validation Using Java « codeautomate blog — February 21, 2012 @ 08:07
Hi, I’m trying to user your code to verify the certificate against list of CA stored in X509Certificate. I extract the X509Certificate in my keystore and validate using your code, but it is not working. Do your code have any limitation regarding certificate stored in java keystore (JKS)? Right now, I’ve built another code to verify my cert chain: http://codeautomate.org/blog/2012/02/certificate-validation-using-java/
Comment by hervin — February 21, 2012 @ 08:45
Great Example
Comment by Aijaz — March 9, 2012 @ 14:29
This code doesn’t work (at least with the latest bouncycastle. The certStore in line 150 has to contain all certificates (trusted, intermediate and certToVerify). If this is fixed, the code seems to work. So it’s still a great example, thank you!
Comment by Andreas Sahlbach — February 4, 2013 @ 14:37
The code doesn’t work (There’s no main class in CertificateVerifier ).
Comment by Asma — April 7, 2013 @ 16:53
Thanks – It works fine for me after tidying the code up a bit and in my case dealing with the case where the CRL URL had been moved – just needed to check the http connnection response code for 301/302 and deal with it .. altering the funcion downloadCRLFromWeb in the CRL verifier. Again, thanks for a very nice example. (for comment nº 10 .. there’s no main class as it’s supposed to used in a package (.jar)
Comment by Paul Crocker — May 19, 2013 @ 02:21