"Manual" OCSP Certificate Validation Part 2: Java

In part 1 of this blog post we discussed how to do "manual" OCSP validation of digital certificates using OpenSSL and curl. In this post we're going to discus how to do the same thing in Java.

Basic Setup

To review, there were three steps to calling OCSP:

  1. Generate the OCSP request. openssl ocsp -cert mycert.cer -issuer cacert.cer -url https://ocsp.my.ca.com [no_nonce] -reqout ocsp_req.req
  2. Send the (base64-encoded) request to the OCSP server. curl https://ocsp.my.ca.com/MEUwQz....c= -o ocsp_response.res
  3. Parse the response. openssl ocsp -respin ocsp_response.res -text -issuer cacert.cer

We are now going to repeat these steps, but this time in Java code. Let's create a small Gradle project with the following file structure:

¦   build.gradle
¦   
+---src
    +---main
        +---java
        ¦   +---com
        ¦       +---capitaltg
        ¦           +---demo
        ¦               +---ocsp
        ¦                       OcspApplication.java
        ¦                       OcspUtil.java
        ¦                       
        +---resources
                ctg-cacert.cer
                ctg-cert.cer

We'll use the main class (OcspApplication) and the certs in the resources folder for testing out our code. We'll come back to those later.

In the non-Java version we used a combination of OpenSSL and curl; for the Java equivalents we are going to use the BouncyCastle library (for OpenSSL functionality) and OkHttp for an HTTP client (for curl functionality). Here are the dependencies we need in the Gradle build file:

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
  implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
  implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

Let's focus on our OcspUtil class, which is where the main logic will be. In this utility class we're going to create three methods, one for each of the three steps outlined above for calling OCSP. We'll also add a method to do the entire OCSP call by calling the other three methods. These methods will take the same inputs that the OpenSSL or curl commands took. We use the following BouncyCastle objects to represent the OCSP request and response: org.bouncycastle.cert.ocsp.OCSPReq and org.bouncycastle.cert.ocsp.OCSPResp. So before adding any logic, the shell of our util class looks like this:

package com.capitaltg.demo.ocsp;

import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;

public class OcspUtil {

  /*
  * Return true if  validation passes, false otherwise
  */ 
  public static boolean ocspValidate(String pathToCert, String pathToIssuerCert, String ocspEndpoint, boolean includeNonce) {
    try {
      OCSPReq oscpReq = getOcspRequest(pathToCert,pathToIssuerCert,ocspEndpoint,includeNonce);
      OCSPResp ocpResp = callOcspEndpoint(oscpReq,ocspEndpoint);
      return parseOcspResponse(ocpResp);
    } catch (Exception e) {
      System.out.println("Exception when attempting OCSP validation: " + e.getMessage());
      return false;
    }
  }

  /*
  * Generate the OCSP request.
  * Equivalent to openssl ocsp -cert mycert.cer -issuer cacert.cer -url https://ocsp.my.ca.com [no_nonce] -reqout ocsp_req.req
  */
  public static OCSPReq getOcspRequest(String pathToCert, String pathToIssuerCert, String ocspEndpoint, boolean includeNonce) throws Exception {
    // Add logic
  }

  /*
  * Given an OCSP request object, call the OCSP endpoint.
  * Equivalent to curl https://ocsp.my.ca.com/MEUwQz....c= -o ocsp_response.res
  */
  public static OCSPResp callOcspEndpoint(OCSPReq request, String ocspEndpoint) throws Exception {
    // Add logic
  }

  /*
  * Given an OCSP response object, return if validation succeeded or not.
  * Similar to openssl ocsp -respin ocsp_response.res -text -issuer cacert.cer
  */
  public static boolean parseOcspResponse(OCSPResp response) throws Exception {
    // Add logic
  }

}

Before we fill in those methods, we're going to add some helper methods that will be useful later on. The BouncyCastle library uses an X509CertificateHolder object to represent certificates, so we're going to create a method to convert the input (the path to a PEM-encoded cert string) into such an object:

...
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.FileReader;
import java.io.Reader;
...
  public static X509CertificateHolder certHolderFromPemFilePath(String filePath) throws Exception {
    return new X509CertificateHolder(certBytesFromReader(new FileReader(filePath)));
  }

  public static byte[] certBytesFromReader(Reader certReader) throws Exception {
    PemReader pemReader = new PemReader(certReader);
    return pemReader.readPemObject().getContent();
  }

(If you would want to do the same from a String containing a PEM-encoded cert simply use a StringReader in place of a FileReader: return new X509CertificateHolder(certBytesFromReader(new StringReader(inputCertString)));)

We will also create a helper method for converting a byte array (i.e., binary data) into a base64-encoded string. We could use native java for this, but BouncyCastle includes a nice helper class:

...
import org.bouncycastle.util.encoders.Base64;
...
  public static String base64EncodeByteArray(byte[] input) {
    return Base64.toBase64String(input);
  }

Now we are ready to add the logic to our three methods.

Generating the OCSP Request

In our first method we want to generate the OCSP request. In order to do that, we use the BouncyCastle OCSPReqBuilder class. We need to add to our request information about the cert we want to validate, and optionally add a nonce extension.
For adding the cert details, OCSPReqBuilder requires a CertificateID object; the latter is constructed with 3 items: 1. a DigestCalculator 2. an X509CertificateHolder of the issuer, i.e. CA cert and 3. the serial number of the cert to validate. For the first of these three items we want a SHA1 Digest Calculator; for now we'll put in such an object without an import and then see where we get that from.

...
import java.util.Random;

import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
...
  public static OCSPReq getOcspRequest(String pathToCert, String pathToIssuerCert, String ocspEndpoint, boolean includeNonce)
      throws Exception {
    OCSPReqBuilder builder = new OCSPReqBuilder();
    X509CertificateHolder cert = certHolderFromPemFilePath(pathToCert);
    X509CertificateHolder issuerCert = certHolderFromPemFilePath(pathToIssuerCert);
    CertificateID certId = new CertificateID(
      new SHA1DigestCalculator(),
      issuerCert,
      cert.getSerialNumber());
    builder.addRequest(certId);
    if (includeNonce) {
      // TODO
    }
    return builder.build();
  }

Regarding the SHA1 Digest Calculator, the BouncyCastle library has such a class (org.bouncycastle.cert.test.SHA1DigestCalculator), but it is included in their test code rather than in their main code, so it is not included in any published jar (as far as I know). Rather than re-invent the wheel we are going to shamelessly copy their code and only change the package name. Since they use an Apache license we don't have any legal issues with this (lawyers, keep me honest!). Bouncy Castle people, if you are reading this, please consider moving this class to the non-test area of the code!

//package org.bouncycastle.cert.test
package com.capitaltg.demo.ocsp;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;

import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.operator.DigestCalculator;

class SHA1DigestCalculator
    implements DigestCalculator
{
    private ByteArrayOutputStream bOut = new ByteArrayOutputStream();

    public AlgorithmIdentifier getAlgorithmIdentifier()
    {
        return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1);
    }

    public OutputStream getOutputStream()
    {
        return bOut;
    }

    public byte[] getDigest()
    {
        byte[] bytes = bOut.toByteArray();

        bOut.reset();

        Digest sha1 = new SHA1Digest();

        sha1.update(bytes, 0, bytes.length);

        byte[] digest = new byte[sha1.getDigestSize()];

        sha1.doFinal(digest, 0);

        return digest;
    }
}

Now we'll add the extension for the nonce. Fortunately for us we can copy the details from org.bouncycastle.cert.ocsp.test.OCSPTest.

  public static OCSPReq getOcspRequest(String certString, String issuerCertString, String ocspEndpoint, boolean includeNonce)
      throws Exception {
    ...
    if (includeNonce) {
      // Based off of org.bouncycastle.cert.ocsp.test.OCSPTest
      byte[] nonce = new byte[16];
      Random rand = new Random();
      rand.nextBytes(nonce);
      Extension nonceExtension = new Extension(
        OCSPObjectIdentifiers.id_pkix_ocsp_nonce,
        false,
        new DEROctetString(nonce));
      builder.setRequestExtensions(new Extensions(nonceExtension));
    }
    return builder.build();
  }

Making the Http Call

In the second method we're going to take in the OCSPReq object and call the OCSP endpoint for a response. Just as we saw in Part 1, we have to base64-encode the OCSP response, and include that encoded version in the url. We will create one instance of the OkHttpClient for re-use by this method.

...
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
...
  private static final OkHttpClient HTTPCLIENT = new OkHttpClient();
...
  public static OCSPResp callOcspEndpoint(OCSPReq request, String ocspEndpoint) throws Exception {
    String fullUrl = ocspEndpoint + "/" + base64EncodeByteArray(request.getEncoded());
    System.out.println("Full url for OCSP request is: " + fullUrl);
    Request.Builder requestBuilder = new Request.Builder();
    Request httpRequest = requestBuilder.url(fullUrl).build();
    Call httpCall = HTTPCLIENT.newCall(httpRequest);
    byte[] httpResponse = httpCall.execute().body().bytes();
    System.out.println("OCSP response (Base64 encoded) is: " + base64EncodeByteArray(httpResponse));
    OCSPResp ocspResponse = new OCSPResp(httpResponse);
    return ocspResponse;
  }

Parsing the Response

The OCSPResp object doesn't have methods to give us too much info about the response. The only useful method is getStatus(). We use that to determine if the response was successful. We'll add a helper method to convert that integer status into human-readable info:

  public static boolean parseOcspResponse(OCSPResp response) throws Exception {
    int status = response.getStatus();
    System.out.println("OCSP response status is: " + getOcspResponseStatusAsString(status));
    return (status==OCSPResp.SUCCESSFUL);
  }
  
  public static String getOcspResponseStatusAsString(int status) {
    switch(status) {
      case OCSPResp.INTERNAL_ERROR:
        return "INTERNAL_ERROR";
      case OCSPResp.MALFORMED_REQUEST:
        return "MALFORMED_REQUEST";
      case OCSPResp.SIG_REQUIRED:
        return "SIG_REQUIRED";
      case OCSPResp.SUCCESSFUL:
        return "SUCCESSFUL";
      case OCSPResp.TRY_LATER:
        return "TRY_LATER";
      case OCSPResp.UNAUTHORIZED:
        return "UNAUTHORIZED";
      default:
        return "UNRECOGNIZED STATUS";
    }
  }

Testing Out

We're ready to test our java util class! We'll use the main class to call our OCSP util class. assume a cert file in src/main/resources/cert.cer and a CA cert in src/main/resources/cacert.cer. For now we will add the OCSP URL manually.

For purposes of the demo, we'll use CTG's TLS certificate, which we can easily grab using our browser. (Google how to download tls certificate from browser for details how to do that.) We save that in src/main/resources/ctg-cert.cer. Examining the cert details (using openssl x509 -in ctg-cert.cer -text -noout ) we can see that the issuer of the cert is Amazon RSA 2048 M02 and can be downloaded from http://crt.r2m02.amazontrust.com/r2m02.cer. We save the CA cert in src/main/resources/ctg-cacert.cer.

From CTG's cert we also see that the OCSP URL is http://ocsp.r2m02.amazontrust.com. We'll hard code this value for now. Here is the main class:

package com.capitaltg.demo.ocsp;

public class OcspApplication {
  
  public static void main(String[] args) {
    try {
      String certPath = "src/main/resources/ctg-cert.cer";
      String caCertPath = "src/main/resources/ctg-cacert.cer";
      String ocspEndpoint = "http://ocsp.r2m02.amazontrust.com";
      boolean useNonce = true;
      boolean valid = OcspUtil.ocspValidate(certPath,caCertPath,ocspEndpoint,useNonce);
      System.out.println("Status of OCSP validation is: " + valid);
    } catch (Exception e) {
      System.out.println("Exception running application: " + e.getMessage());
    }
  }

}

As can been seen, we simply set the filepaths for the CTG cert and the CA cert, as well as the URL for the OCSP endpoint and then call the util class. To run the app we'll use the gradle application plugin and the run task. For that we need to set the mainClass setting. Here is the full build file:

plugins {
  id 'java'
  id 'application'
}

group = 'com.capitaltg.demo'
version = '0.0.1-SNAPSHOT'
application.mainClass = 'com.capitaltg.demo.ocsp.OcspApplication'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
  implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
  implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
  implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

build.gradle

Testing output:

$ gradle run

> Task :run
Full url for OCSP request is: http://ocsp.r2m02.amazontrust.com/MHIwcDBLMEkwRzAHBgUrDgMCGgQUZm0LcJyJ5L5L6+wTRUfktpU2D4wEFMAxUs1aUMOCfHRxzsvpnPl664LiAhAKXCx8mi8qWEKXUyIno4kDoiEwHzAdBgkrBgEFBQcwAQIEEBSAB21N7FUzwuM2KtDINNE=
OCSP response (Base64 encoded) is: MIIB0woBAKCCAcwwggHIBgkrBgEFBQcwAQEEggG5MIIBtTCBnqIWBBTAMVLNWlDDgnx0cc7L6Zz5euuC4hgPMjAyNTA1MDMyMDU1NDJaMHMwcTBJMAkGBSsOAwIaBQAEFGZtC3CcieS+S+vsE0VH5LaVNg+MBBTAMVLNWlDDgnx0cc7L6Zz5euuC4gIQClwsfJovKlhCl1MiJ6OJA4AAGA8yMDI1MDUwMzIwMzkwMlqgERgPMjAyNTA1MTAxOTM5MDJaMA0GCSqGSIb3DQEBCwUAA4IBAQB4etsMvCxn9tUzzKAgn8sg54u7OG9EsQdlHgYIEJ/wicsxlhJ6UlUjIu5qfS1bNI+49AvzIy5SC4U+OptCfXgtRL287G0ZsVGE+C2PS3VyPnwlNEe6IUwT8CJoiaBCrONfuEsQM52MGREh8V683scXttlgJ5lk82nIwkzmbe0peUn9jbB2ULfv57DTWsJUgxIvlVquoV5YP1LevLsxJmuJSezFQkpXA18+VBhxUMYQZYkwbtB33m3AM/IpapsGzDDw2+TfkTo7kInlcjQsEJTraxDAhV4UGMolG+VWEVugIcYOfz3XcIG3hZZb6VMLKEtoE318KayKroH0UgMoBlsU
OCSP response status is: SUCCESSFUL
Status of OCSP validation is: true

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed

Success! Now the advantage of logging the request and response is we can better investigate in case of issues. Let's use OpenSSL to examine the logged request and response.

First the request:

$ echo 'MHIwcDBLMEkwRzAHBgUrDgMCGgQUZm0LcJyJ5L5L6+wTRUfktpU2D4wEFMAxUs1aUMOCfHRxzsvpnPl664LiAhAKXCx8mi8qWEKXUyIno4kDoiEwHzAdBgkrBgEFBQcwAQIEEBSAB21N7FUzwuM2KtDINNE=' | openssl base64 -d > ocsp_req_ctg.req

$ openssl ocsp -reqin ocsp_req_ctg.req -text
OCSP Request Data:
    Version: 1 (0x0)
    Requestor List:
        Certificate ID:
          Hash Algorithm: sha1
          Issuer Name Hash: 666D0B709C89E4BE4BEBEC134547E4B695360F8C
          Issuer Key Hash: C03152CD5A50C3827C7471CECBE99CF97AEB82E2
          Serial Number: 0A5C2C7C9A2F2A584297532227A38903
    Request Extensions:
        OCSP Nonce:
            1480076D4DEC5533C2E3362AD0C834D1

Then the response:

$ echo 'MIIB0woBAKCCAcwwggHIBgkrBgEFBQcwAQEEggG5MIIBtTCBnqIWBBTAMVLNWlDDgnx0cc7L6Zz5euuC4hgPMjAyNTA1MDMyMDU1NDJaMHMwcTBJMAkGBSsOAwIaBQAEFGZtC3CcieS+S+vsE0VH5LaVNg+MBBTAMVLNWlDDgnx0cc7L6Zz5euuC4gIQClwsfJovKlhCl1MiJ6OJA4AAGA8yMDI1MDUwMzIwMzkwMlqgERgPMjAyNTA1MTAxOTM5MDJaMA0GCSqGSIb3DQEBCwUAA4IBAQB4etsMvCxn9tUzzKAgn8sg54u7OG9EsQdlHgYIEJ/wicsxlhJ6UlUjIu5qfS1bNI+49AvzIy5SC4U+OptCfXgtRL287G0ZsVGE+C2PS3VyPnwlNEe6IUwT8CJoiaBCrONfuEsQM52MGREh8V683scXttlgJ5lk82nIwkzmbe0peUn9jbB2ULfv57DTWsJUgxIvlVquoV5YP1LevLsxJmuJSezFQkpXA18+VBhxUMYQZYkwbtB33m3AM/IpapsGzDDw2+TfkTo7kInlcjQsEJTraxDAhV4UGMolG+VWEVugIcYOfz3XcIG3hZZb6VMLKEtoE318KayKroH0UgMoBlsU' | openssl base64 -d > ocsp_response_ctg.res

$ openssl ocsp -respin ocsp_response_ctg.res -text -issuer ctg-cacert.cer
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response
    Version: 1 (0x0)
    Responder Id: C03152CD5A50C3827C7471CECBE99CF97AEB82E2
    Produced At: May  3 20:55:42 2025 GMT
    Responses:
    Certificate ID:
      Hash Algorithm: sha1
      Issuer Name Hash: 666D0B709C89E4BE4BEBEC134547E4B695360F8C
      Issuer Key Hash: C03152CD5A50C3827C7471CECBE99CF97AEB82E2
      Serial Number: 0A5C2C7C9A2F2A584297532227A38903
    Cert Status: good
    This Update: May  3 20:39:02 2025 GMT
    Next Update: May 10 19:39:02 2025 GMT

    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        ...
Response verify OK

That's what a good response looks like. What about a bad response? We can find out by purposely sending the request to the wrong OCSP provider. For this we'll use http://o.pki.goog/we2, a Google OCSP endpoint. We simply update in the main class:

  public static void main(String[] args) {
    try {
      String certPath = "src/main/resources/ctg-cert.cer";
      String caCertPath = "src/main/resources/ctg-cacert.cer";
      //String ocspEndpoint = "http://ocsp.r2m02.amazontrust.com";
      String ocspEndpoint = "http://o.pki.goog/we2";
      boolean useNonce = true;
      boolean valid = OcspUtil.ocspValidate(certPath,caCertPath,ocspEndpoint,useNonce);
      System.out.println("Status of OCSP validation is: " + valid);
    } catch (Exception e) {
      System.out.println("Exception running application: " + e.getMessage());
    }
  }

And now when we run we get this output:

$ gradle run
Starting a Gradle Daemon, 1 stopped Daemon could not be reused, use --status for details

> Task :run
Full url for OCSP request is: http://o.pki.goog/we2/MHIwcDBLMEkwRzAHBgUrDgMCGgQUZm0LcJyJ5L5L6+wTRUfktpU2D4wEFMAxUs1aUMOCfHRxzsvpnPl664LiAhAKXCx8mi8qWEKXUyIno4kDoiEwHzAdBgkrBgEFBQcwAQIEEGQoMN1q7DzUjRi2un73FAo=
OCSP response (Base64 encoded) is: MAMKAQY=
OCSP response status is: UNAUTHORIZED
Status of OCSP validation is: false

BUILD SUCCESSFUL in 25s

Not surprisingly the response is a lot shorter. This is how OpenSSL interprets the response:

$ echo 'MAMKAQY=' | openssl base64 -d > ocsp_response_bad.res

$ openssl ocsp -respin ocsp_response_bad.res -text
Responder Error: unauthorized (6)

Getting the OCSP URL

The code above assumes that you know the OCSP URL ahead of time. But in case you want to grab that from the cert recall that we used an OpenSSL command. What is the java / BouncyCastle version of that? We'll add one final method to pull the OCSP url from the cert.

Unfortunately there isn't a method in X509CertificateHolder like getOcspUrl() to make our lives easier. (Note to BouncyCastle people if you are reading this: Please add such a method!) In fact, it's included in the v3 extensions section of the cert. We can see where it is exactly by using an Openssl command to view the cert contents in human-readable form:

$ openssl x509 -text -in cert.crt -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
        ...
        X509v3 extensions:
            ...
            Authority Information Access:
                OCSP - URI:https://ocsp.my.ca.com
                ...
            X509v3 Subject Alternative Name:
            ...

What we see from here is that the OCSP url contained in the cert structure under X509v3 extensions > Authority Information Access > OCSP . Using this knowledge, we can use BouncyCastle to dig into the cert to get the info we're looking for. That is, we're going to parse the BouncyCastle object AuthorityInformationAccess from the cert, then loop through its relevant sub-structures until we find the right data. AccessDescription.id_ad_ocsp has the OID of the OCSP "access description," which contains the url.

...
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.AccessDescription;
...
  public static String getOcspUrl(String certPath) throws Exception {
    X509CertificateHolder cert = certHolderFromPemFilePath(certPath);
    AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(cert.getExtensions());
    AccessDescription[] accessDescriptions = aiaExtension.getAccessDescriptions();
    for (AccessDescription ad : accessDescriptions) {
      if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) {
        return ad.getAccessLocation().getName().toASN1Primitive().toString();
      }
    }
    return "";
  }

We can now update our main class to take advantage of this method:

package com.capitaltg.demo.ocsp;

public class OcspApplication {
  
  public static void main(String[] args) {
    try {
      String certPath = "src/main/resources/ctg-cert.cer";
      String caCertPath = "src/main/resources/ctg-cacert.cer";
      String ocspEndpoint = OcspUtil.getOcspUrl(certPath);
      System.out.println("OCSP url is: " + ocspEndpoint);
      boolean useNonce = true;
      boolean valid = OcspUtil.ocspValidate(certPath,caCertPath,ocspEndpoint,useNonce);
      System.out.println("Status of OCSP validation is: " + valid);
    } catch (Exception e) {
      System.out.println("Exception running application: " + e.getMessage());
    }
  }

}

And test it out again:

$ gradle run

> Task :run
OCSP url is: http://ocsp.r2m02.amazontrust.com
Full url for OCSP request is: http://ocsp.r2m02.amazontrust.com/MHIwcDBLMEkwRzAHBgUrDgMCGgQUZm0LcJyJ5L5L6+wTRUfktpU2D4wEFMAxUs1aUMOCfHRxzsvpnPl664LiAhAKXCx8mi8qWEKXUyIno4kDoiEwHzAdBgkrBgEFBQcwAQIEEKafqlYDAgXmVNZM90FRg2Q=
OCSP response (Base64 encoded) is: MIIB0woBAKCCAcwwggHIBgkrBgEFBQcwAQEEggG5MIIBtTCBnqIWBBTAMVLNWlDDgnx0cc7L6Zz5euuC4hgPMjAyNTA1MDQyMDU1MzZaMHMwcTBJMAkGBSsOAwIaBQAEFGZtC3CcieS+S+vsE0VH5LaVNg+MBBTAMVLNWlDDgnx0cc7L6Zz5euuC4gIQClwsfJovKlhCl1MiJ6OJA4AAGA8yMDI1MDUwNDIwMzkwMVqgERgPMjAyNTA1MTExOTM5MDFaMA0GCSqGSIb3DQEBCwUAA4IBAQBgOg4pRvcBWk4Zwl+NrblPlkUcZs5ZXApYpw//4oi2jHQpuemkuLz+MsHBf/nxLREdqwOnSZmH0NVN+gwDScE5uHqjefplC2A9LmzIep5hNh8q5WeHTvyaAbR/Wk0IcsV/200doVnhlJcditrOPe9SdkGl6GmRojjLOhdVUfI9qNQzYrhD2zIqXbzjN4MgOsRSR4ffzJZ8vf/NIWho0mFGtjPD282vUrpH3R5EjoAhH8DaMs9ANpG4xEiTe6Q3eZ4xWVOTDEuETVeBYDxsWLuZMhE4VD8NsvrskKejxpuIPCnpZgoLfzvJNe37ZAc1Mvu/4K2Ttg7R+3lYlpDkL5jT
OCSP response status is: SUCCESSFUL
Status of OCSP validation is: true

Putting it all together.

Here is the full code for the util class we created:

package com.capitaltg.demo.ocsp;

import java.io.FileReader;
import java.io.Reader;
import java.util.Random;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;

import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.io.pem.PemReader;

public class OcspUtil {
  
  private static final OkHttpClient HTTPCLIENT = new OkHttpClient();
  
  /*
  * Return true if  validation passes, false otherwise
  */ 
  public static boolean ocspValidate(String pathToCert, String pathToIssuerCert, String ocspEndpoint, boolean includeNonce) {
    try {
      OCSPReq oscpReq = getOcspRequest(pathToCert,pathToIssuerCert,ocspEndpoint,includeNonce);
      OCSPResp ocpResp = callOcspEndpoint(oscpReq,ocspEndpoint);
      return parseOcspResponse(ocpResp);
    } catch (Exception e) {
      System.out.println("Exception when attempting OCSP validation: " + e.getMessage());
      return false;
    }
  }
  
  /*
  * Generate the OCSP request.
  * Equivalent to openssl ocsp -cert mycert.cer -issuer cacert.cer -url https://ocsp.my.ca.com [no_nonce] -reqout ocsp_req.req
  */
  public static OCSPReq getOcspRequest(String pathToCert, String pathToIssuerCert, String ocspEndpoint, boolean includeNonce)
      throws Exception {
    OCSPReqBuilder builder = new OCSPReqBuilder();
    X509CertificateHolder cert = certHolderFromPemFilePath(pathToCert);
    X509CertificateHolder issuerCert = certHolderFromPemFilePath(pathToIssuerCert);
    CertificateID certId = new CertificateID(
      new SHA1DigestCalculator(),
      issuerCert,
      cert.getSerialNumber());
    builder.addRequest(certId);
    if (includeNonce) {
      // Based off of org.bouncycastle.cert.ocsp.test.OCSPTest
      byte[] nonce = new byte[16];
      Random rand = new Random();
      rand.nextBytes(nonce);
      Extension nonceExtension = new Extension(
        OCSPObjectIdentifiers.id_pkix_ocsp_nonce,
        false,
        new DEROctetString(nonce));
      builder.setRequestExtensions(new Extensions(nonceExtension));
    }
    return builder.build();
  }

  /*
  * Given an OCSP request object, call the OCSP endpoint.
  * Equivalent to curl https://ocsp.my.ca.com/MEUwQz....c= -o ocsp_response.res
  */
  public static OCSPResp callOcspEndpoint(OCSPReq request, String ocspEndpoint) throws Exception {
    String fullUrl = ocspEndpoint + "/" + base64EncodeByteArray(request.getEncoded());
    System.out.println("Full url for OCSP request is: " + fullUrl);
    Request.Builder requestBuilder = new Request.Builder();
    Request httpRequest = requestBuilder.url(fullUrl).build();
    Call httpCall = HTTPCLIENT.newCall(httpRequest);
    byte[] httpResponse = httpCall.execute().body().bytes();
    System.out.println("OCSP response (Base64 encoded) is: " + base64EncodeByteArray(httpResponse));
    OCSPResp ocspResponse = new OCSPResp(httpResponse);
    return ocspResponse;
  }

  /*
  * Given an OCSP response object, return if validation succeeded or not.
  * Similar to openssl ocsp -respin ocsp_response.res -text -issuer cacert.cer
  */
  public static boolean parseOcspResponse(OCSPResp response) throws Exception {
    int status = response.getStatus();
    System.out.println("OCSP response status is: " + getOcspResponseStatusAsString(status));
    return (status==OCSPResp.SUCCESSFUL);
  }

  public static String getOcspResponseStatusAsString(int status) {
    switch(status) {
      case OCSPResp.INTERNAL_ERROR:
        return "INTERNAL_ERROR";
      case OCSPResp.MALFORMED_REQUEST:
        return "MALFORMED_REQUEST";
      case OCSPResp.SIG_REQUIRED:
        return "SIG_REQUIRED";
      case OCSPResp.SUCCESSFUL:
        return "SUCCESSFUL";
      case OCSPResp.TRY_LATER:
        return "TRY_LATER";
      case OCSPResp.UNAUTHORIZED:
        return "UNAUTHORIZED";
      default:
        return "UNRECOGNIZED STATUS";
    }
  }

  public static String getOcspUrl(String certPath) throws Exception {
    X509CertificateHolder cert = certHolderFromPemFilePath(certPath);
    AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(cert.getExtensions());
    AccessDescription[] accessDescriptions = aiaExtension.getAccessDescriptions();
    for (AccessDescription ad : accessDescriptions) {
      if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) {
        return ad.getAccessLocation().getName().toASN1Primitive().toString();
      }
    }
    return "";
  }
  
  public static X509CertificateHolder certHolderFromPemFilePath(String filePath) throws Exception {
    return new X509CertificateHolder(certBytesFromReader(new FileReader(filePath)));
  }

  public static byte[] certBytesFromReader(Reader certReader) throws Exception {
    PemReader pemReader = new PemReader(certReader);
    return pemReader.readPemObject().getContent();
  }
  
  public static String base64EncodeByteArray(byte[] input) {
    return Base64.toBase64String(input);
  }

}

(The full build.gradle file was given above, as was the full main class OcspApplication and SHA1DigestCalculator .)

This concludes our look at how to perform OCSP validation "manually" i.e. using an HTTP client to do the actual call over the web. The benefit of this method is that it allows us finer control over the OCSP process and better ability to debug if something goes wrong. We can log the OCSP requests that are made and then try them out ourselves via CLI (see Part 1). And it allows finer control over the HTTP call itself in case there are custom requirements, such as if the call is made behind a corporate proxy. Happy coding!

For the complete example project visit our GitHub repository.