OcspTrustLinker.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.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import be.fedict.trust.linker.PublicKeyTrustLinker;
import be.fedict.trust.linker.TrustLinker;
import be.fedict.trust.linker.TrustLinkerResult;
import be.fedict.trust.linker.TrustLinkerResultException;
import be.fedict.trust.linker.TrustLinkerResultReason;
import be.fedict.trust.policy.AlgorithmPolicy;
import be.fedict.trust.revocation.OCSPRevocationData;
import be.fedict.trust.revocation.RevocationData;
import org.bouncycastle.asn1.DERIA5String;

/**
 * Trust linker based on OCSP revocation information.
 * 
 * @author Frank Cornelis
 * 
 */
public class OcspTrustLinker implements TrustLinker {

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

	private final OcspRepository ocspRepository;

	/**
	 * Default OCSP freshness interval. Apparently 10 seconds it too low for NTP
	 * synchronized servers.
	 */
	public static final long DEFAULT_FRESHNESS_INTERVAL = 1000 * 60 * 5;

	private long freshnessInterval = DEFAULT_FRESHNESS_INTERVAL;

	/**
	 * Main constructor.
	 * 
	 * @param ocspRepository the OCSP repository component used by this OCSP trust
	 *                       linker.
	 */
	public OcspTrustLinker(OcspRepository ocspRepository) {
		this.ocspRepository = ocspRepository;
	}

	/**
	 * Sets the OCSP response freshness interval in milliseconds. This interval is
	 * used to determine whether an OCSP response can be considered fresh enough to
	 * use as basis for linking trust between child certificate and parent
	 * certificate.
	 * 
	 * @param freshnessInterval
	 */
	public void setFreshnessInterval(long freshnessInterval) {
		this.freshnessInterval = freshnessInterval;
	}

	@Override
	public TrustLinkerResult hasTrustLink(X509Certificate childCertificate, X509Certificate certificate,
			Date validationDate, RevocationData revocationData, AlgorithmPolicy algorithmPolicy)
			throws TrustLinkerResultException, Exception {
		URI ocspUri = getOcspUri(childCertificate);
		if (null == ocspUri) {
			LOGGER.debug("no OCSP URI");
			LOGGER.debug("certificate: {}", childCertificate);
			// allow finding OCSPResp in OCSP repository, even without explicit URI.
			// return TrustLinkerResult.UNDECIDED;
		}
		LOGGER.debug("OCSP URI: {}", ocspUri);

		OCSPResp ocspResp = this.ocspRepository.findOcspResponse(ocspUri, childCertificate, certificate,
				validationDate);
		if (null == ocspResp) {
			LOGGER.debug("OCSP response not found");
			return TrustLinkerResult.UNDECIDED;
		}

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

		Object responseObject = ocspResp.getResponseObject();
		BasicOCSPResp basicOCSPResp = (BasicOCSPResp) responseObject;

		X509CertificateHolder[] responseCertificates = basicOCSPResp.getCerts();
		for (X509CertificateHolder responseCertificate : responseCertificates) {
			LOGGER.debug("OCSP response cert: {}", responseCertificate.getSubject());
			LOGGER.debug("OCSP response cert issuer: {}", responseCertificate.getIssuer());
		}

		algorithmPolicy.checkSignatureAlgorithm(basicOCSPResp.getSignatureAlgOID().getId(), validationDate);

		if (0 == responseCertificates.length) {
			/*
			 * This means that the OCSP response has been signed by the issuing CA itself.
			 */
			ContentVerifierProvider contentVerifierProvider = new JcaContentVerifierProviderBuilder()
					.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(certificate.getPublicKey());
			boolean verificationResult = basicOCSPResp.isSignatureValid(contentVerifierProvider);
			if (false == verificationResult) {
				LOGGER.warn("OCSP response signature invalid");
				return TrustLinkerResult.UNDECIDED;
			}
		} else {
			/*
			 * We're dealing with a dedicated authorized OCSP Responder certificate, or of
			 * course with a CA that issues the OCSP Responses itself.
			 */

			X509CertificateHolder ocspResponderCertificate = responseCertificates[0];
			ContentVerifierProvider contentVerifierProvider = new JcaContentVerifierProviderBuilder()
					.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(ocspResponderCertificate);

			boolean verificationResult = basicOCSPResp.isSignatureValid(contentVerifierProvider);
			if (false == verificationResult) {
				LOGGER.debug("OCSP Responser response signature invalid");
				return TrustLinkerResult.UNDECIDED;
			}
			if (false == Arrays.equals(certificate.getEncoded(), ocspResponderCertificate.getEncoded())) {
				// check certificate signature algorithm
				algorithmPolicy.checkSignatureAlgorithm(
						ocspResponderCertificate.getSignatureAlgorithm().getAlgorithm().getId(), validationDate);

				X509Certificate issuingCaCertificate;
				if (responseCertificates.length < 2) {
					// so the OCSP certificate chain only contains a single
					// entry
					LOGGER.debug("OCSP responder complete certificate chain missing");
					/*
					 * Here we assume that the OCSP Responder is directly signed by the CA.
					 */
					issuingCaCertificate = certificate;
				} else {
					CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
					issuingCaCertificate = (X509Certificate) certificateFactory
							.generateCertificate(new ByteArrayInputStream(responseCertificates[1].getEncoded()));
					/*
					 * Is next check really required?
					 */
					if (false == certificate.equals(issuingCaCertificate)) {
						LOGGER.debug("OCSP responder certificate not issued by CA");
						return TrustLinkerResult.UNDECIDED;
					}
				}
				// check certificate signature
				algorithmPolicy.checkSignatureAlgorithm(issuingCaCertificate.getSigAlgOID(), validationDate);

				PublicKeyTrustLinker publicKeyTrustLinker = new PublicKeyTrustLinker();
				CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
				X509Certificate x509OcspResponderCertificate = (X509Certificate) certificateFactory
						.generateCertificate(new ByteArrayInputStream(ocspResponderCertificate.getEncoded()));
				LOGGER.debug("OCSP Responder public key fingerprint: {}",
						DigestUtils.sha1Hex(x509OcspResponderCertificate.getPublicKey().getEncoded()));
				publicKeyTrustLinker.hasTrustLink(x509OcspResponderCertificate, issuingCaCertificate, validationDate,
						revocationData, algorithmPolicy);
				if (null == x509OcspResponderCertificate
						.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId())) {
					LOGGER.debug("OCSP Responder certificate should have id-pkix-ocsp-nocheck");
					/*
					 * TODO: perform CRL validation on the OCSP Responder certificate. On the other
					 * hand, do we really want to check the checker?
					 */
					return TrustLinkerResult.UNDECIDED;
				}
				List<String> extendedKeyUsage = x509OcspResponderCertificate.getExtendedKeyUsage();
				if (null == extendedKeyUsage) {
					LOGGER.debug("OCSP Responder certificate has no extended key usage extension");
					return TrustLinkerResult.UNDECIDED;
				}
				if (false == extendedKeyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.getId())) {
					LOGGER.debug("OCSP Responder certificate should have a OCSPSigning extended key usage");
					return TrustLinkerResult.UNDECIDED;
				}
			} else {
				LOGGER.debug("OCSP Responder certificate equals the CA certificate");
				// and the CA certificate is already trusted at this point
			}
		}

		DigestCalculatorProvider digCalcProv = new JcaDigestCalculatorProviderBuilder()
				.setProvider(BouncyCastleProvider.PROVIDER_NAME).build();
		CertificateID certificateId = new CertificateID(digCalcProv.get(CertificateID.HASH_SHA1),
				new JcaX509CertificateHolder(certificate), childCertificate.getSerialNumber());

		SingleResp[] singleResps = basicOCSPResp.getResponses();
		for (SingleResp singleResp : singleResps) {
			CertificateID responseCertificateId = singleResp.getCertID();
			if (false == certificateId.equals(responseCertificateId)) {
				continue;
			}
			LocalDateTime thisUpdate = singleResp.getThisUpdate().toInstant().atZone(ZoneId.systemDefault())
					.toLocalDateTime();
			LocalDateTime nextUpdate;
			if (null != singleResp.getNextUpdate()) {
				nextUpdate = singleResp.getNextUpdate().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
			} else {
				LOGGER.debug("no OCSP nextUpdate");
				nextUpdate = thisUpdate;
			}
			LOGGER.debug("OCSP thisUpdate: {}", thisUpdate);
			LOGGER.debug("(OCSP) nextUpdate: {}", nextUpdate);
			LOGGER.debug("validation date: {}", validationDate);
			LocalDateTime beginValidity = thisUpdate.minus(this.freshnessInterval, ChronoUnit.MILLIS);
			LocalDateTime endValidity = nextUpdate.plus(this.freshnessInterval, ChronoUnit.MILLIS);
			LocalDateTime validationDateTime = validationDate.toInstant().atZone(ZoneId.systemDefault())
					.toLocalDateTime();
			if (validationDateTime.isBefore(beginValidity)) {
				LOGGER.warn("OCSP response not yet valid");
				continue;
			}
			if (validationDateTime.isAfter(endValidity)) {
				LOGGER.warn("OCSP response expired");
				continue;
			}
			if (null == singleResp.getCertStatus()) {
				LOGGER.debug("OCSP OK for: {}", childCertificate.getSubjectX500Principal());
				addRevocationData(revocationData, ocspResp, ocspUri);
				return TrustLinkerResult.TRUSTED;
			} else {
				LOGGER.debug("OCSP certificate status: {}", singleResp.getCertStatus().getClass().getName());
				if (singleResp.getCertStatus() instanceof RevokedStatus) {
					LOGGER.debug("OCSP status revoked");
				}
				addRevocationData(revocationData, ocspResp, ocspUri);
				throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_REVOCATION_STATUS,
						"certificate revoked by OCSP");
			}
		}

		LOGGER.warn("no matching OCSP response entry");
		return TrustLinkerResult.UNDECIDED;
	}

	private void addRevocationData(RevocationData revocationData, OCSPResp ocspResp, URI uri) throws IOException {
		if (null == revocationData) {
			return;
		}
		OCSPRevocationData ocspRevocationData = new OCSPRevocationData(ocspResp.getEncoded(), uri.toString());
		revocationData.getOcspRevocationData().add(ocspRevocationData);
	}

	private URI getOcspUri(X509Certificate certificate) throws IOException, URISyntaxException {
		URI ocspURI = getAccessLocation(certificate, X509ObjectIdentifiers.ocspAccessMethod);
		return ocspURI;
	}

	private URI getAccessLocation(X509Certificate certificate, ASN1ObjectIdentifier accessMethod)
			throws IOException, URISyntaxException {
		byte[] authInfoAccessExtensionValue = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
		if (null == authInfoAccessExtensionValue) {
			return null;
		}
		AuthorityInformationAccess authorityInformationAccess;
		DEROctetString oct = (DEROctetString) (new ASN1InputStream(
				new ByteArrayInputStream(authInfoAccessExtensionValue)).readObject());
		authorityInformationAccess = AuthorityInformationAccess
				.getInstance(new ASN1InputStream(oct.getOctets()).readObject());
		AccessDescription[] accessDescriptions = authorityInformationAccess.getAccessDescriptions();
		for (AccessDescription accessDescription : accessDescriptions) {
			LOGGER.debug("access method: {}", accessDescription.getAccessMethod());
			boolean correctAccessMethod = accessDescription.getAccessMethod().equals(accessMethod);
			if (!correctAccessMethod) {
				continue;
			}
			GeneralName gn = accessDescription.getAccessLocation();
			if (gn.getTagNo() != GeneralName.uniformResourceIdentifier) {
				LOGGER.debug("not a uniform resource identifier");
				continue;
			}
			DERIA5String str = DERIA5String.getInstance(gn.getName());
			String accessLocation = str.getString();
			LOGGER.debug("OCSP access location: {}", accessLocation);
			URI uri = toURI(accessLocation);
			return uri;
		}
		return null;
	}

	private URI toURI(String str) throws URISyntaxException {
		URI uri = new URI(str);
		return uri;
	}
}