12

I have a requirement to convert certain bash scripts to java and one such script connects to a server using openssl with a vanity-url as a parameter to check if that is connectable using that vanity-url. See command below

/usr/bin/openssl s_client -connect api.sys.found1.cf.company.com:443 -servername www.app.company.com 2>/dev/null

I wanted to do the similar activity in java and test the connectivity. Any ideas on how to make a open-ssl connection using Java .. Is this something that I need to use external Library ?

Arun
  • 3,440
  • 11
  • 60
  • 108
  • Why? where is the benefit here? – Michael-O Nov 08 '19 at 13:07
  • The benefit is that I programmatically check if an certificate exist over there and if not would trigger another job that would generate a certificate – Arun Nov 08 '19 at 13:52
  • You can do that with a Bash script also. `s_client` does what you need. I see a lot of boilerplate code in java which can be done with a few lines in shell and `openssl(1)`. – Michael-O Nov 08 '19 at 14:15
  • Nope... I didn’t want to do in Bash-script... I want this to be written in Java and expose it as an API through Springboot – Arun Nov 08 '19 at 14:17
  • Alright, you should have mentioned this in your question because the solution you strived for looked like overkill. – Michael-O Nov 08 '19 at 14:19
  • I was very specific to the Technology and my question is only about coming out from OpenSSL.. which means am already aware that OpenSSL can do this and also am aware of OpenSSL Command too – Arun Nov 08 '19 at 14:24
  • I know this is a 3 year old question but one useful use-case here : checking for TLS 1.3 compatibility when your available Java supports it but not your OpenSSL version (Hello RHEL6) – mveroone May 25 '22 at 08:25

3 Answers3

9

I was able to achieve this by referring the document over here

Basically, a SSLEngine needs to be created and make a successful handshake along with SNI

 private SocketChannel createSocketChannel() throws IOException {
        InetSocketAddress socketAddress = new InetSocketAddress(PROXY_ADDRESS, PROXY_PORT);
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(socketAddress);
        socketChannel.configureBlocking(false);
        return socketChannel;

    }

private SSLContext createSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext sslContext = SSLContext.getInstance(TLS_VERSION);
        sslContext.init(null,null,null);
        return sslContext;
    }




private SSLEngine createSSLEngine() throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext sslContext = createSSLContext();
        SSLEngine sslEngine = sslContext.createSSLEngine(PROXY_ADDRESS, PROXY_PORT);
        sslEngine.setUseClientMode(true);

        List<SNIServerName> serverNameList = new ArrayList<>();
        serverNameList.add(new SNIHostName(SNI_HOST_NAME));
        SSLParameters sslParameters = sslEngine.getSSLParameters();
        sslParameters.setServerNames(serverNameList);

        sslEngine.setSSLParameters(sslParameters);

        return sslEngine;
    }

After creating SSLEngine, the handShake has to begin

SocketChannel channel = createSocketChannel();
SSLEngine sslEngine = createSSLEngine();
doHandShake(sslEngine,channel);


 private void doHandShake(SSLEngine sslEngine, SocketChannel socketChannel) throws Exception {
        System.out.println("Going to do Handshake");

        SSLSession session = sslEngine.getSession();

        ByteBuffer myAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
        ByteBuffer peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());

        ByteBuffer myNetData = ByteBuffer.allocate(session.getPacketBufferSize());
        ByteBuffer peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());

        sslEngine.beginHandshake();
        SSLEngineResult result;

        handshakeStatus = sslEngine.getHandshakeStatus();

        while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED &&
                handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {


            switch (handshakeStatus) {
                case NEED_UNWRAP:
                    if (! (socketChannel.read(peerNetData) <0)) {
                        peerNetData.flip();
                        result = sslEngine.unwrap(peerNetData,peerAppData);
                        peerNetData.compact();

                        handshakeStatus = result.getHandshakeStatus();

                        switch (result.getStatus()) {
                            case OK:
                                break;
                        }

                    }

                    break;
                case NEED_WRAP:
                    myNetData.clear() ;// Empty the local network packet buffer
                    result = sslEngine.wrap(myAppData,myNetData);
                    handshakeStatus = result.getHandshakeStatus();
                    switch (result.getStatus()) {
                        case OK:
                            myNetData.flip();
                            while (myNetData.hasRemaining()) {
                                socketChannel.write(myNetData);
                            }
                    }
                    break;

                case NEED_TASK:
                    Runnable task  = sslEngine.getDelegatedTask();
                    if (null!=task) {
                        task.run();
                    }
                    handshakeStatus = sslEngine.getHandshakeStatus();
                    break;
            }


        }

Once the handShake is done. you can get the Principal object

Principal principal = sslEngine.getSession().getPeerPrincipal();

            if (principal.getName().contains(SNI_HOST_NAME)) {
                System.out.println("available ... ");
            }else {
                System.out.println("Not available");
            }
Arun
  • 3,440
  • 11
  • 60
  • 108
3

call isAliasExists with your values ,

isAliasExists("api.sys.found1.cf.company.com","www.app.company.com");

Returns true if your alias (servername) is part of the cert,

private static boolean isAliasExists(String hostName, String alias) throws Exception  {
        String host;
        int port;
        String[] parts = hostName.split(":");
        host = parts[0];
        port = (parts.length == 1) ? 443 : Integer.parseInt(parts[1]);
        // key store password
        char[] passphrase = "changeit".toCharArray();
        File file = new File("jssecacerts");
        if (file.isFile() == false) {
            char SEP = File.separatorChar;
            File dir = new File(System.getProperty("java.home") + SEP + "lib" + SEP + "security");
            file = new File(dir, "jssecacerts");
            if (file.isFile() == false) {
                file = new File(dir, "cacerts");
            }
        }
        InputStream in = new FileInputStream(file);
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(in, passphrase);
        in.close();
        SSLContext context = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);
        X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
        SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
        context.init(null, new TrustManager[] { tm }, null);
        SSLSocketFactory factory = context.getSocketFactory();
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        try {
            System.out.println("Starting SSL handshake...");
            socket.startHandshake();
            socket.close();
            System.out.println("Certificate is already trusted");
        } catch (SSLException e) {
            e.printStackTrace();
        }

        X509Certificate[] chain = tm.chain;

        List<String> altNames=new ArrayList<String>();

        for (X509Certificate cert: chain)
        {
            altNames.addAll(getSubjectAltNames(cert));
        }

        for(String altName: altNames) {
            if(altName.trim().contains(alias))
               return true;
        }

        if (chain == null) {
            System.out.println("Could not obtain server certificate chain");
            return false;
        }



        return false;
    }

Returns list of alternative names from cert,

private static List<String> getSubjectAltNames(X509Certificate certificate) throws CertificateParsingException {
         List<String> result = new ArrayList<>();
         try {
          Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
          if (subjectAltNames == null) {
           return Collections.emptyList();
          }
          for (Object subjectAltName : subjectAltNames) {
           List<?> entry = (List<?>) subjectAltName;
           if (entry == null || entry.size() < 2) {
            continue;
           }
           Integer altNameType = (Integer) entry.get(0);
           if (altNameType == null) {
            continue;
           }
            String altName = (String) entry.get(1);
            if (altName != null) {
             result.add(altName);
            }
          }
          return result;
         } catch (CertificateParsingException e) {
          return Collections.emptyList();
         }
        }

custom trust manager,

private static class SavingTrustManager implements X509TrustManager {

        private final X509TrustManager tm;
        private X509Certificate[] chain;

        SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }

        public X509Certificate[] getAcceptedIssuers() {

            return new X509Certificate[0];
            // throw new UnsupportedOperationException();
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            throw new UnsupportedOperationException();
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }
Srinivasan Sekar
  • 2,049
  • 13
  • 22
  • how to set `SNI` here ? According to this example the Server name as which am connecting is `www.app.company.com` – Arun May 16 '19 at 07:03
  • No need to , java will take care of it. – Srinivasan Sekar May 16 '19 at 07:11
  • Nope, i don't want to do that. My requirement is where the user will give a `url` which in this example is `www.app.company.com` and then i will have to verify that if that exists in `api.sys.found1.cf.company.com:443` . This example seems to load all the `cert` available under `api.sys.found1.cf.company.com` which i dont like and wanted to check only for that particular `SNI Host` – Arun May 16 '19 at 07:13
0

Without really knowing what SNI was I tried to get some insight with the test-program shown below.

I don't know the output from the openssl s_client command, but the test-program might prove to be a starting point. When the javax.net.debug output is turned on a lot of output is dumped of which only a few lines are relevant (see also the comments). That is a bit annoying and I do not have an easy solution for that. The TrustAllServers class can be reworked to inspect the certificates you expect to receive from the server (a.ka. host) for a particular domain. There might be other options (e.g. the socket's handshake methods) but this is as far as I got.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;

// https://stackoverflow.com/questions/56005883/java-equivalent-to-openssl-s-client-command
// Please use latest Java 8 version, bugs are around in earlier versions.
public class ServerNameTest {

    public static void main(String[] args) {

        // SSL debug options, see https://stackoverflow.com/q/23659564/3080094 and https://access.redhat.com/solutions/973783
        // System.setProperty("javax.net.debug", "all");
        // System.setProperty("javax.net.debug", "ssl:handshake");
        // System.setProperty("jsse.enableSNIExtension", "true"); // "true" is the default
        try {
            ServerNameTest sn = new ServerNameTest();
            // This will show 2 different server certificate chains.
            // Note this is a random server - please pick your own one.
            sn.test("major.io", "rackerhacker.com");
            sn.test("major.io", "major.io");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Done");
    }

    /*
     * With javax.net.debug output you should see something like:
     * <pre>
     * *** ClientHello
     * ...
     * Extension server_name, server_name: [type=host_name (0), value=DOMAIN;]
     * ...
     * *** ServerHello
     * ...
     * Extension server_name, server_name: 
     * ...
     * </pre>
     * Note that if the server does not provide a value for server_name, 
     * it does not actually mean the server does not support SNI/server_name (see https://serverfault.com/a/506303)
     */
    void test(String host, String domain) throws Exception {

        SSLParameters sslParams = new SSLParameters();
        if (domain != null && !domain.isEmpty()) {
            sslParams.setServerNames(Arrays.asList(new SNIHostName(domain)));
        }
        // Only for webservers: set endpoint algorithm to HTTPS 
        sslParams.setEndpointIdentificationAlgorithm("HTTPS");

        SSLSocketFactory sslsf = serverTrustingSSLFactory();
        try (SSLSocket socket = (SSLSocket) sslsf.createSocket()) {
            socket.setSSLParameters(sslParams);
            socket.setSoTimeout(3_000);
            System.out.println("Connecting to " + host + " for domain " + domain);
            socket.connect(new InetSocketAddress(host, 443), 3_000);
            // Trigger actual connection by getting the session.
            socket.getSession();
            System.out.println("Connected to remote " + socket.getRemoteSocketAddress());
            try (BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
                try (OutputStream out = socket.getOutputStream()) {
                    System.out.println(">> OPTIONS");
                    out.write("OPTIONS * HTTP/1.1\r\n\r\n".getBytes(StandardCharsets.UTF_8));
                    System.out.println("<< " + input.readLine());
                }
            } catch (Exception e) {
                System.err.println("No line read: " + e);
            }
        }
    }

    SSLSocketFactory serverTrustingSSLFactory() throws Exception {

        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, trustManager(), null);
        return ctx.getSocketFactory();
    }

    TrustManager[] trustManager() throws Exception {

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init( (KeyStore) null);
        // Must use "extended" type versus the default javax.net.ssl.X509TrustManager, 
        // otherwise the error "No subject alternative DNS name matching" keeps showing up.
        X509ExtendedTrustManager defaultManager = null;
        for (TrustManager trustManager : tmf.getTrustManagers()) {
            if (trustManager instanceof X509ExtendedTrustManager) {
                defaultManager = (X509ExtendedTrustManager) trustManager;
                break;
            }
        }
        if (defaultManager == null) {
            throw new RuntimeException("Cannot find default X509ExtendedTrustManager");
        }
        return new TrustManager[] { new TrustAllServers(defaultManager) };
    }

    static void printChain(X509Certificate[] chain) {

        try {
            for (int i = 0; i < chain.length; i++) {
                X509Certificate cert = chain[i];
                System.out.println("Cert[" + i + "] " + cert.getSubjectX500Principal() + " :alt: " + cert.getSubjectAlternativeNames());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class TrustAllServers extends X509ExtendedTrustManager {

        final X509ExtendedTrustManager defaultManager;

        public TrustAllServers(X509ExtendedTrustManager defaultManager) {
            this.defaultManager = defaultManager;
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultManager.checkServerTrusted(chain, authType);
            } catch (Exception e) {
                System.err.println("Untrusted server: " + e);
            }
            printChain(chain);
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
            try {
                defaultManager.checkServerTrusted(chain, authType, socket);
            } catch (Exception e) {
                System.err.println("Untrusted server for socket: " + e);
            }
            printChain(chain);
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
            try {
                defaultManager.checkServerTrusted(chain, authType, engine);
            } catch (Exception e) {
                System.err.println("Untrusted server for engine: " + e);
            }
            printChain(chain);
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            defaultManager.checkClientTrusted(chain, authType);
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
            defaultManager.checkClientTrusted(chain, authType, socket);
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
            defaultManager.checkClientTrusted(chain, authType, engine);
        }

        public X509Certificate[] getAcceptedIssuers() {
            return defaultManager.getAcceptedIssuers();
        }

    }

}

vanOekel
  • 6,358
  • 1
  • 21
  • 56