OpenfireX509TrustManager.java 12.7 KB
Newer Older
1 2 3 4
package org.jivesoftware.openfire.keystore;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.util.Log;
5 6
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
7 8 9 10 11 12 13 14 15 16 17 18 19 20

import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;

/**
 * A Trust Manager implementation that adds Openfire-proprietary functionality.
 * 
 * @author Guus der Kinderen, guus.der.kinderen@gmail.com
 */
// TODO re-enable optional OCSP checking.
// TODO re-enable CRL checking.
21
public class OpenfireX509TrustManager implements X509TrustManager
22
{
23 24
    private static final Logger Log = LoggerFactory.getLogger( OpenfireX509TrustManager.class );

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    private static final Provider PROVIDER = new BouncyCastleProvider();

    static
    {
        // Add the BC provider to the list of security providers
        Security.addProvider( PROVIDER );
    }

    /**
     * A boolean that indicates if this trust manager will allow self-signed certificates to be trusted.
     */
    protected final boolean acceptSelfSigned;

    /**
     * A boolean that indicates if this trust manager will check if all certificates in the chain (including the root
     * certificates) are currently valid (notBefore/notAfter check).
     */
    private final boolean checkValidity;

    /**
     * The set of trusted issuers from the trust store. Note that these certificates are not validated. It is assumed
     * that this set can be long-lived. Time-based validation should occur close to the actual usage / invocation.
     */
    protected final Set<X509Certificate> trustedIssuers;

50
    public OpenfireX509TrustManager( KeyStore trustStore, boolean acceptSelfSigned, boolean checkValidity ) throws NoSuchAlgorithmException, KeyStoreException
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
    {
        this.acceptSelfSigned = acceptSelfSigned;
        this.checkValidity = checkValidity;

        // Retrieve all trusted certificates from the store, but don't validate them just yet!
        final Set<X509Certificate> trusted = new HashSet<>();

        final Enumeration<String> aliases = trustStore.aliases();
        while ( aliases.hasMoreElements() )
        {
            final String alias = aliases.nextElement();
            if ( trustStore.isCertificateEntry( alias ) )
            {
                final Certificate certificate = trustStore.getCertificate( alias );
                if ( certificate instanceof X509Certificate )
                {
                    trusted.add( (X509Certificate) certificate );
                }
            }
        }

        trustedIssuers = Collections.unmodifiableSet( trusted );
73 74

        Log.debug( "Constructed trust manager. Number of trusted issuers: {}, accepts self-signed: {}, checks validity: {}", trustedIssuers.size(), acceptSelfSigned, checkValidity );
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
    }

    @Override
    public void checkClientTrusted( X509Certificate[] chain, String authType ) throws CertificateException
    {
        // Find and use the end entity as the selector for verification.
        final X509Certificate endEntityCert = CertificateUtils.identifyEndEntityCertificate( Arrays.asList( chain ) );
        final X509CertSelector selector = new X509CertSelector();
        selector.setCertificate( endEntityCert );

        try
        {
            checkChainTrusted( selector, chain );
        }
        catch ( InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathBuilderException ex )
        {
            throw new CertificateException( ex );
        }
    }

    @Override
    public void checkServerTrusted( X509Certificate[] chain, String authType ) throws CertificateException
    {
        // Find and use the end entity as the selector for verification.
        final X509Certificate endEntityCert = CertificateUtils.identifyEndEntityCertificate( Arrays.asList( chain ) );
        final X509CertSelector selector = new X509CertSelector();
        selector.setCertificate( endEntityCert );

        try
        {
            checkChainTrusted( selector, chain );
        }
        catch ( InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathBuilderException ex )
        {
            throw new CertificateException( ex );
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers()
    {
        final Set<X509Certificate> result;
        if ( checkValidity )
        {
            // Filter the set of issuers to see what certificates are currently valid. Note that this might result in a
            // different result as compared with the last verification.
            result = CertificateUtils.filterValid( this.trustedIssuers );
        }
        else
        {
            result = this.trustedIssuers;
        }
        return result.toArray( new X509Certificate[ result.size() ] );
    }

    /**
     * Determine if the given partial or complete certificate chain can be trusted to represent the entity that is
     * defined by the criteria specified by the 'selector' argument.
     *
     * A (valid) partial chain is a chain that, combined with certificates from the trust store in this manager, can be
     * completed to a full chain.
     *
     * Chains provided to this method do not need to be in any particular order.
     *
     * This implementation uses the trust anchors as represented by {@link #getAcceptedIssuers()} to verify that the
     * chain that is provided either includes a certificate from an accepted issuer, or is directly issued by one.
     *
     * Depending on the configuration of this class, other verification is done:
     * <ul>
     *     <li>{@link #acceptSelfSigned}: when <tt>true</tt>, any chain that has a length of one and is self-signed is
     *                                    considered as a 'trust anchor' (but is still subject to other checks, such as
     *                                    expiration checks).</li>
     * </ul>
     *
     * This method will either return a value, which indicates that the chain is trusted, or will throw an exception.
     *
     * @param selector Characteristics of the entity to be represented by the chain (cannot be null).
     * @param chain The certificate chain that is to be verified (cannot be null or empty).
     * @return A trusted certificate path (never null).
     *
     * @throws InvalidAlgorithmParameterException
     * @throws NoSuchAlgorithmException
     * @throws CertPathBuilderException
     */
    protected CertPath checkChainTrusted( CertSelector selector, X509Certificate... chain ) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertPathBuilderException
    {
        if ( selector == null )
        {
            throw new IllegalArgumentException( "Argument 'selector' cannot be null");
        }

        if ( chain == null || chain.length == 0 )
        {
            throw new IllegalArgumentException( "Argument 'chain' cannot be null or an empty array.");
        }

171 172
        Log.debug( "Attempting to verify a chain of {} certificates.", chain.length );

173 174 175 176 177 178 179 180 181
        // The set of trusted issuers (for this invocation), based on the issuers from the truststore.
        final Set<X509Certificate> trustedIssuers = new HashSet<>();
        trustedIssuers.addAll( this.trustedIssuers );

        // When accepting self-signed certificates, and the chain is a self-signed certificate, add it to the collection
        // of trusted issuers. Blindly accepting this issuer is undesirable, as that would circumvent other checks, such
        // as expiration checking.
        if ( acceptSelfSigned && chain.length == 1 )
        {
182 183
            Log.debug( "Attempting to accept the self-signed certificate of this chain of length one, as instructed by configuration." );

184 185 186
            final X509Certificate cert = chain[0];
            if ( cert.getSubjectDN().equals( cert.getIssuerDN() ) )
            {
187
                Log.debug( "Chain of one appears to be self-signed. Adding it to the set of trusted issuers." );
188 189
                trustedIssuers.add( cert );
            }
190 191 192 193
            else
            {
                Log.debug( "Chain of one is not self-signed. Not adding it to the set of trusted issuers." );
            }
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
        }

        // Turn trusted into accepted issuers.
        final Set<X509Certificate> acceptedIssuers;
        if ( checkValidity )
        {
            // See what certificates are currently valid.
            acceptedIssuers = CertificateUtils.filterValid( trustedIssuers );
        }
        else
        {
            acceptedIssuers = trustedIssuers;
        }

        // Transform all accepted issuers into a set of unique trustAnchors.
        final Set<TrustAnchor> trustAnchors = CertificateUtils.toTrustAnchors( acceptedIssuers );

        // All certificates that are part of the (possibly incomplete) chain.
        final CertStore certificates = CertStore.getInstance( "Collection", new CollectionCertStoreParameters( Arrays.asList( chain ) ) );

        // Build the configuration for the path builder. It is based on the collection of accepted issuers / trust anchors
        final PKIXBuilderParameters parameters = new PKIXBuilderParameters( trustAnchors, selector );

        // Validity checks are enabled by default in the CertPathBuilder implementation.
        if ( !checkValidity )
        {
220 221
            Log.debug( "Attempting to ignore any validity (expiry) issues, as instructed by configuration." );

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
            // There is no way to configure the pathBuilder to ignore date validity. When validity checks are to be
            // ignored, try to find a point in time where all certificates in the chain are valid.
            final Date validPointInTime = CertificateUtils.findValidPointInTime( chain );

            // This strategy to 'disable' validity checks won't work if there's no overlap of validity periods of all
            // certificates. TODO improve the implementation.
            if ( validPointInTime == null )
            {
                Log.warn( "The existing implementation is unable to fully ignore certificate validity periods for this chain, even though it is configured to do so. Certificate checks might fail because of expiration for end entity: " + chain[0] );
            }
            else
            {
                parameters.setDate( validPointInTime );
            }
        }

        // Add all certificates that are part of the chain to the configuration. Together with the trust anchors, the
        // entire chain should now be in the store.
        parameters.addCertStore( certificates );

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        // When true, validation will fail if no CRLs are provided!
        parameters.setRevocationEnabled( false );

        Log.debug( "Validating chain with {} certificates, using {} trust anchors.", chain.length, trustAnchors.size() );

        // Try to use BouncyCastle - if that doesn't work, pick one.
        CertPathBuilder pathBuilder;
        try
        {
            pathBuilder = CertPathBuilder.getInstance( "PKIX", "BC" );
        }
        catch ( NoSuchProviderException e )
        {
            Log.warn( "Unable to use the BC provider! Trying to use a fallback provider.", e );
            pathBuilder = CertPathBuilder.getInstance( "PKIX" );
        }

        try
        {
            // Finally, construct (and implicitly validate) the certificate path.
            final CertPathBuilderResult result = pathBuilder.build( parameters );
            return result.getCertPath();
        }
        catch ( CertPathBuilderException ex )
        {
            // This exception generally isn't very helpful. This block attempts to print more debug information.
            try
            {
270 271 272 273
                Log.debug( "** Accepted Issuers (trust anchors, \"root CA's\"):" );
                for ( X509Certificate acceptedIssuer : acceptedIssuers) {
                    Log.debug( "   - " + acceptedIssuer.getSubjectDN() + "/" + acceptedIssuer.getIssuerDN() );
                }
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
                Log.debug( "** Chain to be validated:" );
                Log.debug( "   length: " + chain.length );
                for (int i=0; i<chain.length; i++) {
                    Log.debug( " Certificate[{}] (valid from {} to {}):", i, chain[ i ].getNotBefore(), chain[ i ].getNotAfter() );
                    Log.debug( "   subjectDN: " + chain[ i ].getSubjectDN() );
                    Log.debug( "   issuerDN: " + chain[ i ].getIssuerDN() );

                    for ( X509Certificate acceptedIssuer : acceptedIssuers) {
                        if ( acceptedIssuer.getIssuerDN().equals( chain[i].getIssuerDN() ) ) {
                            Log.debug( "Found accepted issuer with same DN: " + acceptedIssuer.getIssuerDN() );
                        }
                    }
                }
            }
            finally
            {
                // rethrow the original exception.
                throw ex;
            }
        }
294 295 296

    }
}