mitm.common.security.certificate.validator.PKITrustCheckCertificateValidatorImpl.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.certificate.validator.PKITrustCheckCertificateValidatorImpl.java

Source

/*
 * Copyright (c) 2008-2011, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.common.security.certificate.validator;

import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CRLException;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathBuilderResult;
import java.security.cert.Certificate;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import mitm.common.security.SecurityFactoryFactoryException;
import mitm.common.security.certpath.CertificatePathBuilder;
import mitm.common.security.certpath.CertificatePathBuilderFactory;
import mitm.common.security.certstore.CertStoreUtils;
import mitm.common.security.crl.RevocationChecker;
import mitm.common.security.crl.RevocationResult;
import mitm.common.security.crl.RevocationStatus;
import mitm.common.security.ctl.CTL;
import mitm.common.security.ctl.CTLException;
import mitm.common.security.ctl.CTLValidity;
import mitm.common.security.ctl.CTLValidityResult;
import mitm.common.util.Check;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This CertificateValidator is used to check the trust of a certificate (ie. if a valid certificate chain
 * can be build) and that the certificate is not revoked.
 * 
 * This class if not thread safe.
 * 
 * @author Martijn Brinkers
 *
 */
public class PKITrustCheckCertificateValidatorImpl implements PKITrustCheckCertificateValidator {
    private final static Logger logger = LoggerFactory.getLogger(PKITrustCheckCertificateValidatorImpl.class);

    /* 
     * Default acceptable revocation status. This is the default policy for this class. 
     * */
    private final static RevocationStatus[] DEFAULT_ACCEPTABLE_REVOCATION_STATUS = { RevocationStatus.NOT_REVOKED,
            RevocationStatus.UNKNOWN, RevocationStatus.EXPIRED };

    /*
     * Name of this CertificateValidator.
     */
    private final String name;

    /*
     * Used for the creation of Certificate path builders 
     */
    private final CertificatePathBuilderFactory certificatePathBuilderFactory;

    /*
     * Used to check the revocation state (using CRL's) of the certificate.
     */
    private final RevocationChecker revocationChecker;

    /*
     * Certificate Trust List
     */
    private final CTL ctl;

    /*
     * The revocation reasons that are accepted as valid ie. flag the certificate as not revoked.
     */
    private final RevocationStatus[] acceptableRevocationStatus;

    /*
     * Certificates that will be added to the path builder
     */
    private Set<Certificate> additionalCertificates;

    /* 
     * The date used for path building and revocation checking. If not set the current date will be used.
     */
    private Date date;

    /*
     * True if the last result was valid 
     */
    boolean valid;

    /*
     * True if a valid path could be build.
     */
    private boolean trusted;

    /*
     * True if one of the certificates in the chain is considered revoked (based on acceptedRevocationStatus). 
     */
    private boolean revoked;

    /*
     * True if one of the certificates in the chain is black listed 
     */
    private boolean blackListed;

    /*
     * True if the certificate is white listed 
     */
    private boolean whiteListed;

    /*
     * Gives some info about the cause of the failure (should only contain relevant info when the last call
     * to isValid returned false).
     */
    private String failureMessage = "";

    protected PKITrustCheckCertificateValidatorImpl(String name,
            CertificatePathBuilderFactory certificatePathBuilderFactory, RevocationChecker revocationChecker,
            CTL ctl, Collection<? extends Certificate> additionalCertificates) {
        Check.notNull(certificatePathBuilderFactory, "certificatePathBuilderFactory");
        Check.notNull(revocationChecker, "revocationChecker");

        this.name = name;
        this.certificatePathBuilderFactory = certificatePathBuilderFactory;
        this.revocationChecker = revocationChecker;
        this.ctl = ctl;

        this.acceptableRevocationStatus = DEFAULT_ACCEPTABLE_REVOCATION_STATUS;

        if (additionalCertificates != null && additionalCertificates.size() > 0) {
            /*
             * Clone the additionalCertificates
             */
            this.additionalCertificates = new HashSet<Certificate>(additionalCertificates);
        }
    }

    public PKITrustCheckCertificateValidatorImpl(CertificatePathBuilderFactory certificatePathBuilderFactory,
            RevocationChecker revocationChecker, CTL ctl,
            Collection<? extends Certificate> additionalCertificates) {
        this("PKITrustCheckCertificateValidator", certificatePathBuilderFactory, revocationChecker, ctl,
                additionalCertificates);
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * True if the last time isValid(certificate) returned true, false otherwise. The certificate
     * is valid when a complete trusted path can be build up to a trusted root and the 
     * certificate is not revoked.
     */
    @Override
    public boolean isValid() {
        return valid;
    }

    /**
     * True if a path could be build (ie. up to a trusted root). trusted does not mean the certificate is not
     * revoked, it only means that a complete chain could be build from the certificate to a trusted root.  
     */
    @Override
    public boolean isTrusted() {
        return trusted;
    }

    /**
     * True if one of the certificates in the chain is revoked (based on acceptedRevocationStatus). This value is
     * only valid if isTrusted is true. If isTrusted is false isRevoked will always be false. Revocation checking
     * cannot be done when the chain is incomplete. We will therefore return false when the chain in incomplete 
     * because we do not know better.
     */
    @Override
    public boolean isRevoked() {
        return revoked;
    }

    /**
     * True if one of the certificates in the chain is black listed. Blacklist checking is only done when
     * the certificate is valid. 
     */
    @Override
    public boolean isBlackListed() {
        return blackListed;
    }

    /**
     * True if the certificate is whitelisted. A white list check is only done when the certificate is
     * invalid.
     */
    @Override
    public boolean isWhiteListed() {
        return whiteListed;
    }

    @Override
    public String getFailureMessage() {
        return failureMessage;
    }

    /** 
     * Sets the date used for path building and revocation checking. If not set the current date will be used.
     */
    @Override
    public void setDate(Date date) {
        this.date = date;
    }

    private Date getDate() {
        if (date != null) {
            return date;
        }

        return new Date();
    }

    private boolean isBlackListed(CertPath certPath) throws CTLException {
        if (ctl == null) {
            return false;
        }

        List<? extends Certificate> certificates = certPath.getCertificates();

        for (int i = 0; i < certificates.size(); i++) {
            Certificate certificate = certificates.get(i);

            if (!(certificate instanceof X509Certificate)) {
                logger.warn("Only X509Certificates can be black listed.");

                continue;
            }

            CTLValidityResult result = ctl.checkValidity((X509Certificate) certificate);

            if (CTLValidity.INVALID == result.getValidity()) {
                /*
                 * If the certificate is the first it's not an intermediate.
                 */
                String failureMessage = i == 0 ? result.getMessage() : "Intermediate " + result.getMessage();

                reportFailure(failureMessage);

                return true;
            }
        }

        return false;
    }

    private boolean isWhiteListed(X509Certificate certificate) throws CTLException {
        boolean whiteListed = false;

        if (ctl != null) {
            CTLValidityResult result = ctl.checkValidity((X509Certificate) certificate);

            switch (result.getValidity()) {
            case VALID:
                whiteListed = true;
                break;
            case NOT_LISTED:
                break;
            default:
                reportFailure(result.getMessage());
            }
        }

        return whiteListed;
    }

    @Override
    public boolean isValid(Certificate certificate) {
        valid = false;
        trusted = false;
        revoked = false;
        blackListed = false;
        whiteListed = false;

        if (!(certificate instanceof X509Certificate)) {
            failureMessage = "Certificate is not a X509Certificate";

            return false;
        }

        failureMessage = "";

        X509Certificate x509Certificate = (X509Certificate) certificate;

        try {
            CertPathAndAnchor certPathAndAnchor = getCertPathAndAnchor(x509Certificate);

            CertPath certPath = certPathAndAnchor.getCertPath();
            TrustAnchor trustAnchor = certPathAndAnchor.getTrustAnchor();

            if (certPath != null && trustAnchor != null) {
                trusted = true;

                revoked = isRevoked(certPath, trustAnchor);

                if (!revoked) {
                    /*
                     * Chain is valid, not expired, not revoked. We now need to check
                     * whether a certificate in the chain is not BlackListed.
                     */
                    blackListed = isBlackListed(certPath);

                    valid = !blackListed;
                } else {
                    valid = false;
                }
            } else {
                throw new CertPathBuilderException("A valid CertPath could not be built.");
            }
        } catch (CertPathBuilderException e) {
            /*
             * A valid certificate chain could not be built. This can happen because of a lot of reasons:
             * an intermediate or root certificate is not trusted, a certificate in the chain has expired,
             * a certificate in the chain is invalid etc. We now check whether the certificate is white listed.
             *   
             * Note: Because the chain is not valid we cannot check the revocation status so white listing
             * a certificate should be done with care.
             */
            logger.debug("CertPathBuilderException", e);

            try {
                whiteListed = isWhiteListed(x509Certificate);

                valid = whiteListed;
            } catch (CTLException ctle) {
                logger.error("Error checking the CTL.", ctle);
            }

            if (!valid) {
                /*
                 * We do not want a complete stack trace on a CertPathBuilderException because this exception can be thrown
                 * quite often. CertPathBuilderException is also thrown when a path validator exception occurs. We will
                 * therefore try to extract the root cause.
                 */
                Throwable cause = ExceptionUtils.getRootCause(e);

                if (cause == null) {
                    cause = e;
                }

                reportFailure("Error building certPath. " + cause.getMessage());
            }
        } catch (InvalidAlgorithmParameterException e) {
            reportFailure("Error building certPath.", e);
        } catch (NoSuchAlgorithmException e) {
            reportFailure("Error building certPath.", e);
        } catch (NoSuchProviderException e) {
            reportFailure("Error building certPath.", e);
        } catch (SecurityFactoryFactoryException e) {
            reportFailure("Error building certPath.", e);
        } catch (CTLException e) {
            reportFailure("Error checking CTL status.", e);
        }

        if (!valid) {
            logger.debug("Failure message: " + failureMessage);
        }

        return valid;
    }

    private void reportFailure(String message, Throwable t) {
        Throwable cause = ExceptionUtils.getRootCause(t);

        if (cause == null) {
            cause = t;
        }

        failureMessage = StringUtils.isNotBlank(failureMessage) ? failureMessage + "; " + message : message;

        if (cause != null) {
            failureMessage = failureMessage + " Exception: " + cause.getMessage();

            logger.error(message, cause);
        } else {
            logger.debug(message);
        }
    }

    private void reportFailure(String message) {
        reportFailure(message, null);
    }

    protected void modifyPathBuilder(CertificatePathBuilder pathBuilder) {
        /*
         * Subclasses can use this to add CertPathChecker's etc.
         */
    }

    private CertPathAndAnchor getCertPathAndAnchor(X509Certificate certificate)
            throws CertPathBuilderException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            NoSuchProviderException, SecurityFactoryFactoryException {
        CertificatePathBuilder pathBuilder = certificatePathBuilderFactory.createCertificatePathBuilder();

        modifyPathBuilder(pathBuilder);

        /* 
         * Add the x509Certificate to the stores used for path building to make sure the 
         * certificate is found by the path builder.
         */
        pathBuilder.addCertStore(CertStoreUtils.createCertStore(certificate));

        /*
         * Add the additional certificates if there are any
         */
        if (additionalCertificates != null) {
            pathBuilder.addCertStore(CertStoreUtils.createCertStore(additionalCertificates));
        }

        pathBuilder.setDate(getDate());

        CertPathBuilderResult pathBuilderResult = pathBuilder.buildPath(certificate);

        CertPath certPath = pathBuilderResult.getCertPath();
        TrustAnchor trustAnchor = null;

        if (pathBuilderResult instanceof PKIXCertPathBuilderResult) {
            PKIXCertPathBuilderResult pkixResult = (PKIXCertPathBuilderResult) pathBuilderResult;

            trustAnchor = pkixResult.getTrustAnchor();
        }

        return new CertPathAndAnchor(certPath, trustAnchor);
    }

    private boolean isRevoked(CertPath certPath, TrustAnchor trustAnchor) {
        boolean revoked = true;

        try {
            /* 
             * check if the certificate is revoked 
             */

            RevocationResult revocationResult = revocationChecker.getRevocationStatus(certPath, trustAnchor,
                    getDate());

            if (revocationResult != null) {
                /* 
                 * Check if the returned revocation status is acceptable. What an acceptable revocation 
                 * status is is determined by a policy. Currently we will use a hardcoded policy. See
                 * DEFAULT_ACCEPTABLE_REVOCATION_STATUS.
                 */
                for (RevocationStatus acceptableStatus : acceptableRevocationStatus) {
                    if (acceptableStatus == revocationResult.getStatus()) {
                        revoked = false;
                        break;
                    }
                }

                if (revoked) {
                    reportFailure("Certificate not accepted. Revocation status :" + revocationResult.getStatus());
                }
            }
        } catch (CRLException e) {
            reportFailure("Error while checking revocation status.", e);
        }

        return revoked;
    }

    /*
     * Helper class so we can return a CertPath and TrustAnchor
     */
    private static class CertPathAndAnchor {
        private final CertPath certPath;
        private final TrustAnchor trustAnchor;

        public CertPathAndAnchor(CertPath certPath, TrustAnchor trustAnchor) {
            this.certPath = certPath;
            this.trustAnchor = trustAnchor;
        }

        public CertPath getCertPath() {
            return certPath;
        }

        public TrustAnchor getTrustAnchor() {
            return trustAnchor;
        }
    }
}