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