Exception Reporting Difference

Bradford Wetmore bradford.wetmore at oracle.com
Wed Aug 8 00:45:41 UTC 2018


Hi Xuelei,

We've noticed a significant difference in the way exceptions are being 
reported/consumed post TLSv1.3 integration.

In the original SSLEngine API/implementation, my design was to fail-fast 
and let the application know of problems immediately, then the app could 
recover and close by doing the necessary I/O.  If some call generated a 
Exception/Alert (from a warning (connection stays open) or fatal 
(connection must close)), the Exception would be reported immediately, 
and then the SSLEngine could continue on and continue to consume/produce 
as required, closing/alerting if necessary, or just carrying on if it's 
only a warning.  You'd wrap the outbound alert later.

For example, in the case of a failed ALPN negotiation, say the client 
server set the ALPN values to something that doesn't intersect, and the 
connection will fail:

     // produce CH+ALPN ext.
     client.wrap();     // OK/NEED_UNWRAP

     // consume CH+ALPN ext.
     server.unwrap();   // OK/NEED_TASK

     // Generate Exception/Alert which is placed in outbound queue.
     serverTask.run();  // no output/exception yet

     // Let caller immediately know server is in trouble.
     server.wrap();     // throws SSLHandshakeException (No matching
                        // ALPN), server.getHandshakeStatus() = NEED_WRAP

     // produce the alert for client.
     server.unwrap();   // optional:  CLOSED/NEED_WRAP
     server.wrap();     // wraps alert (7 bytes), CLOSED/NEED_UNWRAP
                        //
                        // I think the NEED_UNWRAP is an existing bug.
                        // A fatal alert should be close the connection,
                        // with no more handshaking possible.

     // Client receives alert, report trouble.
     client.unwrap();   // throws SSLHandshakeException
                        // (no task necessary)
                        // getHandshakeStatus() = NOT_HANDSHAKING

     both isInboundDone()/isOutboundDone() are true.

At this point, both sides are closed.

In the new code we have:

     // produce CH+ALPN ext.
     client.wrap();     // OK/NEED_UNWRAP

     // consume CH+ALPN ext.
     server.unwrap();   // OK/NEED_TASK

     // Generate Exception/Alert which is placed in outbound queue.
     serverTask.run();  // no output/exception yet

*DIFFERENCE starts here*
     // Does not let caller immediately know that server had trouble.
     // Just quietly succeeds.
     server.wrap();     // wraps alert, returns CLOSED/NOT_HANDSHAKING

     // Client receives alert, parses
     client.unwrap();   // Reports SSLSHandshakeException
                        // getHandshakeStatus() = NOT_HANDSHAKING

     server.unwrap();   // throws SSLHandshakeException (No matching
                        // ALPN)
                        // server.getHandshakeStatus() = NOT_HANDSHAKING

     both isInboundDone()/isOutboundDone() are true.

While ultimately the final state eventually becomes the same, it's 
strange to me that the caller can suddenly transition from a seemingly 
fine state to closed and NOT_HANDSHAKING with no warning.  Existing apps 
closely following the handshake (isIn/OutboundDone()) state may be 
surprised to see this, and may not know that an extra wrap()/unwrap() 
after the engine is CLOSED/NOT_HANDSHAKING is now needed to get the true 
underlying cause of the error.

The attached test passes with both implementations only because of the 
way it was written 
(client.wrap/server.wrap/client.unwrap/server.unwrap), but it wouldn't 
if the loop was just looking for isIn/OutboundClosed(), and the 
exception would go missing.

I'm just not comfortable with the new implementation.

Thanks,

Brad
-------------- next part --------------
import static javax.net.ssl.SSLEngineResult.HandshakeStatus;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.SecureRandom;

public class TestSSLEngineALPN {

    private KeyStore ks = null;

    private String keystore = "D:\\intellij\\JCK11\\api\\tests\\api\\javax_net\\ssl\\testkeyspkcs";
    private String ksPassword = "DukesSecretPassword";
    private char[] passphrase = ksPassword.toCharArray();

    private ByteBuffer clientIn;
    private ByteBuffer serverIn;

    private ByteBuffer clientToServer;
    private ByteBuffer serverToClient;

    private ByteBuffer clientOut;
    private ByteBuffer serverOut;
    private static boolean resultOnce = true;

    private SSLEngineResult clientResult;
    private SSLEngineResult serverResult;

    public static void main(String[] args) throws Exception{
        new TestSSLEngineALPN().test();
    }

    private void test() throws Exception {

        String[] clientStrings = { "placeholder", "http/1.1" };
        String[] serverStrings = { "h2" };

        SSLContext sslContext = createInitSSLContext();

        SSLEngine clientEngine = sslContext.createSSLEngine();
        SSLEngine serverEngine = sslContext.createSSLEngine();

        createBuffers(clientEngine);

        clientEngine.setUseClientMode(true);
        serverEngine.setUseClientMode(false);
        serverEngine.setNeedClientAuth(true);


        //set client and server application protocol.
        SSLParameters paramsClient = clientEngine.getSSLParameters();
        paramsClient.setApplicationProtocols(clientStrings);
        clientEngine.setSSLParameters(paramsClient);

        SSLParameters paramsServer = serverEngine.getSSLParameters();
        paramsServer.setApplicationProtocols(serverStrings);
        serverEngine.setSSLParameters(paramsServer);


        clientResult = clientEngine.unwrap(serverToClient, clientIn);
        serverResult = serverEngine.wrap(serverOut, clientToServer);

        checkHandshakeResult(clientResult,HandshakeStatus.NEED_WRAP,0,0,false);
        checkHandshakeResult(serverResult,HandshakeStatus.NEED_UNWRAP,0,0,false);

        //complete handshake and exchange some data
        if(!doHandShakeAppData(clientEngine, serverEngine)){
            System.out.println("********************************************************");
            System.out.println("Handshake failed with SSLException during SSLEngine#wrap, This is OK,  when "
                      + "negotiation fail");
            System.out.println("********************************************************");
            return;
        }

        try {
            // No common application protocols.   Underlying protocol can determine what to do.
            // It is very possible that the handshake throws an exception and we never get here
            String negotiatedALPN = clientEngine.getApplicationProtocol();
            // If we do get here, the negotiated application protocols should be an empty string
            assert "".equals(negotiatedALPN);
        } catch (UnsupportedOperationException uoe) {
            System.out.println("UnsupportedOperationException Caught - expected if provider does not support "
                      + "getApplicationProtocol()");
        }

    }


    private boolean  doHandShakeAppData(SSLEngine clientEngine, SSLEngine serverEngine) throws Exception {
        System.out.println("============HANDSHAKE STARTED============");
        clientEngine.beginHandshake();
        serverEngine.beginHandshake();
        while(!isNotHandshaking()){
            System.out.println("================");

            clientResult = clientEngine.wrap(clientOut, clientToServer);
            log("client wrap: ", clientResult);
            runDelegatedTasks(clientResult, clientEngine);

            try{
                serverResult = serverEngine.wrap(serverOut, serverToClient);
                log("server wrap: ", serverResult);


                if(serverResult.getStatus() == SSLEngineResult.Status.CLOSED){
                    System.out.println("********************************************************************************");
                    System.out.println("SSLEngine#wrap does not throw SSLException with JDKb20 , it closed the server, "
                              + "Please run with JDKb15 and you will receive SSLException ");

                    serverToClient.flip();
                    byte[] sToCBytes = new byte[serverToClient.limit()];
                    serverToClient.get(sToCBytes, 0, sToCBytes.length);
                    System.out.println("Generated bytes during the closure is :"+
                              new String(sToCBytes));

                    System.out.println("********************************************************************************");
                    //System.exit(-1);
                }
            }catch (SSLException e){
                //e.printStackTrace();
                return false;
            }
            runDelegatedTasks(serverResult, serverEngine);

            clientToServer.flip();
            serverToClient.flip();

            //Handshake ends here,hence below unwrap is missed ,
            // so additional unwrap needs to be done after handshake
            if(isNotHandshaking()){
                System.out.println("Breaking...");
                break;
            }
            System.out.println("----");

            clientResult = clientEngine.unwrap(serverToClient, clientIn);
            log("client unwrap: ", clientResult);
            runDelegatedTasks(clientResult, clientEngine);

            serverResult = serverEngine.unwrap(clientToServer, serverIn);
            log("server unwrap: ", serverResult);
            runDelegatedTasks(serverResult, serverEngine);

            clientToServer.compact();
            serverToClient.compact();
        }
        System.out.println("============HANDSHAKE COMPLETE============");
        return true;
    }


    /*
     * If the result indicates that we have outstanding tasks to do,
     * go ahead and run them in this thread.
     */
    private static void runDelegatedTasks(SSLEngineResult result,
                                          SSLEngine engine) throws Exception {

        if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
            Runnable runnable;
            while ((runnable = engine.getDelegatedTask()) != null) {
                System.out.println("\trunning delegated task...");
                runnable.run();
            }
            HandshakeStatus hsStatus = engine.getHandshakeStatus();
            if (hsStatus == HandshakeStatus.NEED_TASK) {
                throw new Exception(
                          "handshake shouldn't need additional tasks");
            }
            System.out.println("\tnew HandshakeStatus: " + hsStatus);
        }
    }

    private static void log(String str, SSLEngineResult result) {
        if (resultOnce) {
            resultOnce = false;
            System.out.println("The format of the SSLEngineResult is: \n" +
                      "\t\"getStatus() / getHandshakeStatus()\" +\n" +
                      "\t\"bytesConsumed() / bytesProduced()\"\n");
        }
        HandshakeStatus hsStatus = result.getHandshakeStatus();
        System.out.println(str +
                  result.getStatus() + "/" + hsStatus + ", " +
                  result.bytesConsumed() + "/" + result.bytesProduced() +
                  " bytes");
        if (hsStatus == HandshakeStatus.FINISHED) {
            System.out.println("\t...ready for application data");
        }
    }


    private boolean isNotHandshaking() {
        return  clientResult.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING &&
                  serverResult.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
    }


    private void createBuffers(SSLEngine sse1) {

        SSLSession session = sse1.getSession();

        int appBufferMax = session.getApplicationBufferSize();
        int netBufferMax = session.getPacketBufferSize();

        clientIn = ByteBuffer.allocateDirect(appBufferMax);
        serverIn = ByteBuffer.allocateDirect(appBufferMax);

        clientToServer = ByteBuffer.allocateDirect(netBufferMax);
        serverToClient = ByteBuffer.allocateDirect(netBufferMax);
        try {
            String clientMessae = "Good Morning Server, I'm Client";
            clientOut = ByteBuffer.wrap(clientMessae.getBytes("ISO-8859-1"));
            String serverMesage = "Greetings from Server";
            serverOut = ByteBuffer.wrap(serverMesage.getBytes("ISO-8859-1"));
        } catch (UnsupportedEncodingException uee) {
            // Should never happen
            throw new RuntimeException("getBytes(ISO-8859-1) threw an UnsupportedEncodingException", uee);
        }
    }


    void checkHandshakeResult(SSLEngineResult result, HandshakeStatus status, int consumed, int
              produced, boolean done) {
        boolean returnCode = true;
        if ((status != null) && (result.getHandshakeStatus() != status)) {
            returnCode = false;
            System.err.println("Unexpected Status: need = " + status + " got = " + result.getStatus());
            System.exit(1);
        }
        if ((consumed != -1) && (consumed != result.bytesConsumed())) {
            returnCode = false;
            System.err.println("Unexpected consumed: need = " + consumed + " got = " + result.bytesConsumed());
            System.exit(1);
        }
        if ((produced != -1) && (produced != result.bytesProduced())) {
            returnCode = false;
            System.err.println("Unexpected produced: need = " + produced + " got = " + result.bytesProduced());
            System.exit(1);
        }
        if (done && (status == HandshakeStatus.FINISHED)) {
            returnCode = false;
            System.err.println("Handshake already reported finished");
            System.exit(1);
        }
    }


    private SSLContext createInitSSLContext() throws Exception {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        KeyManagerFactory kmf = null;
        TrustManagerFactory tmf = null;
        kmf =  KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        tmf =  TrustManagerFactory.getInstance("PKIX");
        ks = KeyStore.getInstance("PKCS12");
        ks.load(new FileInputStream(keystore), passphrase);
        kmf.init(ks, passphrase);
        tmf.init(ks);
        sslContext.init(kmf.getKeyManagers(),
                  tmf.getTrustManagers(),
                  new SecureRandom());
        return sslContext;
    }
}


More information about the security-dev mailing list