December 1, 2009

X.509 Certificate Validation in Java: Build and Verify Chain and Verify CLR with Bouncy Castle

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: , , , , , , , , ,

4 Comments »

  1. 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

  2. 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

  3. 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

  4. 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

RSS feed for comments on this post. TrackBack URL

Leave a comment