/*
 * SSLTest.java
 *
 * Tests servers for SSL/TLS protocol and cipher support.
 *
 * Copyright (c) 2015 Christopher Schultz
 *
 * Christopher Schultz licenses this file to You under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package net.christopherschultz.ssltest;

import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.crypto.Cipher;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;

/**
 * A driver class to test a server's SSL/TLS support.
 *
 * Usage: java SSLTest [opts] host[:port]
 *
 * Try "java SSLTest -h" for help.
 *
 * This tester will attempts to handshake with the target host with all
 * available protocols and ciphers and report which ones were accepted and
 * which were rejected. An HTTP connection is never fully made, so these
 * connections should not flood the host's access log with entries.
 *
 * @author Christopher Schultz
 */
public class SSLTest
{
    private static void usage()
    {
        System.out.println("Usage: java " + SSLTest.class + " [opts] host[:port]");
        System.out.println();
        System.out.println("-sslprotocol                 Sets the SSL/TLS protocol to be used (e.g. SSL, TLS, SSLv3, TLSv1.2, etc.)");
        System.out.println("-enabledprotocols protocols  Sets individual SSL/TLS ptotocols that should be enabled");
        System.out.println("-ciphers cipherspec          A comma-separated list of SSL/TLS ciphers");
        System.out.println("-cipherFilter filter         A regular expression containing cipher suite patterns which should be REMOVED from the acceptable list (e.g. '(NULL|anon|RC4)')");
        System.out.println("-connectonly                 Don't scan; only connect a single time");

        System.out.println("-keystore                    Sets the key store for connections (for TLS client certificates)");
        System.out.println("-keystoretype type           Sets the type for the key store");
        System.out.println("-keystorepassword pass       Sets the password for the key store");
        System.out.println("-keystoreprovider provider   Sets the crypto provider for the key store");

        System.out.println("-truststore                  Sets the trust store for connections");
        System.out.println("-truststoretype type         Sets the type for the trust store");
        System.out.println("-truststorepassword pass     Sets the password for the trust store");
        System.out.println("-truststorealgorithm alg     Sets the algorithm for the trust store");
        System.out.println("-truststoreprovider provider Sets the crypto provider for the trust store");
        System.out.println("-crlfilename                 Sets the CRL filename to use for the trust store");

        System.out.println("-check-certificate           Checks certificate trust (default: false)");
        System.out.println("-no-check-certificate        Ignores certificate errors (default: true)");
        System.out.println("-verify-hostname             Verifies certificate hostname (default: false)");
        System.out.println("-no-verify-hostname          Ignores hostname mismatches (default: true)");

        System.out.println("-showcerts                   Show server's certificate chain information");
        System.out.println("-showsslerrors               Show SSL/TLS error details");
        System.out.println("-showhandshakeerrors         Show SSL/TLS handshake error details");
        System.out.println("-showerrors                  Show all connection error details");
        System.out.println("-hiderejects                 Only show protocols/ciphers which were successful");
        System.out.println();
        System.out.println("-client-info                 Show this client's capabilities and exit");
        System.out.println("-h -help --help              Shows this help message");
    }

    public static void main(String[] args)
        throws Exception
    {
        // Enable all algorithms + protocols
        // System.setProperty("jdk.tls.client.protocols", "SSLv2Hello,SSLv3,TLSv1,TLSv1.1,TLSv1.2");
        Security.setProperty("jdk.tls.disabledAlgorithms", "");
        //System.setProperty("jdk.tls.namedGroups", "secp256r1, secp384r1, secp521r1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, secp256k1");
        Security.setProperty("crypto.policy", "unlimited"); // For Java 9+

        int connectTimeout = 0; // default = infinite
        int readTimeout = 1000;

        boolean disableHostnameVerification = true;
        boolean disableCertificateChecking = true;
        boolean hideRejects = false;

        String trustStoreFilename = System.getProperty("javax.net.ssl.trustStore");
        String trustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword");
        String trustStoreType = System.getProperty("javax.net.ssl.trustStoreType");
        String trustStoreProvider = System.getProperty("javax.net.ssl.trustStoreProvider");
        String trustStoreAlgorithm = null;
        String keyStoreFilename = System.getProperty("javax.net.ssl.keyStore");
        String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword");
        String keyStoreType = System.getProperty("javax.net.ssl.keyStoreType");
        String keyStoreProvider = System.getProperty("javax.net.ssl.keyStoreProvider");
        String sslProtocol = "TLS";
        String[] sslEnabledProtocols = null; // new String[] { "SSLv2", "SSLv2hello", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2" };
        String[] sslCipherSuites = null; // Default = default for protocol
        Pattern sslCipherSuitesFilter = null;
        String crlFilename = null;
        boolean showCerts = false;
        boolean connectOnly = false;
        boolean showHandshakeErrors = false;
        boolean showSSLErrors = false;
        boolean showErrors = false;
        boolean dumpClientInfo = false;

        if(args.length < 1)
        {
            usage();
            System.exit(0);
        }

        int argIndex;
        for(argIndex = 0; argIndex < args.length; ++argIndex)
        {
            String arg = args[argIndex];

            if(!arg.startsWith("-"))
                break;
            else if("--".equals(arg))
                break;
            else if("-no-check-certificate".equals(arg))
                disableCertificateChecking = true;
            else if("-check-certificate".equals(arg))
                disableCertificateChecking = false;
            else if("-no-verify-hostname".equals(arg))
                disableHostnameVerification = true;
            else if("-verify-hostname".equals(arg))
                disableHostnameVerification = false;
            else if("-sslprotocol".equals(arg))
                sslProtocol = args[++argIndex];
            else if("-enabledprotocols".equals(arg))
                sslEnabledProtocols = args[++argIndex].split("\\s*,\\s*");
            else if("-ciphers".equals(arg))
                sslCipherSuites = args[++argIndex].split("\\s*,\\s*");
            else if("-cipherFilter".equals(arg)) {
                try {
                    sslCipherSuitesFilter = Pattern.compile(args[++argIndex]);
                } catch (PatternSyntaxException pse) {
                    System.err.println("Invalid cipher filter pattern: " + pse.getMessage());
                    System.exit(1);
                }
            }
            else if("-connecttimeout".equals(arg))
                connectTimeout = Integer.parseInt(args[++argIndex]);
            else if("-readtimeout".equals(arg))
                readTimeout = Integer.parseInt(args[++argIndex]);
            else if("-truststore".equals(arg))
                trustStoreFilename = args[++argIndex];
            else if("-truststoretype".equals(arg))
                trustStoreType = args[++argIndex];
            else if("-truststorepassword".equals(arg))
                trustStorePassword = args[++argIndex];
            else if("-truststoreprovider".equals(arg))
                trustStoreProvider = args[++argIndex];
            else if("-truststorealgorithm".equals(arg))
                trustStoreAlgorithm = args[++argIndex];
            else if("-crlfilename".equals(arg))
                crlFilename = args[++argIndex];
            else if("-keystore".equals(arg))
                keyStoreFilename = args[++argIndex];
            else if("-keystoretype".equals(arg))
                keyStoreType = args[++argIndex];
            else if("-keystorepassword".equals(arg))
                keyStorePassword = args[++argIndex];
            else if("-keystoreprovider".equals(arg))
                keyStoreProvider = args[++argIndex];
            else if("-showcerts".equals(arg))
                showCerts = true;
            else if("-showerrors".equals(arg))
                showErrors = showHandshakeErrors = showSSLErrors = true;
            else if("-showhandshakeerrors".equals(arg))
                showHandshakeErrors = true;
            else if("-showsslerrors".equals(arg))
                showSSLErrors = true;
            else if("-connectonly".equals(arg))
                connectOnly = true;
            else if("-hiderejects".equals(arg))
                hideRejects = true;
            else if("-client-info".equals(arg))
                dumpClientInfo = true;
            else if("-list-curves".equals(arg)) {
                listCurves(System.out);
                return;
            }
            else if("--help".equals(arg)
                    || "-h".equals(arg)
                    || "-help".equals(arg))
            {
                usage();
                System.exit(0);
            }
            else
            {
                System.err.println("Unrecognized option: " + arg);
                System.exit(1);
            }
        }

        int port = 443;
        String host;

        if(argIndex == args.length - 1) {
            host = args[argIndex++];
        } else if (argIndex < args.length) {
            System.err.println("Unexpected additional arguments: "
                               + java.util.Arrays.asList(args).subList(argIndex + 1, args.length));

            usage();
            System.exit(1);
            host = "[unknown]";
        } else if (dumpClientInfo) {
            host = "[unknown]";
        } else {
            System.err.println("Expected hostname[:port]");

            usage();

            System.exit(1);
            host = "[unknown]";
        }

        if(null != sslCipherSuites && null != sslCipherSuitesFilter) {
            System.err.println("The -ciphers and -cipherFilter are mutually-exclusive. Please specify only one of the two.");
            System.exit(1);
        }

        // TODO: Does this actually do anything?
        if(disableHostnameVerification)
            SSLUtils.disableSSLHostnameVerification();

        KeyManager[] keyManagers;
        TrustManager[] trustManagers;

        if(null != keyStoreFilename)
        {
            if(null == keyStoreType)
                keyStoreType = "JKS";

            KeyStore keyStore = SSLUtils.getStore(keyStoreFilename, keyStorePassword, keyStoreType, keyStoreProvider);
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            char[] kpwd;
            if(null != keyStorePassword && 0 < keyStorePassword.length())
                kpwd = keyStorePassword.toCharArray();
            else
                kpwd = null;
            kmf.init(keyStore, kpwd);
            keyManagers = kmf.getKeyManagers();
        }
        else
            keyManagers = null;

        if(disableCertificateChecking
           || "true".equalsIgnoreCase(System.getProperty("disable.ssl.cert.checks")))
        {
            trustManagers = SSLUtils.getTrustAllCertsTrustManagers();
        }
        else if(null != trustStoreFilename)
        {
            if(null == trustStoreType)
                trustStoreType = "JKS";

            trustManagers = SSLUtils.getTrustManagers(trustStoreFilename, trustStorePassword, trustStoreType, trustStoreProvider, trustStoreAlgorithm, null, crlFilename);
        }
        else
        {
            // Use platform default
            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            // null keystore == default trust store
            tmf.init((KeyStore)null);

            trustManagers = tmf.getTrustManagers();
        }

        if(dumpClientInfo) {
            System.out.println("Dumping Clilent Info");

            SecureRandom rand = new SecureRandom();
            String[] supportedCipherSuites = getJVMSupportedCipherSuites(sslProtocol, rand);
            TreeSet<String> supportedCipherSuiteSet = new TreeSet<String>();
            for(String cipherSuite : supportedCipherSuites)
                supportedCipherSuiteSet.add(cipherSuite);

            String[] defaultCipherSuites = getJVMDefaultCipherSuites(sslProtocol, rand);

            HashSet<String> defaultCipherSuiteSet = new HashSet<String>(defaultCipherSuites.length);
            for(String cipherSuite : defaultCipherSuites)
                defaultCipherSuiteSet.add(cipherSuite);

            System.out.println("Supported cipher suites:                            [Enabled by Default]");
            for(String cipherSuite : supportedCipherSuiteSet) {
                System.out.print("  ");
                System.out.print(cipherSuite);
                if(defaultCipherSuiteSet.contains(cipherSuite)) {
                    for(int i=0; i<50 - cipherSuite.length(); ++i)
                        System.out.print(' ');
                    System.out.println('*');
                } else {
                    System.out.println();
                }
            }

            System.out.println();

            if(null != keyManagers) {
                System.out.println("Available Client Keys:");
                for(KeyManager keyManager : keyManagers) {
                    if(keyManager instanceof X509KeyManager) {
                        X509KeyManager xkm = (X509KeyManager)keyManager;

                        String alias = xkm.chooseClientAlias(new String[] { "RSA", "DSA", "EC" }, null, null);
                        boolean first = true;
                        for(X509Certificate cert : xkm.getCertificateChain(alias)) {
                            if(first) first = false;
                            else System.out.println("===");
                            dumpCertificate(cert);
                        }
                    } else {
                        System.out.println(keyManager);
                    }
                }
                System.out.println();
            }
            if(null == trustManagers) {
                System.out.println("No trust managers installed?");
            } else if(1 == trustManagers.length && Arrays.equals(trustManagers, SSLUtils.getTrustAllCertsTrustManagers())) {
                System.out.println("[Trust All Server Certificates]");
            } else {
                for(TrustManager trustManager : trustManagers) {
                    if(trustManager instanceof X509TrustManager) {
                        X509TrustManager xtm = (X509TrustManager)trustManager;

                        System.out.println("Trusted Certificates:");
                        X509Certificate[] issuers = xtm.getAcceptedIssuers();
                        if(null == issuers || 0 == issuers.length) {
                            System.out.println("This trust manager evidently contains no trusted issuers, or the trust store could not be opened.");
                        } else {
                            boolean first = true;
                            for(X509Certificate cert : issuers) {
                                if(first) first = false;
                                else System.out.println("===");
                                dumpCertificate(cert);
                            }
                        }
                    } else {
                        System.out.println(trustManager);
                    }
                }
            }
            System.exit(1);

        }

        int pos = host.indexOf(':');
        if(pos > 0)
        {
            port = Integer.parseInt(host.substring(pos + 1));
            host = host.substring(0, pos);
        }

        try
        {
            InetAddress[] iaddrs = InetAddress.getAllByName(host);
            if(null == iaddrs || 0 == iaddrs.length)
            {
                System.err.println("Unknown hostname: " + host);
                System.exit(1);
            }
            if(1 == iaddrs.length)
                System.out.println("Host [" + host + "] resolves to address [" + iaddrs[0].getHostAddress() + "]");
            else
            {
                System.out.print("Host [" + host + "] resolves to addresses ");
                for(int i=0; i<iaddrs.length; ++i)
                {
                    if(i > 0) System.out.print(", ");
                    System.out.print("[" + iaddrs[i].getHostAddress() + "]");
                }
                System.out.println();
            }
        }
        catch (UnknownHostException uhe)
        {
            System.err.println("Unknown hostname: " + host);
            System.exit(1);
        }

        InetSocketAddress address = new InetSocketAddress(host, port);
        if(address.isUnresolved())
        {
            System.err.println("Unknown hostname: " + host);
            System.exit(1);
        }

        List<String> supportedProtocols;

        if(null == sslEnabledProtocols)
        {
            // Auto-detect supported protocols
            ArrayList<String> protocols = new ArrayList<String>();
            // TODO: Allow the specification of a specific provider (or set?)
            for(Provider provider : Security.getProviders())
            {
                for(Object prop : provider.keySet())
                {
                    String key = (String)prop;
                    if(key.startsWith("SSLContext.")
                       && !"SSLContext.Default".equals(key)
                       && key.matches(".*[0-9].*"))
                        protocols.add(key.substring("SSLContext.".length()));
                    else if(key.startsWith("Alg.Alias.SSLContext.")
                            && key.matches(".*[0-9].*"))
                        protocols.add(key.substring("Alg.Alias.SSLContext.".length()));
                }
            }
            Collections.sort(protocols); // Should give us a nice sort-order by default
            System.out.println("Auto-detected client-supported protocols: " + protocols);
            supportedProtocols = protocols;
            sslEnabledProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]);
        }
        else
        {
            supportedProtocols = new ArrayList<String>(Arrays.asList(sslEnabledProtocols));
        }

        // Warn about operating under limited cryptographic controls.
        if(Integer.MAX_VALUE > Cipher.getMaxAllowedKeyLength("foo"))
            System.err.println("[warning] Client is running under LIMITED cryptographic controls. Consider installing the JCE Unlimited Strength Jurisdiction Policy Files.");

        SecureRandom rand = new SecureRandom();

        if(!connectOnly) {
            System.out.println("Testing server " + host + ":" + port);

            String reportFormat = "%9s %8s %s%n";
            String errorReportFormat = "%9s %8s %s %s%n";

            System.out.print(String.format(reportFormat, "Supported", "Protocol", "Cipher"));

        HashSet<String> cipherSuites = new HashSet<String>();

        boolean stop = false;

        for(int i=0; i<sslEnabledProtocols.length && !stop; ++i)
        {
            String protocol = sslEnabledProtocols[i];

            String[] supportedCipherSuites = null;

            try
            {
                supportedCipherSuites = getJVMSupportedCipherSuites(protocol, rand);
            }
            catch (NoSuchAlgorithmException nsae)
            {
                System.out.print(String.format(reportFormat, "-----", protocol, " Not supported by client"));
                supportedProtocols.remove(protocol);
                continue;
            }
            catch (Exception e)
            {
                e.printStackTrace();
                continue; // Skip this protocol
            }

            // Restrict cipher suites to those specified by sslCipherSuites
            cipherSuites.clear();
            cipherSuites.addAll(Arrays.asList(supportedCipherSuites));
            if(null != sslCipherSuites)
                cipherSuites.retainAll(Arrays.asList(sslCipherSuites));
            else if(null != sslCipherSuitesFilter) {
                for(Iterator<String> j=cipherSuites.iterator(); j.hasNext(); ) {
                    String cipherSuite = j.next();

                    if(sslCipherSuitesFilter.matcher(cipherSuite).find()) {
                        j.remove();
                    }
                }
            }

            if(cipherSuites.isEmpty())
            {
                System.err.println("No overlapping cipher suites found for protocol " + protocol);
                supportedProtocols.remove(protocol);
                continue; // Go to the next protocol
            }

            for(Iterator<String> j=cipherSuites.iterator(); j.hasNext() && !stop; )
            {
                String cipherSuite = j.next();
                String status;

                SSLSocketFactory sf = SSLUtils.getSSLSocketFactory(protocol,
                                                                   new String[] { protocol },
                                                                   new String[] { cipherSuite },
                                                                   rand,
                                                                   trustManagers,
                                                                   keyManagers);

                SSLSocket socket = null;
                String error = null;

                try
                {
                    socket = createSSLSocket(address, host, port, connectTimeout, readTimeout, sf);
/*
socket.addHandshakeCompletedListener(new HandshakeCompletedListener() {

    @Override
    public void handshakeCompleted(HandshakeCompletedEvent evt)
    {
        System.err.println("======== COMPLETED HANDSHAKE, SESSION=" + evt.getSession());
        System.err.println("HANDSHAKE THREADNAME: " + Thread.currentThread().getName());
        SSLSocket socket = evt.getSocket();
        System.err.println("parameters=" + socket.getSSLParameters());
        System.err.println(java.util.Arrays.asList(socket.getSSLParameters().getProtocols()));
        System.err.println(java.util.Arrays.asList(socket.getSSLParameters().getCipherSuites()));
        System.err.println("constraints=" + socket.getSSLParameters().getAlgorithmConstraints());
        System.err.println("endpoint id algo=" + socket.getSSLParameters().getEndpointIdentificationAlgorithm());
        System.err.println("server names=" + socket.getSSLParameters().getServerNames());
try
{
    System.err.println("principal=" + evt.getPeerPrincipal());
        for(Certificate cert : evt.getSession().getPeerCertificates())
        {
            if("X.509".equals(cert.getType()))
            {
                X509Certificate x509cert = (X509Certificate)cert;
                System.out.println("==HS== certificate subject=" + x509cert.getSubjectDN());
                if(null != x509cert.getSigAlgParams())
                    System.out.println("==HS== parameters: " + Arrays.asList(x509cert.getSigAlgParams()));
            }
            else
                System.out.println("==HS== Unrecognized cert type: " + cert.getType());

            PublicKey pk = cert.getPublicKey();
            if("RSA".equals(pk.getAlgorithm()))
            {
                RSAPublicKey rsa = (RSAPublicKey)pk;
                System.out.println("==HS== RSA mod length: " + rsa.getModulus().bitLength());
                System.out.println("==HS== RSA format " + rsa.getFormat());
                System.out.println("==HS== RSA encoded: " + Arrays.asList(rsa.getEncoded()));
            } else {
                System.out.println("==HS== UNKNOWN Certificate algorithm: " + pk.getAlgorithm());
            }
            System.out.println("==HS== Implementing PK class: " + pk.getClass());
        }
}
catch (SSLPeerUnverifiedException e)
{
    e.printStackTrace();
}
    } });
//    */
                    socket.startHandshake();

                    /*
                    System.err.println(socket.getSSLParameters());//.getEndpointIdentificationAlgorithm()
                    System.err.println(java.util.Arrays.asList(socket.getSSLParameters().getProtocols()));
                    System.err.println(java.util.Arrays.asList(socket.getSSLParameters().getCipherSuites()));
                    System.err.println(socket.getSSLParameters().getAlgorithmConstraints());
                    System.err.println(socket.getSSLParameters().getEndpointIdentificationAlgorithm());
                    System.err.println(socket.getSSLParameters().getServerNames());
                    //System.err.println("cert 0: " + socket.getSession().getPeerCertificates()[0]);
                    System.err.println(socket.getSession());
*/

                    SSLSession sess = socket.getSession();
//                    Thread.currentThread().sleep(200);System.exit(0);
//                    System.err.println("NORMAL SESSION = " + sess);
//                    System.err.println("MAIN THREADNAME: " + Thread.currentThread().getName());
                    assert protocol.equals(sess.getProtocol());
                    assert cipherSuite.equals(sess.getCipherSuite());

                    /*
                    Certificate[] certs = sess.getPeerCertificates();
                    int certCount = certs.length;
                    Certificate cert = certs[certCount - 1];
                    // for(Certificate cert : certs)
                    {
                        //                        System.out.println("cert format: " + cert.getPublicKey().getFormat());
                        //                        System.out.println("Implementing class: " + cert.getClass().getName());
                        if("X.509".equals(cert.getType()))
                        {
                            X509Certificate x509cert = (X509Certificate)cert;
                            if(null != x509cert.getSigAlgParams())
                                System.out.println("parameters: " + Arrays.asList(x509cert.getSigAlgParams()));
                        }
                        else
                            System.out.println("Unrecognized cert type: " + cert.getType());
                        PublicKey pk = cert.getPublicKey();
                        if("RSA".equals(pk.getAlgorithm()))
                        {
                            RSAPublicKey rsa = (RSAPublicKey)pk;
                            System.out.println("RSA mod length: " + rsa.getModulus().bitLength());
                        } else {
                            System.out.println("UNKNOWN Certificate algorithm: " + pk.getAlgorithm());
                        }
                        System.out.println("Implementing PK class: " + pk.getClass());
                    }
*/
                    status = "Accepted";
                }
                catch (SSLHandshakeException she)
                {
                    Throwable cause = she.getCause();
                    if(null != cause && cause instanceof CertificateException) {
                        status = "Untrusted";
                        error = "Server certificate is not trusted. All other connections will fail similarly.";
                        stop = true;
                    } else
                        status = "Rejected";

                    if(showHandshakeErrors)
                        error = "SHE: " + she.getLocalizedMessage() + ", type=" + she.getClass().getName() + ", nested=" + she.getCause();
                }
                catch (SSLException ssle)
                {
                    if(showSSLErrors)
                        error = "SE: " + ssle.getLocalizedMessage();

                    status = "Rejected";
                }
                catch (SocketTimeoutException ste)
                {
                    if(showErrors)
                        error = "SocketException" + ste.getLocalizedMessage();

                    status = "Timeout";
                }
                catch (SocketException se)
                {
                    if(showErrors)
                        error = se.getLocalizedMessage();

                    status = "Failed";
                }
                catch (IOException ioe)
                {
                    if(showErrors)
                        error = ioe.getLocalizedMessage();

                    ioe.printStackTrace();
                    status = "Failed";
                }
                catch (Exception e)
                {
                    if(showErrors)
                        error = e.getLocalizedMessage();

                    e.printStackTrace();
                    status = "Failed";
                }
                finally
                {
                    if(null != socket) try { socket.close(); }
                    catch (IOException ioe) { ioe.printStackTrace(); }
                }

                if(null != error)
                    System.out.print(String.format(errorReportFormat,
                                                   status,
                                                   protocol,
                                                   cipherSuite,
                                                   error));
                else if(!hideRejects || !"Rejected".equals(status))
                    System.out.print(String.format(reportFormat,
                                                   status,
                                                   protocol,
                                                   cipherSuite));
            }
        }

        if(supportedProtocols.isEmpty())
        {
            System.err.println("This client supports none of the requested protocols: "
                               + Arrays.asList(sslEnabledProtocols));
            System.err.println("Exiting.");
            System.exit(1);
        }
        }

        // Now get generic and allow the server to decide on the protocol and cipher suite
        String[] protocolsToTry = supportedProtocols.toArray(new String[supportedProtocols.size()]);

        // If the user didn't provide a specific set of cipher suites,
        // use the system's *complete* set of supported cipher suites.
        if(null == sslCipherSuites)
            sslCipherSuites = getJVMSupportedCipherSuites(sslProtocol, rand);

        // Java 9-10 doesn't seem to like having any DTLS protocols
        // in the list of enabled protocols.
        // Java 11 seems okay with DTLS being in the mix.
        String javaVersion = System.getProperty("java.vm.specification.version", null);
        if(null != javaVersion) {
            double jv = Double.parseDouble(javaVersion);
            if(jv == 9 || jv == 10) {
                ArrayList<String> cleansedProtocolNames = new ArrayList<String>(protocolsToTry.length);
                for(String protocol : protocolsToTry)
                    if(!protocol.startsWith("DTLS"))
                        cleansedProtocolNames.add(protocol);

                protocolsToTry = cleansedProtocolNames.toArray(new String[cleansedProtocolNames.size()]);
            }
        }

        SSLSocketFactory sf = SSLUtils.getSSLSocketFactory(sslProtocol,
                                                           protocolsToTry,
                                                           sslCipherSuites,
                                                           rand,
                                                           trustManagers,
                                                           keyManagers);

        SSLSocket socket = null;

        try
        {
            socket = createSSLSocket(address, host, port, connectTimeout, readTimeout, sf);

            try
            {
                socket.startHandshake();

                System.out.print("Given this client's capabilities ("
                        + supportedProtocols
                        + "), the server prefers protocol=");
                System.out.print(socket.getSession().getProtocol());
                System.out.print(", cipher=");
                System.out.println(socket.getSession().getCipherSuite());

                if(showCerts)
                {
                    System.out.println("Attempting to check certificates:");
                    Certificate[] certs = socket.getSession().getPeerCertificates();
                    int i = 1;
                    for(Certificate cert : certs)
                    {
                        String certType = cert.getType();
                        if("X.509".equals(certType))
                        {
                            System.out.println("Certificate " + (i++) + ": " + getCertificateType((X509Certificate)cert));
                            X509Certificate x509 = (X509Certificate)cert;
                            dumpCertificate(x509);
                        }
                        else
                        {
                            System.out.println("Unknown certificate type (" + certType + "): " + cert);
                        }
                    }

                    if(certs instanceof X509Certificate[]
                       && checkTrust((X509Certificate[])certs, trustManagers))
                        System.out.println("Certificate chain is trusted");
                    else
                        System.out.println("Certificate chain is UNTRUSTED");
                }
            }
            catch (SocketException se)
            {
                System.out.println("Error during connection handshake for protocols "
                                   + supportedProtocols
                                   + ": server likely does not support any of these protocols.");

                if(showCerts)
                    System.out.println("Unable to show server certificate without a successful handshake.");
            } catch (SSLHandshakeException she) {
                Throwable cause = she.getCause();
                if(cause instanceof CertificateException)
                    System.out.println("Server certificate is not trusted, cannot complete handshake. Try -no-check-certificate");

                if(showCerts)
                    System.out.println("Unable to show server certificate without a successful handshake.");
            }
        }
        finally
        {
            if (null != socket) try { socket.close(); }
            catch (IOException ioe) { ioe.printStackTrace(); }
        }

/*
        System.out.println("Attempting to determine the server's SSLv2 capabilities using OpenSSL s_client...");
        // Try an SSLv2 connection with OpenSSL's s_client, just for kicks.
        Process p = Runtime.getRuntime().exec(new String[] {
                "openssl", "s_client",
                "-ssl2",
                "-connect", host + ":" + port }
        );
        // Make sure this process isn't trying to read from stdin
        OutputStream out = p.getOutputStream();
        out.close();

        InputStream stdout = p.getInputStream();
        InputStream stderr = p.getErrorStream();

        // Use NIO so we don't block like an idiot
        ReadableByteChannel in = Channels.newChannel(stdout);
        ReadableByteChannel err = Channels.newChannel(stderr);
        ByteBuffer buf = ByteBuffer.allocate(4096);
        StringBuilder outsb = new StringBuilder();
        StringBuilder errsb = new StringBuilder();

        byte[] buffer = new byte[4096];
        boolean outDone = false, errDone = false;

        do
        {
            int read;
*/
/*
            if(!outDone) {
                read = in.read(buf);
                if(-1 != read) {
                    buf.flip();
                    buf.get(buffer, 0, read);
                    System.out.println("Read " + read + " from stdout");
                    outsb.append(new String(buffer, 0, read));
                } else {
                    outDone = true;
                    // System.out.println("Output stream is done");
                }

                buf.flip();
            }
*/
/*
outDone = true;
            if(!errDone) {
                read = err.read(buf);
                if(-1 == read) {
                    buf.flip();
                    buf.get(buffer, 0, read);
                    System.out.println("Read " + read + " from stderr");
                    errsb.append(new String(buffer, 0, read));
                } else {
                    errDone = true;
                    // System.out.println("Error stream is done");
                }

                buf.flip();
            }

            Thread.sleep(100);
        } while(!outDone && !errDone);
        int status = p.waitFor();
        System.out.println("finally read " + err.read(buf) + " from stderr");
        if(0 < outsb.length())
            System.out.print("STDOUT: " + outsb);
        if(0 < errsb.length())
            System.out.print("STDERR: " + errsb);
        System.out.println("Process exit code was: " + status);
        if(outsb.toString().contains("SSL handshake")) {
            System.out.println("!!! host " + host + " supports SSLv2");
        }
*/
    }

    public static void listCurves(PrintStream out) throws Exception {
        Provider[] ecProviders = Security.getProviders("AlgorithmParameters.EC");
        if(null != ecProviders) {
            for(Provider provider : ecProviders) {
                // Unfortunately, we have to parse this string
                out.println("Provider: " + provider);
                String list = provider.getService("AlgorithmParameters", "EC").getAttribute("SupportedCurves");
                // Be nice and sort the list of curves
                TreeMap<String,String> sorted = new TreeMap<String,String>();
                for(String curve : list.split("\\|"))
                    sorted.put(curve.toLowerCase(), curve);
                for(String curve : sorted.values())
                    out.println(curve);
            }
        }

    }

    private static SSLSocket createSSLSocket(InetSocketAddress address,
                                             String host,
                                             int port,
                                             int readTimeout,
                                             int connectTimeout,
                                             SSLSocketFactory sf)
        throws IOException
    {
        //
        // Note: SSLSocketFactory has several create() methods.
        // Those that take arguments all connect immediately
        // and have no options for specifying a connection timeout.
        //
        // So, we have to create a socket and connect it (with a
        // connection timeout), then have the SSLSocketFactory wrap
        // the already-connected socket.
        //
        Socket sock = new Socket();
        sock.setSoTimeout(readTimeout);
        sock.connect(address, connectTimeout);

        // Wrap plain socket in an SSL socket
        return (SSLSocket)sf.createSocket(sock, host, port, true);
    }

    private static String[] getJVMSupportedCipherSuites(String protocol, SecureRandom rand)
        throws NoSuchAlgorithmException, KeyManagementException
    {
        SSLContext sc = SSLContext.getInstance(protocol);

        sc.init(null, null, rand);

        return sc.getSocketFactory().getSupportedCipherSuites();
    }

    private static String[] getJVMDefaultCipherSuites(String protocol, SecureRandom rand)
            throws NoSuchAlgorithmException, KeyManagementException
        {
            SSLContext sc = SSLContext.getInstance(protocol);

            sc.init(null, null, rand);

            return sc.getSocketFactory().getDefaultCipherSuites();
        }

    private static boolean checkTrust(X509Certificate[] chain, TrustManager[] trustManagers)
    {
        if(null == trustManagers)
            return false;

        if(1 == trustManagers.length
           && trustManagers[0] instanceof SSLUtils.TrustAllTrustManager)
            System.out.println("NOTE: Certificate chain will be trusted because all certificates are trusted");

        for(TrustManager tm : trustManagers) {
            if(tm instanceof X509TrustManager) {
                try {
                    ((X509TrustManager)tm).checkServerTrusted(chain, "RSA"); // TODO: Not always RSA?
                    return true;
                } catch (CertificateException ce) {
                    return false;
                }
            }
        }

        return false;
    }

    static void dumpCertificate(X509Certificate cert) throws GeneralSecurityException {
        System.out.print("  Subject: ");
        System.out.println(cert.getSubjectDN());
        Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
        if(null != altNames && !altNames.isEmpty()) {
            System.out.print("Subject Alternative Names (SANs): ");

            // NOTE: cert.getSubjectAlternativeNames is a really bad API
            boolean needsComma = false;
            for(List<?> sanList : altNames) {
               if(null != sanList && !sanList.isEmpty() && sanList.size() > 1) {
                   if(needsComma) System.out.print(", ");
                   else needsComma = true;
                   // Ignore sanList[0]
                   // sanList[1] is either a String or a byte array :(
                   Object thing = sanList.get(1);
                   if(thing instanceof String) {
                       System.out.print(thing);
                   } else {
                       System.out.print("[binary]"); // TODO
                   }
               }
            }
            System.out.println();
        }

        System.out.print("  Issuer: ");
        System.out.println(cert.getIssuerDN());

        System.out.print("  Signature Algorithm: ");
        System.out.println(cert.getSigAlgName());

        System.out.print("  SHA-256 Fingerprint: ");
        MessageDigest md = MessageDigest.getInstance("SHA-256");

        byte[] sig = md.digest(cert.getEncoded());

        System.out.println(toHexString(sig, ":"));

        System.out.println("  Valid from " + cert.getNotBefore() + " until " + cert.getNotAfter());
        Date now = new Date();
        System.out.println("  Currently valid: " + (cert.getNotBefore().before(now) && cert.getNotAfter().after(now)));
    }

    static String getCertificateType(X509Certificate cert) {
        PublicKey pubKey = cert.getPublicKey();

        if(pubKey instanceof RSAPublicKey) {
            return ((RSAPublicKey)pubKey).getModulus().bitLength() + "-bit RSA";
        } else if(pubKey instanceof ECPublicKey) {
            ECParameterSpec params = ((ECPublicKey)pubKey).getParams();
            if(null != params) {
                return params.getOrder().bitLength() + "-bit Elliptic-curve";
            } else {
                return "Unknown strength Elliptic-curve";
            }
        } else if(pubKey instanceof DSAPublicKey) {
            DSAParams params = ((DSAPublicKey)pubKey).getParams();
            if(null != params) {
                return params.getP().bitLength() + "-bit DSA";
            } else {
                return ((DSAPublicKey)pubKey).getY().bitLength() + "-bit DSA";
            }
        } else {
            return "Other X.509 key, class=" + pubKey.getClass().getName();
        }
    }

    static final char[] hexChars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f' };
    static String toHexString(byte[] bytes, String separator)
    {
        StringBuilder sb = new StringBuilder(bytes.length * 2);

        boolean first = true;
        for(byte b : bytes) {
            if(first) first = false;
            else if(null != separator) sb.append(separator);
            sb.append(hexChars[(b >> 4) & 0x0f])
              .append(hexChars[b & 0x0f]);
        }

        return sb.toString();
    }
}