PublicKeyTrustLinker.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.linker;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import be.fedict.trust.policy.AlgorithmPolicy;
import be.fedict.trust.revocation.RevocationData;

/**
 * Public key trust linker implementation. Performs simple sanity checks based
 * on the public keys.
 * 
 * @author Frank Cornelis
 */
public class PublicKeyTrustLinker implements TrustLinker {

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

	private final boolean expiredMode;

	public PublicKeyTrustLinker() {
		this(false);
	}

	public PublicKeyTrustLinker(boolean expiredMode) {
		this.expiredMode = expiredMode;
	}

	@Override
	public TrustLinkerResult hasTrustLink(X509Certificate childCertificate, X509Certificate certificate,
			Date validationDate, RevocationData revocationData, AlgorithmPolicy algorithmPolicy)
			throws TrustLinkerResultException, Exception {
		if (false == childCertificate.getIssuerX500Principal().equals(certificate.getSubjectX500Principal())) {
			LOGGER.warn("child certificate issuer not the same as the issuer certificate subject");
			LOGGER.warn("child certificate: {}", childCertificate.getSubjectX500Principal());
			LOGGER.warn("certificate: {}", certificate.getSubjectX500Principal());
			LOGGER.warn("child certificate issuer: {}", childCertificate.getIssuerX500Principal());
			throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST,
					"child certificate issuer not the same as the issuer certificate subject");
		}
		try {
			childCertificate.verify(certificate.getPublicKey());
		} catch (Exception e) {
			LOGGER.debug("verification error: " + e.getMessage(), e);
			throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_SIGNATURE,
					"verification error: " + e.getMessage());
		}

		algorithmPolicy.checkSignatureAlgorithm(childCertificate.getSigAlgOID(), validationDate);

		if (true == childCertificate.getNotAfter().after(certificate.getNotAfter())) {
			LOGGER.warn("child certificate validity end is after issuing certificate validity end");
			LOGGER.warn("child certificate validity end: {}", childCertificate.getNotAfter());
			LOGGER.warn("issuing certificate validity end: {}", certificate.getNotAfter());
		}
		if (true == childCertificate.getNotBefore().before(certificate.getNotBefore())) {
			LOGGER.warn("child certificate validity begin before issuing certificate validity begin");
			LOGGER.warn("child certificate validity begin: {}", childCertificate.getNotBefore());
			LOGGER.warn("issuing certificate validity begin: {}", certificate.getNotBefore());
		}
		if (true == validationDate.before(childCertificate.getNotBefore())) {
			LOGGER.debug("certificate is not yet valid");
			LOGGER.debug("validation date: {}", validationDate);
			LOGGER.debug("not before: {}", childCertificate.getNotBefore());
			throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_VALIDITY_INTERVAL,
					"certificate is not yet valid");
		}
		if (true == validationDate.after(childCertificate.getNotAfter())) {
			LOGGER.debug("certificate already expired");
			LOGGER.debug("validation date: {}", validationDate);
			LOGGER.debug("not after: {}", childCertificate.getNotAfter());
			if (!this.expiredMode) {
				throw new TrustLinkerResultException(TrustLinkerResultReason.INVALID_VALIDITY_INTERVAL,
						"certificate already expired");
			} else {
				LOGGER.debug("running in expired mode");
			}
		}
		if (-1 == certificate.getBasicConstraints()) {
			LOGGER.warn("certificate not a CA: {}", certificate.getSubjectX500Principal());
			/*
			 * http://www.valicert.com/ Root CA has no CA flag set. Actually this is in
			 * violation with 4.2.1.10 Basic Constraints of RFC2459.
			 */
			try {
				certificate.verify(certificate.getPublicKey());
				LOGGER.warn("allowing self-signed Root CA without CA flag set");
			} catch (Exception e) {
				throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST, "certificate not a CA");
			}
		}
		if (0 == certificate.getBasicConstraints() && -1 != childCertificate.getBasicConstraints()) {
			LOGGER.warn("child should not be a CA: " + childCertificate.getSubjectX500Principal());
			throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST, "child should not be a CA");
		}

		/*
		 * SKID/AKID sanity check
		 */
		boolean isCa = isCa(certificate);
		boolean isChildCa = isCa(childCertificate);

		byte[] subjectKeyIdentifierData = certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId());
		byte[] authorityKeyIdentifierData = childCertificate
				.getExtensionValue(Extension.authorityKeyIdentifier.getId());

		if (isCa && null == subjectKeyIdentifierData) {
			LOGGER.error("certificate is CA and MUST contain a Subject Key Identifier");
			throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST,
					"certificate is CA and  MUST contain a Subject Key Identifier");
		}

		if (isChildCa && null == authorityKeyIdentifierData && null != subjectKeyIdentifierData) {
			LOGGER.error("child certificate is CA and MUST contain an Authority Key Identifier");
			// return new TrustLinkerResult(false,
			// TrustLinkerResultReason.INVALID_TRUST,
			// "child certificate is CA and MUST contain an Authority Key Identifier");
		}

		if (null != subjectKeyIdentifierData && null != authorityKeyIdentifierData) {
			AuthorityKeyIdentifier authorityKeyIdentifier = AuthorityKeyIdentifier
					.getInstance(JcaX509ExtensionUtils.parseExtensionValue(authorityKeyIdentifierData));
			SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier
					.getInstance(JcaX509ExtensionUtils.parseExtensionValue(subjectKeyIdentifierData));
			if (!Arrays.equals(authorityKeyIdentifier.getKeyIdentifier(), subjectKeyIdentifier.getKeyIdentifier())) {
				LOGGER.error(
						"certificate's subject key identifier does not match child certificate's authority key identifier");
				throw new TrustLinkerResultException(TrustLinkerResultReason.NO_TRUST,
						"certificate's subject key identifier does not match child certificate's authority key identifier");
			}
		}

		/*
		 * We don't check pathLenConstraint since this one is only there to protect the
		 * PKI business.
		 */
		/*
		 * Keep in mind that this trust linker can never return TRUSTED.
		 */
		return TrustLinkerResult.UNDECIDED;
	}

	private boolean isCa(X509Certificate certificate) {
		byte[] basicConstraintsValue = certificate.getExtensionValue(Extension.basicConstraints.getId());
		if (null == basicConstraintsValue) {
			return false;
		}

		ASN1Encodable basicConstraintsDecoded;
		try {
			basicConstraintsDecoded = JcaX509ExtensionUtils.parseExtensionValue(basicConstraintsValue);
		} catch (IOException e) {
			LOGGER.error("IO error", e);
			return false;
		}
		if (false == basicConstraintsDecoded instanceof ASN1Sequence) {
			LOGGER.debug("basic constraints extension is not an ASN1 sequence");
			return false;
		}
		ASN1Sequence basicConstraintsSequence = (ASN1Sequence) basicConstraintsDecoded;
		BasicConstraints basicConstraints = BasicConstraints.getInstance(basicConstraintsSequence);
		return basicConstraints.isCA();
	}
}