Menu

 

Thoughts on Software Engineering

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.

Previews (26,703), Views (20,502), Comments (32)

32 Responses to “X.509 Certificate Validation in Java: Build and Verify Chain and Verify CLR with Bouncy Castle”

  1. Rod says:

    Great example, well written and complete. Many thanks …the code answered many questions I had regarding CRL’s.

  2. Dpak says:

    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

  3. nakov says:

    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

  4. Tomas says:

    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.

  5. NoproblemBabe says:

    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!

  6. […] 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 […]

  7. hervin says:

    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/

  8. Aijaz says:

    Great Example

  9. Andreas Sahlbach says:

    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!

  10. Asma says:

    The code doesn’t work (There’s no main class in CertificateVerifier ).

  11. Paul Crocker says:

    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)

  12. If you are going for finest contents like I do, just pay a quick visit this site everyday as it
    gives quality contents, thanks

    Also visit my web blog … back pain singapore

  13. Mauricio Giacomini says:

    Great example. However, I´m not get compiling because of expression “List”. This expression exists in Java?
    Yours faithfully.

  14. Mauricio Giacomini says:

    List that not working for me.
    Thanks.

  15. Mauricio Giacomini says:

    Your blog cut my special characters.

  16. Francisco says:

    Amazing! This blog looks just like my old one! It’s on a
    entirely different subject but it has pretty
    much the same layout and design. Wonderful choice of colors!

    Feel free to surf to my weblog; search engine optimization; Francisco,

  17. Bashar Mohammad Othman says:

    Very very useful ,Big thanks and your job is valuable.

  18. Diego Mariano says:

    Thank you so much! Your code works very well for me!

  19. Yashi says:

    I am having a certificate i want to verify that certificate how can i extract chain certificate from it.I want to know what should i pass in the method,verifyCertificate(X509Certificate cert, Set additionalCerts) in class CertificateVerifier.

  20. aayush says:

    i am having problem with the function downloadCrlFromLdap(). It says in order to perform operation; a successful bind is required. can you help ?

  21. Arun says:

    Is there a version of this code available in C#?

    Thanks,

    Arun

  22. Nakov says:

    I don’t have C# version

  23. kumar says:

    I have been provided with X509 certificates in PEM format by interface system. they are sending byte of 256 length which they call it as public certificate. I need to verify this 256 bytes with X.509 certificate.Please advice how can I do this.

  24. Wladdy says:

    Great tutorial… thanks.

    Anybody knows how validate againts OCSP??

    Thanks again

  25. casiopusat says:

    i am a just beginer blogger, i want to be a question to you ? how i have SSL Certificates Validation on?

  26. cau says:

    Great tutorial, very clear, nevertheless I am wondering why the selector is initialized with the certificate to verify, for me only the Root CA has to be in the selector in order to be matched with the TrustAnchor

  27. وظائف خالية

    X.509 Certificate Validation in Java: Build and Verify Chain and Verify CLR with Bouncy Castle | Svetlin Nakov’s Blog

  28. Anton Antonov says:

    Hey,

    Thanks for the tutorial. However is it really needed to do the manual check, aren’t the OCSP/CRLs checked automatically if you set the following properties:

    CertPath certificatePath = …;
    PKIXParameters params = …;
    params.setRevocationEnabled(true);
    Security.setProperty(“ocsp.enable”, “true”);
    // Fallback check for CRL if no OCSP is available
    System.setProperty(“com.sun.security.enableCRLDP”, “true”);
    CertPathValidator validator = CertPathValidator.getInstance(“PKIX”, BouncyCastleProvider.PROVIDER_NAME);
    validator.validate(certificatePath, params);

  29. hosein aqajani says:

    I have two problem with your code in the following statement:
    Set

    first: There is not any x509certificate in java and the imported libraries, and you should use X509Certificate with capital letter ‘X’ and ‘C’. Am I right?

    second: Is there such a statement in java? I mean the additional statement of “”

RSS feed for comments on this post. TrackBack URL

LEAVE A COMMENT