at.gv.egiz.pdfas.lib.pki.impl.OCSPClient.java Source code

Java tutorial

Introduction

Here is the source code for at.gv.egiz.pdfas.lib.pki.impl.OCSPClient.java

Source

/*******************************************************************************
 * <copyright> Copyright 2017 by PrimeSign GmbH, Graz, Austria </copyright>
 *
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * http://www.osor.eu/eupl/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 *
 * This product combines work with different licenses. See the "NOTICE" text
 * file for details on the various modules and licenses.
 * The "NOTICE" text file is part of the distribution. Any derivative works
 * that you distribute must include a readable copy of the "NOTICE" text file.
 ******************************************************************************/
package at.gv.egiz.pdfas.lib.pki.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import iaik.asn1.CodingException;
import iaik.asn1.ObjectID;
import iaik.asn1.structures.AccessDescription;
import iaik.asn1.structures.AlgorithmID;
import iaik.x509.X509Certificate;
import iaik.x509.X509ExtensionInitException;
import iaik.x509.extensions.AuthorityInfoAccess;
import iaik.x509.ocsp.BasicOCSPResponse;
import iaik.x509.ocsp.CertID;
import iaik.x509.ocsp.CertStatus;
import iaik.x509.ocsp.OCSPException;
import iaik.x509.ocsp.OCSPRequest;
import iaik.x509.ocsp.OCSPResponse;
import iaik.x509.ocsp.ReqCert;
import iaik.x509.ocsp.Request;
import iaik.x509.ocsp.SingleResponse;
import iaik.x509.ocsp.UnknownResponseException;

/**
 * Simple OCSP client for requesting OCSP responses via http. Uses Apache http client.
 * 
 * @author Thomas Knall, PrimeSign GmbH
 * @see <a href="https://tools.ietf.org/html/rfc6960#appendix-A">PKIX OCSP - OCSP over HTTP</a>
 * @implNote This class is immutable and thread-safe.
 */
public class OCSPClient implements AutoCloseable {

    private Logger log = LoggerFactory.getLogger(OCSPClient.class);

    // @formatter:off
    private final CloseableHttpClient httpClient;
    private final RequestConfig requestConfig;
    private final boolean httpCachingEnabled;
    // @formatter:on

    /**
     * Builder for creating an OCSP client.
     * 
     * @author tknall
     */
    public static class Builder {

        // @formatter:off
        private boolean httpCachingEnabled = false;
        private int connectTimeOutMillis = 10000;
        private int socketTimeOutMillis = 10000;
        // @formatter:on

        Builder() {
        }

        /**
         * Enables support for HTTP caching by sending OCSP requests using HTTP GET.
         * <p>
         * <a href="https://tools.ietf.org/html/rfc6960#appendix-A">RFC6960</a> says: <i>HTTP-based OCSP requests can
         * use either the GET or the POST method to submit their requests. To enable HTTP caching, small requests (that
         * after encoding are less than 255 bytes) MAY be submitted using GET. If HTTP caching is not important or if
         * the request is greater than 255 bytes, the request SHOULD be submitted using POST.</i>
         * </p>
         * 
         * @param httpCachingEnabled
         *            {@code true} in order to allow for caching, {@code false} otherwise.
         * @return This builder (fluent interface).
         * @implNote Default: {@code false}<p>Note that in case the encoded request is greater than 255 bytes, this setting is ignored and the
         *           request will still being sent using POST.</p>
         */
        public Builder setHttpCachingEnabled(boolean httpCachingEnabled) {
            this.httpCachingEnabled = httpCachingEnabled;
            return this;
        }

        /**
         * Sets the socket timeout in milliseconds.
         * 
         * @param socketTimeOutMillis
         *            The timeout in milliseconds (-1 means no limit).
         * @return This builder (fluent interface).
         * @implNote Default: {@code 10000}
         */
        public Builder setSocketTimeOutMillis(int socketTimeOutMillis) {
            this.socketTimeOutMillis = socketTimeOutMillis;
            return this;
        }

        /**
         * Sets the connection timeout in milliseconds.
         * 
         * @param connectTimeOutMillis
         *            The timeout in milliseconds (-1 means no limit).
         * @return This builder (fluent interface).
         * @implNote Default: {@code 10000}
         */
        public Builder setConnectTimeOutMillis(int connectTimeOutMillis) {
            this.connectTimeOutMillis = connectTimeOutMillis;
            return this;
        }

        /**
         * Creates a new OCSP client using the builder's settings. 
         * @return The OCSP client.
         */
        public OCSPClient build() {
            // @formatter:off
            RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeOutMillis)
                    .setConnectTimeout(connectTimeOutMillis).build();
            // @formatter:on
            return new OCSPClient(httpCachingEnabled, requestConfig);
        }

    }

    /**
     * Creates the client using the settings from the builder.
     * 
     * @param httpCachingEnabled
     *            {@code true} in order to support HTTP caching, {@code false} otherwise.
     * @param requestConfig
     *            The settings to be applied to the respective http request (required; must not be {@code null}).
     */
    private OCSPClient(boolean httpCachingEnabled, RequestConfig requestConfig) {
        this.httpCachingEnabled = httpCachingEnabled;
        this.requestConfig = Objects.requireNonNull(requestConfig);
        httpClient = HttpClients.createDefault();
    }

    /**
     * Returns a builder to be used for creating the OCSP client.
     * 
     * @return The builder (never {@code null}).
     */
    public static Builder builder() {
        return new Builder();
    }

    @Override
    public void close() throws Exception {
        httpClient.close();
    }

    @Override
    protected void finalize() throws Throwable {
        close();
    }

    /**
     * Utility class with useful helper methods in terms of OCSP handling. 
     * @author tknall
     *
     */
    public static class Util {

        private Util() {
        }

        /**
         * Determines if the provided certificate has an OCSP responder url.
         * 
         * @param x509Certificate
         *            The certificate (required; must not be {@code null}).
         * @return {@code true} if a responder url is available, {@code false} if not.
         * @apiNote When this method returns {@code true} it is regarded safe to invoke
         *          {@link #getOcspResponse(X509Certificate, X509Certificate)}.
         */
        public static boolean hasOcspResponder(X509Certificate x509Certificate) {
            return getOcspUrl(x509Certificate) != null;
        }

        /**
         * Determines the certificate's OCSP responder url (if any).
         * 
         * @param x509Certificate
         *            The certificate (required; must not be {@code null}).
         * @return The OCSP responder url or {@code null} if the certificate does not provide an OCSP responder url.
         */
        static String getOcspUrl(X509Certificate x509Certificate) {
            AuthorityInfoAccess aia;
            try {
                aia = (AuthorityInfoAccess) Objects.requireNonNull(x509Certificate)
                        .getExtension(AuthorityInfoAccess.oid);
            } catch (X509ExtensionInitException e) {
                throw new IllegalStateException("Unable to initialize cert extension AuthorityInfoAccess.", e);
            }
            if (aia != null) {
                AccessDescription ad = aia.getAccessDescription(ObjectID.ocsp);
                if (ad != null) {
                    return ad.getUriAccessLocation();
                }
            }
            return null;
        }

    }

    /**
     * Retrieves an OCSP response for a certain certificate ({@code eeCertificate}) of a certain CA
     * ({@code issuerCertificate}).
     * 
     * @param issuerCertificate The issuer certificate (required; must not be {@code null}).
     * @param eeCertificate     The end entity certificate (required; must not be {@code null}).
     * @return The OCSP response (never {@code null}) with guaranteed response status "successful" and with <strong>any
     *         revocation state</strong>.
     * @throws IOException              Thrown in case of error communicating with OCSP responder.
     * @throws OCSPClientException      In case the client could not process the response (e.g. non-successful response
     *                                  state like malformedRequest, internalError... or an unknown/unsupported response
     *                                  type).
     * @throws IllegalArgumentException In case the provided {@code eeCertificate} does not provide an OCSP responder
     *                                  url (use {@link Util#hasOcspResponder(X509Certificate)} in order to determine if
     *                                  it is safe to call this method) or the provided certificates could not be used
     *                                  for OCSP request creation.
     * @implNote This implementation just returns OCSP responses (<strong>of any revocation status</strong>) as they
     *           were retrieved from the OCSP responder (provided the response status indicates a successful response)
     *           without performing further checks like OCSP signature verification or OCSP responder certificate
     *           validation.
     */
    public OCSPResponse getOcspResponse(X509Certificate issuerCertificate, X509Certificate eeCertificate)
            throws IOException, OCSPClientException {

        Objects.requireNonNull(issuerCertificate, "Issuer certificate required... must not be null.");
        Objects.requireNonNull(eeCertificate, "End-entity certificate required... must not be null.");

        StopWatch sw = new StopWatch();
        sw.start();

        if (log.isDebugEnabled()) {
            log.debug("Retrieving OCSP revocation info for: {}", eeCertificate.getSubjectDN());
        } else if (log.isInfoEnabled()) {
            log.info("Retrieving OCSP revocation info for certificate (SHA-1 fingerprint): {}",
                    Hex.encodeHexString(eeCertificate.getFingerprintSHA()));
        }

        String ocspUrl = Util.getOcspUrl(eeCertificate);
        if (ocspUrl == null) {
            throw new IllegalArgumentException("The provided certificate does not feature an ocsp responder url.");
        }

        // create request
        byte[] ocspRequestEncoded;
        ReqCert reqCert;
        try {

            CertID certID = new CertID(AlgorithmID.sha1, issuerCertificate, eeCertificate);
            reqCert = new ReqCert(ReqCert.certID, certID);
            Request request = new Request(reqCert);
            OCSPRequest ocspRequest = new OCSPRequest();
            ocspRequest.setRequestList(new Request[] { request });
            ocspRequestEncoded = ocspRequest.getEncoded();

            if (log.isTraceEnabled()) {
                log.trace("Creating OCSP request: {}", request);
            }

        } catch (NoSuchAlgorithmException e) {
            // should not occur actually
            throw new IllegalStateException("Required algorithm (SHA-1) not available.", e);
        } catch (CodingException e) {
            throw new IllegalArgumentException(
                    "Unable to encode ocsp request with the provided issuer and end-entity certificates.", e);
        }

        // https://tools.ietf.org/html/rfc6960
        // GET {url}/{url-encoding of base-64 encoding of the DER encoding of the OCSPRequest}
        String b64OcspRequest = org.apache.commons.codec.binary.Base64.encodeBase64String(ocspRequestEncoded);
        String urlEncodedB64OcspRequest = URLEncoder.encode(b64OcspRequest, "UTF-8");

        HttpRequestBase request;

        if (httpCachingEnabled && urlEncodedB64OcspRequest.length() <= 255) {
            // spec proposes GET request
            URI ocspResponderUri;
            try {
                URIBuilder uriBuilder = new URIBuilder(ocspUrl);
                uriBuilder
                        .setPath(StringUtils.appendIfMissing(uriBuilder.getPath(), "/") + urlEncodedB64OcspRequest);
                ocspResponderUri = uriBuilder.build();
            } catch (URISyntaxException e) {
                // can only occur with eeCertificate containing invalid ocsp responder url
                throw new IllegalArgumentException(
                        "Unable process OCSP responder uri of provided certificate: " + ocspUrl, e);
            }

            request = new HttpGet(ocspResponderUri);
            log.debug("Sending OCSP request using HTTP GET to: {}", ocspUrl);

        } else {
            // spec proposes POST request
            HttpPost httpPost = new HttpPost(ocspUrl);
            httpPost.setEntity(
                    new ByteArrayEntity(ocspRequestEncoded, ContentType.create("application/ocsp-request")));
            request = httpPost;
            log.debug("Sending OCSP request using HTTP POST to: {}", ocspUrl);
        }

        request.setConfig(requestConfig);

        try (CloseableHttpResponse httpResponse = httpClient.execute(request)) {

            StatusLine statusLine = httpResponse.getStatusLine();
            log.debug("OCSP response HTTP status: {}", statusLine);
            if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
                throw new IOException("OCSP responder did not report HTTP 200: " + statusLine);
            }

            HttpEntity responseEntity = httpResponse.getEntity();

            OCSPResponse ocspResponse;
            try (InputStream in = responseEntity.getContent()) {

                ocspResponse = new OCSPResponse(in);

            } catch (UnknownResponseException e) {
                throw new OCSPClientException("Unknown (unsupported) OCSP response type: " + e.getResponseType(),
                        e);
            }

            if (log.isTraceEnabled()) {
                log.trace("OCSP response: {}", ocspResponse);
            }

            log.debug("OCSP response status: {}", ocspResponse.getResponseStatusName());
            if (ocspResponse.getResponseStatus() != OCSPResponse.successful) {
                throw new OCSPClientException("OCSP response status was not successful, got response status: "
                        + ocspResponse.getResponseStatusName());
            }

            // get the basic ocsp response (which is the only type currently supported, otherwise an
            // UnknownResponseException would have been thrown during parsing the response)
            BasicOCSPResponse basicOCSPResponse = (BasicOCSPResponse) ocspResponse.getResponse();

            // for future improvement: verify ocsp response, responder certificate...

            SingleResponse singleResponse;
            try {
                log.trace("Looking for OCSP response specific for: {}", reqCert);
                singleResponse = basicOCSPResponse.getSingleResponse(reqCert);
            } catch (OCSPException e) {
                try {
                    singleResponse = basicOCSPResponse.getSingleResponse(issuerCertificate, eeCertificate, null);
                } catch (OCSPException e1) {
                    throw new OCSPClientException(
                            "Unable to process received OCSP response for the provided certificate (SHA-1 fingerprint): "
                                    + Hex.encodeHexString(eeCertificate.getFingerprintSHA()),
                            e1);
                }
            }

            if (log.isTraceEnabled()) {
                log.trace("OCSP respose for specific certificate: {}", singleResponse);
            }

            CertStatus certStatus = singleResponse.getCertStatus();
            String formattedThisUpdate = DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT
                    .format(singleResponse.getThisUpdate());
            log.info("Certificate revocation state (@{}}: {}", formattedThisUpdate, certStatus);

            sw.stop();
            log.debug("OCSP query took: {}ms", sw.getTime());

            return ocspResponse;

        } // close httpResponse

    }

}