TrustValidator.java

/*
 * Java Trust Project.
 * Copyright (C) 2009 FedICT.
 * Copyright (C) 2014-2021 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;

import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import be.fedict.trust.constraints.CertificateConstraint;
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.policy.DefaultAlgorithmPolicy;
import be.fedict.trust.repository.CertificateRepository;
import be.fedict.trust.revocation.RevocationData;

/**
 * Trust Validator.
 * <p>
 * Notice that this component is not thread-safe as it is using internal state.
 * </p>
 * 
 * @author Frank Cornelis
 * 
 */
public class TrustValidator {

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

	private final CertificateRepository certificateRepository;

	private final List<TrustLinker> trustLinkers;

	private final List<CertificateConstraint> certificateConstraints;

	private RevocationData revocationData;

	private AlgorithmPolicy algorithmPolicy;

	/**
	 * Main constructor.
	 * 
	 * @param certificateRepository the certificate repository used by this trust
	 *                              validator.
	 */
	public TrustValidator(CertificateRepository certificateRepository) {
		this(certificateRepository, null);
	}

	/**
	 * Main constructor.
	 * 
	 * @param certificateRepository the certificate repository used by this trust
	 *                              validator.
	 * @param revocationData        optional {@link RevocationData} object. If not
	 *                              <code>null</code> the added
	 *                              {@link TrustLinker}'s should fill it up with
	 *                              used revocation data.
	 */
	public TrustValidator(CertificateRepository certificateRepository, RevocationData revocationData) {
		this.certificateRepository = certificateRepository;
		this.trustLinkers = new LinkedList<>();
		this.certificateConstraints = new LinkedList<>();
		this.revocationData = revocationData;
		this.algorithmPolicy = new DefaultAlgorithmPolicy();
	}

	/**
	 * Adds a trust linker to this trust validator. The order in which trust linkers
	 * are added determine the runtime behavior of the trust validator.
	 * 
	 * @param trustLinker the trust linker component.
	 */
	public void addTrustLinker(TrustLinker trustLinker) {
		this.trustLinkers.add(trustLinker);
	}

	/**
	 * Sets the algorithm policy to be used when validation used signature
	 * algorithms.
	 * 
	 * @param algorithmPolicy the algorithm policy component.
	 */
	public void setAlgorithmPolicy(AlgorithmPolicy algorithmPolicy) {
		this.algorithmPolicy = algorithmPolicy;
	}

	/**
	 * Adds a certificate constraint to this trust validator. Keep this typo-version
	 * of addCertificateContrainT for downwards compatibility.
	 * 
	 * @param certificateConstraint the certificate constraint component.
	 * @deprecated
	 * @see TrustValidator#addCertificateConstraint(CertificateConstraint)
	 */
	public void addCertificateConstrain(CertificateConstraint certificateConstraint) {
		this.certificateConstraints.add(certificateConstraint);
	}

	/**
	 * Adds a certificate constraint to this trust validator.
	 *
	 * @param certificateConstraint the certificate constraint component.
	 */
	public void addCertificateConstraint(CertificateConstraint certificateConstraint) {
		this.certificateConstraints.add(certificateConstraint);
	}

	/**
	 * Validates whether the given certificate path is valid according to the
	 * configured trust linkers.
	 * 
	 * @param certificatePath the X509 certificate path to validate.
	 * @throws TrustLinkerResultException in case the certificate path is invalid.
	 * @see #isTrusted(List, Date)
	 */
	public void isTrusted(List<X509Certificate> certificatePath) throws TrustLinkerResultException {
		isTrusted(certificatePath, new Date());
	}

	/**
	 * Validates whether the given certificate path is valid according to the
	 * configured trust linkers.
	 * 
	 * @param certificatePath the X509 certificate path to validate.
	 * @param expiredMode     set to <code>true</code> for validation mode of
	 *                        expired certificates.
	 * @throws TrustLinkerResultException in case the certificate path is invalid.
	 * @see #isTrusted(List, Date)
	 */
	public void isTrusted(List<X509Certificate> certificatePath, boolean expiredMode)
			throws TrustLinkerResultException {
		isTrusted(certificatePath, new Date(), expiredMode);
	}

	/**
	 * Validates whether the given certificate path is valid according to the
	 * configured trust linkers. Convenience method when loading a certificate chain
	 * directly from a JCA key store implementation.
	 * 
	 * @param certificates
	 * @throws be.fedict.trust.linker.TrustLinkerResultException
	 */
	public void isTrusted(Certificate[] certificates) throws TrustLinkerResultException {
		List<X509Certificate> certificateChain = new LinkedList<>();
		for (Certificate certificate : certificates) {
			X509Certificate x509Certificate = (X509Certificate) certificate;
			certificateChain.add(x509Certificate);
		}
		isTrusted(certificateChain);
	}

	/**
	 * Returns the {@link RevocationData} returned by the configured
	 * {@link TrustLinker}'s if a {@link RevocationData} object was specified in the
	 * constructor.
	 * 
	 * @return {@link RevocationData}
	 */
	public RevocationData getRevocationData() {

		return this.revocationData;
	}

	/**
	 * Checks whether the given certificate is self-signed.
	 * 
	 * @param certificate the X509 certificate.
	 */
	public static void isSelfSigned(X509Certificate certificate) throws TrustLinkerResultException {
		checkSelfSigned(certificate);
	}

	/**
	 * Gives back the trust linker result of a verification of a self-signed X509
	 * certificate.
	 * 
	 * @param certificate the self-signed certificate to validate.
	 */
	public static void checkSelfSigned(X509Certificate certificate) throws TrustLinkerResultException {
		if (false == certificate.getIssuerX500Principal().equals(certificate.getSubjectX500Principal())) {
			throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST,
					"root certificate should be self-signed: " + certificate.getSubjectX500Principal());
		}
		try {
			certificate.verify(certificate.getPublicKey());
		} catch (Exception e) {
			throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_SIGNATURE,
					"certificate signature error: " + e.getMessage());
		}
	}

	private void checkSignatureAlgorithm(String signatureAlgorithm, Date validationDate)
			throws TrustLinkerResultException {
		try {
			this.algorithmPolicy.checkSignatureAlgorithm(signatureAlgorithm, validationDate);
		} catch (TrustLinkerResultException e) {
			// re-wrapping this type of exception doesn't bring anything
			throw e;
		} catch (Exception e) {
			throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_ALGORITHM,
					"Invalid signature algorithm: " + signatureAlgorithm);
		}
	}

	/**
	 * Validates whether the certificate path was valid at the given validation
	 * date.
	 * 
	 * @param certificatePath the X509 certificate path to be validated.
	 * @param validationDate  the date at which the certificate path validation
	 *                        should be verified.
	 * @see #isTrusted(List)
	 */
	public void isTrusted(List<X509Certificate> certificatePath, Date validationDate)
			throws TrustLinkerResultException {
		isTrusted(certificatePath, validationDate, false);
	}

	/**
	 * Validates whether the certificate path was valid at the given validation
	 * date.
	 * 
	 * @param certificatePath the X509 certificate path to be validated.
	 * @param validationDate  the date at which the certificate path validation
	 *                        should be verified.
	 * @see #isTrusted(List)
	 */
	public void isTrusted(List<X509Certificate> certificatePath, Instant validationDate)
			throws TrustLinkerResultException {
		isTrusted(certificatePath, Date.from(validationDate), false);
	}

	/**
	 * Validates whether the certificate path was valid at the given validation
	 * date.
	 * 
	 * @param certificatePath the X509 certificate path to be validated.
	 * @param validationDate  the date at which the certificate path validation
	 *                        should be verified.
	 * @see #isTrusted(List)
	 */
	public void isTrusted(List<X509Certificate> certificatePath, LocalDateTime validationDate)
			throws TrustLinkerResultException {
		isTrusted(certificatePath, Date.from(validationDate.atZone(ZoneId.systemDefault()).toInstant()), false);
	}

	/**
	 * Validates whether the certificate path was valid at the given validation
	 * date.
	 * 
	 * @param certificatePath the X509 certificate path to be validated.
	 * @param validationDate  the date at which the certificate path validation
	 *                        should be verified.
	 * @param expiredMode     set to <code>true</code> for validation mode of
	 *                        expired certificates.
	 * @see #isTrusted(List)
	 */
	public void isTrusted(List<X509Certificate> certificatePath, Date validationDate, boolean expiredMode)
			throws TrustLinkerResultException {
		if (certificatePath.isEmpty()) {
			throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED, "certificate path is empty");
		}
		for (X509Certificate certificate : certificatePath) {
			if (null == certificate) {
				throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED,
						"certificate path contains null certificate");
			}
		}
		if (expiredMode) {
			LOGGER.debug("expired certificate validation mode");
		}

		int certIdx = certificatePath.size() - 1;
		X509Certificate certificate = certificatePath.get(certIdx);
		LOGGER.debug("verifying root certificate: {}", certificate.getSubjectX500Principal());
		checkSelfSigned(certificate);
		// check certificate signature
		checkSignatureAlgorithm(certificate.getSigAlgName(), validationDate);
		checkSelfSignedTrust(certificate, validationDate, expiredMode);

		certIdx--;

		while (certIdx >= 0) {
			X509Certificate childCertificate = certificatePath.get(certIdx);
			LOGGER.debug("verifying certificate: {}", childCertificate.getSubjectX500Principal());
			certIdx--;
			checkTrustLink(childCertificate, certificate, validationDate);
			certificate = childCertificate;
		}

		for (CertificateConstraint certificateConstraint : this.certificateConstraints) {
			String certificateConstraintName = certificateConstraint.getClass().getSimpleName();
			LOGGER.debug("certificate constraint check: {}", certificateConstraintName);
			try {
				certificateConstraint.check(certificate);
			} catch (TrustLinkerResultException e) {
				// let this specific type of exception pass as is
				throw e;
			} catch (Exception e) {
				throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED,
						"certificate constraint error " + certificateConstraintName + ": " + e.getMessage(), e);
			}
		}
	}

	private void checkTrustLink(X509Certificate childCertificate, X509Certificate certificate, Date validationDate)
			throws TrustLinkerResultException {
		if (null == childCertificate) {
			return;
		}
		// check certificate signature
		checkSignatureAlgorithm(childCertificate.getSigAlgName(), validationDate);

		boolean sometrustLinkerTrusts = false;
		for (TrustLinker trustLinker : this.trustLinkers) {
			LOGGER.debug("trying trust linker: {}", trustLinker.getClass().getSimpleName());
			TrustLinkerResult trustLinkerResult;
			try {
				trustLinkerResult = trustLinker.hasTrustLink(childCertificate, certificate, validationDate,
						this.revocationData, this.algorithmPolicy);
			} catch (TrustLinkerResultException e) {
				// we let this type of exception pass as is
				LOGGER.warn("trust linker exception: " + e.getMessage(), e);
				throw e;
			} catch (Exception e) {
				LOGGER.warn("trust linker error: " + e.getMessage(), e);
				throw new TrustLinkerResultException(TrustLinkerResultReason.UNSPECIFIED,
						"trust linker error: " + e.getMessage(), e);
			}
			if (null == trustLinkerResult) {
				LOGGER.warn("trust linker result should not be NULL");
			}
			if (TrustLinkerResult.TRUSTED == trustLinkerResult) {
				// we don't break as there still might be a trust linker that
				// complains
				sometrustLinkerTrusts = true;
			}
		}
		if (false == sometrustLinkerTrusts) {
			String message = "no trust between " + childCertificate.getSubjectX500Principal() + " and "
					+ certificate.getSubjectX500Principal();
			LOGGER.warn(message);
			throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST, message);
		}
	}

	private void checkSelfSignedTrust(X509Certificate certificate, Date validationDate, boolean expiredMode)
			throws TrustLinkerResultException {
		if (certificate.getNotBefore().after(validationDate)) {
			LOGGER.error("certificate not yet valid");
			LOGGER.error("validation date: {}", validationDate);
			LOGGER.error("not before: {}", certificate.getNotBefore());
			LOGGER.error("not after: {}", certificate.getNotAfter());
			throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_VALIDITY_INTERVAL,
					"certificate not yet valid");
		}
		if (certificate.getNotAfter().before(validationDate)) {
			if (!expiredMode) {
				LOGGER.error("certificate expired");
				LOGGER.error("validation date: {}", validationDate);
				LOGGER.error("not before: {}", certificate.getNotBefore());
				LOGGER.error("not after: {}", certificate.getNotAfter());
				throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_VALIDITY_INTERVAL,
						"certificate expired");
			} else {
				LOGGER.warn("certificate expired");
				LOGGER.warn("validation date: {}", validationDate);
				LOGGER.warn("not before: {}", certificate.getNotBefore());
				LOGGER.warn("not after: {}", certificate.getNotAfter());
			}
		}
		if (this.certificateRepository.isTrustPoint(certificate)) {
			return;
		}
		LOGGER.warn("self-signed certificate not in repository: {}", certificate.getSubjectX500Principal());
		throw new TrustLinkerResultException(TrustLinkerResultReason.ROOT,
				"self-signed certificate not in repository: " + certificate.getSubjectX500Principal());
	}

	/**
	 * Sets the revocation data container used by this trust validator while
	 * validating certificate chains.
	 * 
	 * @param revocationData
	 */
	public void setRevocationData(RevocationData revocationData) {
		this.revocationData = revocationData;
	}
}