OnlineOcspRepository.java

/*
 * Java Trust Project.
 * Copyright (C) 2009 FedICT.
 * Copyright (C) 2014-2023 e-Contract.be BV.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see 
 * http://www.gnu.org/licenses/.
 */
package be.fedict.trust.ocsp;

import java.net.HttpURLConnection;
import java.net.URI;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import be.fedict.trust.Credentials;
import be.fedict.trust.NetworkConfig;

/**
 * Online OCSP repository. This implementation will contact the OCSP Responder
 * to retrieve the OCSP response.
 *
 * @author Frank Cornelis
 *
 */
public class OnlineOcspRepository implements OcspRepository {

	private static final Logger LOGGER = LoggerFactory.getLogger(OnlineOcspRepository.class);

	private final NetworkConfig networkConfig;

	private Credentials credentials;

	/**
	 * Main construtor.
	 *
	 * @param networkConfig the optional network configuration used during OCSP
	 *                      Responder communication.
	 */
	public OnlineOcspRepository(NetworkConfig networkConfig) {
		this.networkConfig = networkConfig;
	}

	/**
	 * Default constructor.
	 */
	public OnlineOcspRepository() {
		this(null);
	}

	/**
	 * Sets the credentials to use to access protected OCSP services.
	 *
	 * @param credentials
	 */
	public void setCredentials(Credentials credentials) {
		this.credentials = credentials;
	}

	@Override
	public OCSPResp findOcspResponse(URI ocspUri, X509Certificate certificate, X509Certificate issuerCertificate,
			Date validationDate) {
		if (null == ocspUri) {
			return null;
		}
		OCSPResp ocspResp = null;
		try {
			ocspResp = getOcspResponse(ocspUri, certificate, issuerCertificate);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return ocspResp;
	}

	private OCSPResp getOcspResponse(URI ocspUri, X509Certificate certificate, X509Certificate issuerCertificate)
			throws Exception {
		LOGGER.debug("OCSP URI: {}", ocspUri);
		OCSPReqBuilder ocspReqBuilder = new OCSPReqBuilder();
		DigestCalculatorProvider digCalcProv = new JcaDigestCalculatorProviderBuilder()
				.setProvider(BouncyCastleProvider.PROVIDER_NAME).build();
		CertificateID certId = new CertificateID(digCalcProv.get(CertificateID.HASH_SHA1),
				new JcaX509CertificateHolder(issuerCertificate), certificate.getSerialNumber());
		ocspReqBuilder.addRequest(certId);

		byte[] nonce = new byte[20];
		SecureRandom secureRandom = new SecureRandom();
		secureRandom.nextBytes(nonce);
		DEROctetString encodedNonceValue = new DEROctetString(new DEROctetString(nonce).getEncoded());
		Extension extension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, encodedNonceValue);
		Extensions extensions = new Extensions(extension);
		ocspReqBuilder.setRequestExtensions(extensions);

		OCSPReq ocspReq = ocspReqBuilder.build();
		byte[] ocspReqData = ocspReq.getEncoded();

		HttpPost httpPost = new HttpPost(ocspUri.toString());
		ContentType contentType = ContentType.create("application/ocsp-request");
		HttpEntity requestEntity = new ByteArrayEntity(ocspReqData, contentType);
		httpPost.addHeader("User-Agent", "jTrust OCSP Client");
		httpPost.setEntity(requestEntity);

		int timeout = 10;
		RequestConfig.Builder requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(timeout,
				TimeUnit.SECONDS);

		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
		BasicHttpClientConnectionManager basicHttpClientConnectionManager = new BasicHttpClientConnectionManager();
		ConnectionConfig connectionConfig = ConnectionConfig.custom().setConnectTimeout(timeout, TimeUnit.SECONDS)
				.setSocketTimeout(timeout, TimeUnit.SECONDS).build();
		basicHttpClientConnectionManager.setConnectionConfig(connectionConfig);
		httpClientBuilder.setConnectionManager(basicHttpClientConnectionManager);
		if (null != this.networkConfig) {
			HttpHost proxy = new HttpHost(this.networkConfig.getProxyHost(), this.networkConfig.getProxyPort());
			httpClientBuilder.setProxy(proxy);
		}
		HttpClientContext httpClientContext = HttpClientContext.create();
		if (null != this.credentials) {
			this.credentials.init(httpClientContext);
		}
		RequestConfig requestConfig = requestConfigBuilder.build();
		httpClientBuilder.setDefaultRequestConfig(requestConfig);
		try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
			HttpClientResponseHandler<OCSPResp> responseHandler = (ClassicHttpResponse httpResponse) -> {
				int responseCode = httpResponse.getCode();
				if (HttpURLConnection.HTTP_OK != responseCode) {
					LOGGER.error("HTTP response code: {}", responseCode);
					return null;
				}

				Header responseContentTypeHeader = httpResponse.getFirstHeader("Content-Type");
				if (null == responseContentTypeHeader) {
					LOGGER.error("no Content-Type response header");
					return null;
				}
				String resultContentType = responseContentTypeHeader.getValue();
				if (!"application/ocsp-response".equals(resultContentType)) {
					LOGGER.error("result content type not application/ocsp-response");
					LOGGER.error("actual content-type: {}", resultContentType);
					if ("text/html".equals(resultContentType)) {
						LOGGER.error("content: {}", EntityUtils.toString(httpResponse.getEntity()));
					}
					return null;
				}

				Header responseContentLengthHeader = httpResponse.getFirstHeader("Content-Length");
				if (null != responseContentLengthHeader) {
					String resultContentLength = responseContentLengthHeader.getValue();
					if ("0".equals(resultContentLength)) {
						LOGGER.debug("no content returned");
						return null;
					}
				}

				HttpEntity httpEntity = httpResponse.getEntity();
				OCSPResp ocspResp = new OCSPResp(httpEntity.getContent());
				LOGGER.debug("OCSP response size: {} bytes", ocspResp.getEncoded().length);

				int ocspRespStatus = ocspResp.getStatus();
				if (OCSPResponseStatus.SUCCESSFUL != ocspRespStatus) {
					LOGGER.debug("OCSP response status: {}", ocspRespStatus);
					return ocspResp;
				}

				Object responseObject;
				try {
					responseObject = ocspResp.getResponseObject();
				} catch (OCSPException ex) {
					LOGGER.error("OCSP error: " + ex.getMessage(), ex);
					return null;
				}
				BasicOCSPResp basicOCSPResp = (BasicOCSPResp) responseObject;
				Extension nonceExtension = basicOCSPResp.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
				if (null == nonceExtension) {
					LOGGER.debug("no nonce extension in response");
					return ocspResp;
				}

				ASN1OctetString nonceExtensionValue = extension.getExtnValue();
				ASN1Primitive nonceValue = ASN1Primitive.fromByteArray(nonceExtensionValue.getOctets());
				byte[] responseNonce = ((DEROctetString) nonceValue).getOctets();
				if (!Arrays.areEqual(nonce, responseNonce)) {
					LOGGER.error("nonce mismatch");
					return null;
				}
				LOGGER.debug("nonce match");

				return ocspResp;
			};
			OCSPResp ocspResp = httpClient.execute(httpPost, httpClientContext, responseHandler);
			return ocspResp;
		}
	}
}