CrlTrustLinker.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.crl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidParameterException;
import java.security.cert.CRLException;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Date;

import org.bouncycastle.asn1.ASN1Enumerated;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.IssuingDistributionPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.CRLRevocationData;
import be.fedict.trust.revocation.RevocationData;
import org.bouncycastle.asn1.DERIA5String;

/**
 * Trust linker implementation based on CRL revocation information.
 * 
 * @author Frank Cornelis
 * 
 */
public class CrlTrustLinker implements TrustLinker {

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

	private final CrlRepository crlRepository;

	/**
	 * Main constructor.
	 * 
	 * @param crlRepository the CRL repository used by this CRL trust linker.
	 */
	public CrlTrustLinker(CrlRepository crlRepository) {
		this.crlRepository = crlRepository;
	}

	@Override
	public TrustLinkerResult hasTrustLink(X509Certificate childCertificate, X509Certificate certificate,
			Date validationDate, RevocationData revocationData, AlgorithmPolicy algorithmPolicy)
			throws TrustLinkerResultException, Exception {

		URI crlUri = getCrlUri(childCertificate);
		if (null == crlUri) {
			LOGGER.debug("no CRL uri in certificate: {}", childCertificate.getSubjectX500Principal());
			return TrustLinkerResult.UNDECIDED;
		}

		LOGGER.debug("CRL URI: {}", crlUri);
		X509CRL x509crl = this.crlRepository.findCrl(crlUri, certificate, validationDate);
		if (null == x509crl) {
			LOGGER.debug("CRL not found");
			return TrustLinkerResult.UNDECIDED;
		}

		// check CRL integrity
		boolean crlIntegrityResult = checkCrlIntegrity(x509crl, certificate, validationDate);
		if (false == crlIntegrityResult) {
			LOGGER.debug("CRL integrity check failed");
			return TrustLinkerResult.UNDECIDED;
		}

		// check CRL signature algorithm
		algorithmPolicy.checkSignatureAlgorithm(x509crl.getSigAlgOID(), validationDate);

		// we don't support indirect CRLs
		if (isIndirectCRL(x509crl)) {
			LOGGER.debug("indirect CRL detected");
			return TrustLinkerResult.UNDECIDED;
		}

		LOGGER.debug("CRL number: {}", getCrlNumber(x509crl));

		// fill up revocation data if not null with this valid CRL
		if (null != revocationData) {
			try {
				CRLRevocationData crlRevocationData = new CRLRevocationData(x509crl.getEncoded(), crlUri.toString());
				revocationData.getCrlRevocationData().add(crlRevocationData);
			} catch (CRLException e) {
				LOGGER.error("CRLException: " + e.getMessage(), e);
				throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED,
						"CRLException : " + e.getMessage(), e);
			}
		}

		X509CRLEntry crlEntry = x509crl.getRevokedCertificate(childCertificate.getSerialNumber());
		if (null == crlEntry) {
			LOGGER.debug("CRL OK for: {}", childCertificate.getSubjectX500Principal());
			return TrustLinkerResult.TRUSTED;
		} else if (crlEntry.getRevocationDate().after(validationDate)) {
			LOGGER.debug("CRL OK for: {} at {}", childCertificate.getSubjectX500Principal(), validationDate);
			LOGGER.debug("CRL entry revocation date: {}", crlEntry.getRevocationDate());
			return TrustLinkerResult.TRUSTED;
		}

		LOGGER.debug("certificate revoked/suspended at: {}", crlEntry.getRevocationDate());
		if (crlEntry.hasExtensions()) {
			LOGGER.debug("critical extensions: {}", crlEntry.getCriticalExtensionOIDs());
			LOGGER.debug("non-critical extensions: {}", crlEntry.getNonCriticalExtensionOIDs());
			byte[] reasonCodeExtension = crlEntry.getExtensionValue(Extension.reasonCode.getId());
			if (null != reasonCodeExtension) {
				try {
					DEROctetString octetString = (DEROctetString) (new ASN1InputStream(
							new ByteArrayInputStream(reasonCodeExtension)).readObject());
					byte[] octets = octetString.getOctets();
					CRLReason crlReason = CRLReason
							.getInstance(ASN1Enumerated.getInstance(new ASN1InputStream(octets).readObject()));
					BigInteger crlReasonValue = crlReason.getValue();
					LOGGER.debug("CRL reason value: {}", crlReasonValue);
					switch (crlReasonValue.intValue()) {
					case CRLReason.certificateHold:
						throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_REVOCATION_STATUS,
								"certificate suspended by CRL=" + crlEntry.getSerialNumber());
					}
				} catch (IOException e) {
					throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED,
							"IO error: " + e.getMessage(), e);
				}
			}
		}

		throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_REVOCATION_STATUS,
				"certificate revoked by CRL=" + crlEntry.getSerialNumber());

	}

	/**
	 * Checks the integrity of the given X509 CRL.
	 * 
	 * @param x509crl           the X509 CRL to verify the integrity.
	 * @param issuerCertificate the assumed issuer of the given X509 CRL.
	 * @param validationDate    the validate date.
	 * @return <code>true</code> if integrity is OK, <code>false</code> otherwise.
	 */
	public static boolean checkCrlIntegrity(X509CRL x509crl, X509Certificate issuerCertificate, Date validationDate) {
		if (null == x509crl) {
			throw new IllegalArgumentException("CRL is null");
		}
		if (null == issuerCertificate) {
			throw new IllegalArgumentException("issuer certificate is null");
		}
		if (null == validationDate) {
			throw new IllegalArgumentException("validation date is null");
		}

		LOGGER.debug("CRL number: {}", getCrlNumber(x509crl));
		LOGGER.debug("CRL issuer: {}", x509crl.getIssuerX500Principal());
		LOGGER.debug("issuer certificate: {}", issuerCertificate.getSubjectX500Principal());
		if (false == x509crl.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) {
			LOGGER.warn("CRL issuer mismatch with certificate issuer");
			return false;
		}
		try {
			x509crl.verify(issuerCertificate.getPublicKey());
		} catch (Exception e) {
			LOGGER.warn("CRL signature verification failed");
			LOGGER.warn("exception: " + e.getMessage(), e);
			return false;
		}
		Date thisUpdate = x509crl.getThisUpdate();
		LOGGER.debug("validation date: {}", validationDate);
		LOGGER.debug("CRL this update: {}", thisUpdate);
		if (thisUpdate.after(validationDate)) {
			LOGGER.warn("CRL too young");
			return false;
		}
		LOGGER.debug("CRL next update: {}", x509crl.getNextUpdate());
		if (null != x509crl.getNextUpdate()) {
			if (validationDate.after(x509crl.getNextUpdate())) {
				LOGGER.debug("CRL too old");
				return false;
			}
		} else {
			LOGGER.warn("CRL has no nextUpdate");
		}

		// assert cRLSign KeyUsage bit
		if (null == issuerCertificate.getKeyUsage()) {
			LOGGER.warn("No KeyUsage extension for CRL issuing certificate");
			/*
			 * Not really required according to RFC2459.
			 */
			return true;
		}

		if (false == issuerCertificate.getKeyUsage()[6]) {
			LOGGER.debug("cRLSign bit not set for CRL issuing certificate");
			return false;
		}

		return true;
	}

	/**
	 * Gives back the CRL URI meta-data found within the given X509 certificate.
	 * 
	 * @param certificate the X509 certificate.
	 * @return the CRL URI, or <code>null</code> if the extension is not present.
	 */
	public static URI getCrlUri(X509Certificate certificate) {
		byte[] crlDistributionPointsValue = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId());
		if (null == crlDistributionPointsValue) {
			return null;
		}
		ASN1Sequence seq;
		try {
			DEROctetString oct;
			oct = (DEROctetString) (new ASN1InputStream(new ByteArrayInputStream(crlDistributionPointsValue))
					.readObject());
			seq = (ASN1Sequence) new ASN1InputStream(oct.getOctets()).readObject();
		} catch (IOException e) {
			throw new RuntimeException("IO error: " + e.getMessage(), e);
		}
		CRLDistPoint distPoint = CRLDistPoint.getInstance(seq);
		DistributionPoint[] distributionPoints = distPoint.getDistributionPoints();
		for (DistributionPoint distributionPoint : distributionPoints) {
			DistributionPointName distributionPointName = distributionPoint.getDistributionPoint();
			if (DistributionPointName.FULL_NAME != distributionPointName.getType()) {
				continue;
			}
			GeneralNames generalNames = (GeneralNames) distributionPointName.getName();
			GeneralName[] names = generalNames.getNames();
			for (GeneralName name : names) {
				if (name.getTagNo() != GeneralName.uniformResourceIdentifier) {
					LOGGER.debug("not a uniform resource identifier");
					continue;
				}
				DERIA5String derStr = DERIA5String.getInstance(name.getName());
				String str = derStr.getString();
				if (false == str.startsWith("http")) {
					/*
					 * skip ldap:// protocols
					 */
					LOGGER.debug("not HTTP/HTTPS: {}", str);
					continue;
				}
				URI uri = toURI(str);
				LOGGER.debug("CRL URI: {}", uri);
				return uri;
			}
		}
		return null;
	}

	/**
	 * Gives back the CRL number of the given X509 CRL.
	 * 
	 * @param crl the X509 CRL.
	 * @return the CRL number, or <code>null</code> if not specified.
	 */
	public static BigInteger getCrlNumber(X509CRL crl) {
		byte[] crlNumberExtensionValue = crl.getExtensionValue(Extension.cRLNumber.getId());
		if (null == crlNumberExtensionValue) {
			return null;
		}
		try {
			ASN1OctetString octetString = (ASN1OctetString) (new ASN1InputStream(
					new ByteArrayInputStream(crlNumberExtensionValue)).readObject());
			byte[] octets = octetString.getOctets();
			ASN1Integer integer = (ASN1Integer) new ASN1InputStream(octets).readObject();
			BigInteger crlNumber = integer.getPositiveValue();
			return crlNumber;
		} catch (IOException e) {
			throw new RuntimeException("IO error: " + e.getMessage(), e);
		}
	}

	private boolean isIndirectCRL(X509CRL crl) {
		byte[] idp = crl.getExtensionValue(Extension.issuingDistributionPoint.getId());
		boolean isIndirect = false;
		if (idp != null) {
			isIndirect = IssuingDistributionPoint.getInstance(idp).isIndirectCRL();
		}

		return isIndirect;
	}

	private static URI toURI(String str) {
		try {
			URI uri = new URI(str);
			return uri;
		} catch (URISyntaxException e) {
			throw new InvalidParameterException("CRL URI syntax error: " + e.getMessage());
		}
	}
}