SSLEngine.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice.SSL;

import com.zeroc.Ice.Communicator;
import com.zeroc.Ice.InitializationData;
import com.zeroc.Ice.InitializationException;
import com.zeroc.Ice.Logger;
import com.zeroc.Ice.Properties;
import com.zeroc.Ice.SecurityException;
import com.zeroc.Ice.Util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509TrustManager;

/**
 * @hidden Public because it's used by Ice.Instance
 */
public class SSLEngine {
    public SSLEngine(Communicator communicator) {
        _communicator = communicator;
        _logger = _communicator.getLogger();
        _securityTraceLevel =
            _communicator.getProperties().getIcePropertyAsInt("IceSSL.Trace.Security");
        _securityTraceCategory = "Security";
        _trustManager = new TrustManager(_communicator);
    }

    public void initialize() {
        Properties properties = communicator().getProperties();

        // CheckCertName determines whether we compare the name in a peer's certificate against its hostname.
        _checkCertName = properties.getIcePropertyAsInt("IceSSL.CheckCertName") > 0;

        // CheckCertName > 1 enables SNI, the SNI extension applies to client connections, indicating the hostname to
        // the server (must be DNS hostname, not an IP address).
        _serverNameIndication = properties.getIcePropertyAsInt("IceSSL.CheckCertName") > 1;

        // VerifyPeer determines whether certificate validation failures abort a connection.
        _verifyPeer = properties.getIcePropertyAsInt("IceSSL.VerifyPeer");

        // If the user doesn't supply an SSLContext, we need to create one based on property settings.
        if (_context == null) {
            try {
                // Check for a default directory. We look in this directory for files mentioned in the configuration.
                _defaultDir = properties.getIceProperty("IceSSL.DefaultDir");

                // The keystore holds private keys and associated certificates.
                String keystorePath = properties.getIceProperty("IceSSL.Keystore");

                // The password for the keys.
                String password = properties.getIceProperty("IceSSL.Password");

                // The password for the keystore.
                String keystorePassword = properties.getIceProperty("IceSSL.KeystorePassword");

                // The default keystore type is usually "JKS", but the legal values are determined by the JVM
                // implementation. Other possibilities include "PKCS12" and "BKS".
                final String defaultType = KeyStore.getDefaultType();
                final String keystoreType =
                    properties.getPropertyWithDefault("IceSSL.KeystoreType", defaultType);

                // The alias of the key to use in authentication.
                String alias = properties.getIceProperty("IceSSL.Alias");
                boolean overrideAlias = !alias.isEmpty(); // Always use the configured alias

                // The truststore holds the certificates of trusted CAs.
                String truststorePath = properties.getIceProperty("IceSSL.Truststore");

                // The password for the truststore.
                String truststorePassword = properties.getIceProperty("IceSSL.TruststorePassword");

                // The default truststore type is usually "JKS", but the legal values are determined by the JVM
                // implementation. Other possibilities include "PKCS12" and "BKS".
                final String truststoreType =
                    properties.getPropertyWithDefault("IceSSL.TruststoreType", defaultType);

                // Collect the key managers.
                KeyManager[] keyManagers = null;
                KeyStore keys = null;
                if (!keystorePath.isEmpty()) {
                    InputStream keystoreStream = null;
                    try {
                        keystoreStream = openResource(keystorePath);
                        if (keystoreStream == null) {
                            throw new InitializationException(
                                "SSL transport: keystore not found:\n" + keystorePath);
                        }

                        keys = KeyStore.getInstance(keystoreType);
                        char[] passwordChars = null;
                        if (!keystorePassword.isEmpty()) {
                            passwordChars = keystorePassword.toCharArray();
                        } else if ("BKS".equals(keystoreType) || "PKCS12".equals(keystoreType)) {
                            // Bouncy Castle or PKCS12 does not permit null passwords.
                            passwordChars = new char[0];
                        }

                        keys.load(keystoreStream, passwordChars);

                        if (passwordChars != null) {
                            Arrays.fill(passwordChars, '\0');
                        }
                        keystorePassword = null;
                    } catch (IOException ex) {
                        throw new InitializationException(
                            "SSL transport: unable to load keystore:\n" + keystorePath, ex);
                    } finally {
                        if (keystoreStream != null) {
                            try {
                                keystoreStream.close();
                            } catch (IOException e) {
                                // Ignore.
                            }
                        }
                    }

                    String algorithm = KeyManagerFactory.getDefaultAlgorithm();
                    KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
                    char[] passwordChars = new char[0]; // This password cannot be null.
                    if (!password.isEmpty()) {
                        passwordChars = password.toCharArray();
                    }
                    kmf.init(keys, passwordChars);
                    if (passwordChars.length > 0) {
                        Arrays.fill(passwordChars, '\0');
                    }
                    password = null;
                    keyManagers = kmf.getKeyManagers();

                    // If no alias is specified, we look for the first key entry in the key store.
                    //
                    // This is required to force the key manager to always choose a certificate even if there's no
                    // certificate signed by any of the CA names sent by the server. Ice servers might indeed not
                    // always send the CA names of their trusted roots.
                    if (alias.isEmpty()) {
                        for (Enumeration<String> e = keys.aliases(); e.hasMoreElements(); ) {
                            String a = e.nextElement();
                            if (keys.isKeyEntry(a)) {
                                alias = a;
                                break;
                            }
                        }
                    } else {
                        // If the user selected a specific alias, ensure it correspond with a key entry.
                        if (!keys.isKeyEntry(alias)) {
                            throw new InitializationException(
                                "SSL transport: keystore does not contain an entry with alias `" + alias + "'");
                        }
                    }

                    if (!alias.isEmpty()) {
                        // wrap the key managers in order to return the desired alias.
                        for (int i = 0; i < keyManagers.length; i++) {
                            keyManagers[i] =
                                new X509KeyManagerI(
                                    (X509ExtendedKeyManager) keyManagers[i],
                                    alias,
                                    overrideAlias);
                        }
                    }
                }

                // Load the truststore.
                KeyStore truststore = null;
                if (!truststorePath.isEmpty()) {
                    // If the trust store and the key store are the same file, don't create another key store.
                    if (truststorePath.equals(keystorePath)) {
                        assert keys != null;
                        truststore = keys;
                    } else {
                        InputStream truststoreStream = null;
                        try {
                            truststoreStream = openResource(truststorePath);
                            if (truststoreStream == null) {
                                throw new InitializationException(
                                    "SSL transport: truststore not found:\n" + truststorePath);
                            }

                            truststore = KeyStore.getInstance(truststoreType);

                            char[] passwordChars = null;
                            if (!truststorePassword.isEmpty()) {
                                passwordChars = truststorePassword.toCharArray();
                            } else if ("BKS".equals(truststoreType) || "PKCS12".equals(truststoreType)) {
                                // Bouncy Castle or PKCS12 does not permit null passwords.
                                passwordChars = new char[0];
                            }

                            truststore.load(truststoreStream, passwordChars);

                            if (passwordChars != null) {
                                Arrays.fill(passwordChars, '\0');
                            }
                            truststorePassword = null;
                        } catch (IOException ex) {
                            throw new InitializationException(
                                "SSL transport: unable to load truststore:\n" + truststorePath,
                                ex);
                        } finally {
                            if (truststoreStream != null) {
                                try {
                                    truststoreStream.close();
                                } catch (IOException e) {
                                    // Ignore.
                                }
                            }
                        }
                    }
                }

                // Collect the trust managers. Use IceSSL.Truststore if specified, otherwise use the Java root CAs if
                // Ice.Use.PlatformCAs is enabled. If none of these are enabled use a dummy trust manager which
                // rejects any certificate.
                javax.net.ssl.TrustManager[] trustManagers = null;
                {
                    if (truststore != null) {
                        String algorithm = TrustManagerFactory.getDefaultAlgorithm();
                        TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
                        // Attempting to establish an outgoing connection with an empty truststore can cause hangs that
                        // eventually result in an exception such as: `InvalidAlgorithmParameterException` the
                        // trustAnchors parameter must be non-empty.
                        if (truststore.size() == 0) {
                            throw new InitializationException("SSL transport: truststore is empty");
                        }
                        tmf.init(truststore);
                        trustManagers = tmf.getTrustManagers();
                        assert (trustManagers != null);
                    } else if (properties.getIcePropertyAsInt("IceSSL.UsePlatformCAs") > 0) {
                        // Set trustManagers to null to use the platform CAs
                        trustManagers = null;
                    } else {
                        // Use a custom trust manager that rejects all certificates.
                        trustManagers =
                            new javax.net.ssl.TrustManager[]{
                                new X509TrustManager() {
                                    @Override
                                    public void checkClientTrusted(
                                                X509Certificate[] chain, String authType)
                                        throws CertificateException {
                                        throw new CertificateException("no trust anchors");
                                    }

                                    @Override
                                    public void checkServerTrusted(
                                                X509Certificate[] chain, String authType)
                                        throws CertificateException {
                                        throw new CertificateException("no trust anchors");
                                    }

                                    @Override
                                    public X509Certificate[] getAcceptedIssuers() {
                                        return new X509Certificate[0];
                                    }
                                }
                            };
                    }
                }

                // Initialize the SSL context.
                _context = SSLContext.getInstance("TLS");
                _context.init(keyManagers, trustManagers, null);
            } catch (GeneralSecurityException ex) {
                throw new InitializationException(
                    "SSL transport: unable to initialize context", ex);
            }
        }
    }

    int securityTraceLevel() {
        return _securityTraceLevel;
    }

    String securityTraceCategory() {
        return _securityTraceCategory;
    }

    javax.net.ssl.SSLEngine createSSLEngine(boolean incoming, String host, int port) {
        javax.net.ssl.SSLEngine engine;
        try {
            if (host != null) {
                engine = _context.createSSLEngine(host, port);
            } else {
                engine = _context.createSSLEngine();
            }
            engine.setUseClientMode(!incoming);
        } catch (Exception ex) {
            throw new SecurityException("SSL transport: couldn't create SSL engine", ex);
        }

        if (incoming) {
            if (_verifyPeer == 0) {
                engine.setWantClientAuth(false);
                engine.setNeedClientAuth(false);
            } else if (_verifyPeer == 1) {
                engine.setWantClientAuth(true);
            } else {
                engine.setNeedClientAuth(true);
            }
        } else {
            // Enable the HTTPS hostname verification algorithm
            if (_checkCertName && _verifyPeer > 0 && host != null) {
                SSLParameters params = new SSLParameters();
                params.setEndpointIdentificationAlgorithm("HTTPS");
                engine.setSSLParameters(params);
            }
        }

        // Server name indication
        if (!incoming && _serverNameIndication) {
            SNIHostName serverName = null;
            try {
                serverName = new SNIHostName(host);
                SSLParameters sslParams = engine.getSSLParameters();
                List<SNIServerName> serverNames = new ArrayList<>();
                serverNames.add(serverName);
                sslParams.setServerNames(serverNames);
                engine.setSSLParameters(sslParams);
            } catch (IllegalArgumentException ex) {
                // Invalid SNI hostname, ignore because it might be an IP
            }
        }

        return engine;
    }

    void traceConnection(String desc, javax.net.ssl.SSLEngine engine, boolean incoming) {
        SSLSession session = engine.getSession();
        String msg =
            "SSL summary for "
                + (incoming ? "incoming" : "outgoing")
                + " connection\n"
                + "cipher = "
                + session.getCipherSuite()
                + "\n"
                + "protocol = "
                + session.getProtocol()
                + "\n"
                + desc;
        _logger.trace(_securityTraceCategory, msg);
    }

    Communicator communicator() {
        return _communicator;
    }

    void verifyPeer(String address, ConnectionInfo info, String desc) {
        // IceSSL.VerifyPeer is translated into the proper SSLEngine configuration for a server, but we have to do it
        // ourselves for a client.
        if (!info.incoming) {
            if (_verifyPeer > 0 && !info.verified) {
                throw new SecurityException(
                    "SSL transport: server did not supply a certificate");
            }
        }

        if (!_trustManager.verify(info, desc)) {
            String msg =
                (info.incoming ? "incoming" : "outgoing")
                    + " connection rejected by trust manager\n"
                    + desc;
            if (_securityTraceLevel >= 1) {
                _logger.trace(_securityTraceCategory, msg);
            }
            throw new SecurityException(msg);
        }
    }

    void trustManagerFailure(boolean incoming, CertificateException ex)
        throws CertificateException {
        if (_verifyPeer == 0) {
            if (_securityTraceLevel >= 1) {
                String msg = "ignoring peer verification failure";
                if (_securityTraceLevel > 1) {
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw);
                    ex.printStackTrace(pw);
                    pw.flush();
                    msg += ":\n" + sw.toString();
                }
                _logger.trace(_securityTraceCategory, msg);
            }
        } else {
            throw ex;
        }
    }

    @SuppressWarnings("deprecation")
    private InputStream openResource(String path) throws IOException {
        boolean isAbsolute = false;
        try {
            new URL(path);
            isAbsolute = true;
        } catch (MalformedURLException ex) {
            File f = new File(path);
            isAbsolute = f.isAbsolute();
        }

        InitializationData initData = _communicator.getInstance().initializationData();

        ClassLoader classLoader =
            initData.classLoader != null ? initData.classLoader : getClass().getClassLoader();

        InputStream stream = Util.openResource(classLoader, path);

        // Retry with default directory if the first attempt failed and the path is relative.
        if (stream == null && !_defaultDir.isEmpty() && !isAbsolute) {
            stream = Util.openResource(classLoader, _defaultDir + File.separator + path);
        }

        if (stream != null) {
            stream = new BufferedInputStream(stream);
        }

        return stream;
    }

    private final Communicator _communicator;
    private final Logger _logger;
    private final int _securityTraceLevel;
    private final String _securityTraceCategory;
    private SSLContext _context;
    private String _defaultDir;
    private boolean _checkCertName;
    private boolean _serverNameIndication;
    private int _verifyPeer;
    private final TrustManager _trustManager;
}