Inconsistent SSLEngine behavior for closing outbound while in handshake in 11ea22
Bradford Wetmore
bradford.wetmore at oracle.com
Sat Aug 11 01:14:49 UTC 2018
On 8/9/2018 5:57 PM, Xuelei Fan wrote:
> Thank you, Tim! Please feel free to submit bugs and comments.
I went through a standard SSLEngineTemplate handshake/data/close with a
fine-tooth comb (debugger), and found two bugs based on your -04 patch.
1. Things may have changed since the original SSLEngine implementation,
but unwrap() was supposed to return a BUFFER_UNDERFLOW if there is not
enough (or any) inbound data to make a packet. I'm seeing a mix of OK
and BUFFER_UNDERFLOW. I haven't determined the pattern, but you should
be able to run my test (attached) and see pretty quickly.
2. I haven't not had the cycles to follow the earlier discussion, so
this may be a duplicate of something already discussed, but I got myself
into an endless loop because the closes weren't happening as expected.
In the attached file, after the handshaking and data exchange, close
just one of the SSLEngines outbound side. I'll use client for this
example, but same error with server side:
// clientEngine.closeOutbound();
clientEngine.getHandshakeStatus() == NEED_WRAP
client.isInboundDone(): false
client.isOutboundDone(): false
// client.wrap() sends the Close Notify (CN).
client wrap: CLOSED/NEED_UNWRAP, 0/24 bytes
clientEngine.getHandshakeStatus() == NEED_UNWRAP
client.isInboundDone(): false
client.isOutboundDone(): true
// server.unwrap() parses the CN, and closes the inbound side.
server unwrap: CLOSED/NEED_WRAP, 24/0 bytes
clientEngine.getHandshakeStatus() == NEED_WRAP
server.isInboundDone(): true
server.isOutboundDone(): false
So far so good.
// server.wrap() needs to send the corresponding close_notify.
server wrap: OK/NEED_WRAP, 0/0 bytes
clientEngine.getHandshakeStatus() == NEED_WRAP
server.isInboundDone(): true
server.isOutboundDone(): false
Whoops, it didn't happen. No outbound data was generated. State is
still in NEED_WRAP. I just let the app run, it never closes because no
CN is generated on the server side.
Run the attached file for full details.
Thanks,
Brad
> Xuelei
>
> On 8/9/2018 4:23 PM, Tim Brooks wrote:
>> Hi Xuelei,
>>
>> My test passed using that patch. I’ll continue to explore over the
>> next few days. But that patch resolves the main issues that I had
>> encountered.
>>
>> Thanks,
>>
>> - Tim
>>
>>> On Aug 7, 2018, at 8:54 AM, Xuelei Fan <xuelei.fan at oracle.com
>>> <mailto:xuelei.fan at oracle.com>> wrote:
>>>
>>> Xuelei
>>
-------------- next part --------------
/*
* Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
// SunJSSE does not support dynamic system properties, no way to re-use
// system properties in samevm/agentvm mode.
/*
* @test
* @bug 1234567
* @summary XYZ
* @run main/othervm TestSSLEngineFailedALPN
*/
/**
* A SSLEngine usage example which simplifies the presentation
* by removing the I/O and multi-threading concerns.
*
* The test creates two SSLEngines, simulating a client and server.
* The "transport" layer consists two byte buffers: think of them
* as directly connected pipes.
*
* Note, this is a *very* simple example: real code will be much more
* involved. For example, different threading and I/O models could be
* used, transport mechanisms could close unexpectedly, and so on.
*
* When this application runs, notice that several messages
* (wrap/unwrap) pass before any application data is consumed or
* produced. (For more information, please see the SSL/TLS
* specifications.) There may several steps for a successful handshake,
* so it's typical to see the following series of operations:
*
* client server message
* ====== ====== =======
* wrap() ... ClientHello
* ... unwrap() ClientHello
* ... wrap() ServerHello/Certificate
* unwrap() ... ServerHello/Certificate
* wrap() ... ClientKeyExchange
* wrap() ... ChangeCipherSpec
* wrap() ... Finished
* ... unwrap() ClientKeyExchange
* ... unwrap() ChangeCipherSpec
* ... unwrap() Finished
* ... wrap() ChangeCipherSpec
* ... wrap() Finished
* unwrap() ... ChangeCipherSpec
* unwrap() ... Finished
*/
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.*;
import java.io.*;
import java.security.*;
import java.nio.*;
public class SSLEngineFailedALPN {
/*
* Enables logging of the SSLEngine operations.
*/
private static final boolean logging = true;
/*
* Enables the JSSE system debugging system property:
*
* -Djavax.net.debug=all
*
* This gives a lot of low-level information about operations underway,
* including specific handshake messages, and might be best examined
* after gaining some familiarity with this application.
*/
private static final boolean debug = true;
private final SSLContext sslc;
private SSLEngine clientEngine; // client Engine
private ByteBuffer clientOut; // write side of clientEngine
private ByteBuffer clientIn; // read side of clientEngine
private SSLEngine serverEngine; // server Engine
private ByteBuffer serverOut; // write side of serverEngine
private ByteBuffer serverIn; // read side of serverEngine
/*
* For data transport, this example uses local ByteBuffers. This
* isn't really useful, but the purpose of this example is to show
* SSLEngine concepts, not how to do network transport.
*/
private ByteBuffer cTOs; // "reliable" transport client->server
private ByteBuffer sTOc; // "reliable" transport server->client
/*
* The following is to set up the keystores.
*/
private static final String pathToStores = "../../../../javax/net/ssl/etc";
private static final String keyStoreFile = "keystore";
private static final String trustStoreFile = "truststore";
private static final String passwd = "passphrase";
private static final String keyFilename
= System.getProperty("test.src", ".") + "/" + pathToStores
+ "/" + keyStoreFile;
private static final String trustFilename
= System.getProperty("test.src", ".") + "/" + pathToStores
+ "/" + trustStoreFile;
/*
* Main entry point for this test.
*/
public static void main(String args[]) throws Exception {
if (debug) {
System.setProperty("javax.net.debug", "all");
}
SSLEngineFailedALPN test = new SSLEngineFailedALPN();
test.runTest();
System.out.println("Test Passed.");
}
/*
* Create an initialized SSLContext to use for these tests.
*/
public SSLEngineFailedALPN() throws Exception {
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore ts = KeyStore.getInstance("JKS");
char[] passphrase = passwd.toCharArray();
ks.load(new FileInputStream(keyFilename), passphrase);
ts.load(new FileInputStream(trustFilename), passphrase);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, passphrase);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
sslc = sslCtx;
}
/*
* Run the test.
*
* Sit in a tight loop, both engines calling wrap/unwrap regardless
* of whether data is available or not. We do this until both engines
* report back they are closed.
*
* The main loop handles all of the I/O phases of the SSLEngine's
* lifetime:
*
* initial handshaking
* application data transfer
* engine closing
*
* One could easily separate these phases into separate
* sections of code.
*/
private void runTest() throws Exception {
boolean dataDone = false;
createSSLEngines();
createBuffers();
// results from client's last operation
SSLEngineResult clientResult = null;
// results from server's last operation
SSLEngineResult serverResult = null;
/*
* Examining the SSLEngineResults could be much more involved,
* and may alter the overall flow of the application.
*
* For example, if we received a BUFFER_OVERFLOW when trying
* to write to the output pipe, we could reallocate a larger
* pipe, but instead we wait for the peer to drain it.
*/
Exception clientException = null;
Exception serverException = null;
while (!isEngineClosed(clientEngine)
|| !isEngineClosed(serverEngine)) {
log("================");
try {
clientResult = clientEngine.wrap(clientOut, cTOs);
log("client wrap: ", clientResult);
} catch (Exception e) {
clientException = e;
System.out.println("Client wrap() threw: " + e.getMessage());
}
runDelegatedTasks(clientResult, clientEngine);
log(clientEngine.getHandshakeStatus().toString());
log("client.isInboundDone(): " + clientEngine.isInboundDone());
log("client.isOutboundDone(): " + clientEngine.isOutboundDone());
try {
serverResult = serverEngine.wrap(serverOut, sTOc);
log("server wrap: ", serverResult);
} catch (Exception e) {
serverException = e;
System.out.println("Server wrap() threw: " + e.getMessage());
}
runDelegatedTasks(serverResult, serverEngine);
log(serverEngine.getHandshakeStatus().toString());
log("server.isInboundDone(): " + serverEngine.isInboundDone());
log("server.isOutboundDone(): " + serverEngine.isOutboundDone());
cTOs.flip();
sTOc.flip();
log("----");
try {
clientResult = clientEngine.unwrap(sTOc, clientIn);
log("client unwrap: ", clientResult);
} catch (Exception e) {
clientException = e;
System.out.println("Client unwrap() threw: " + e.getMessage());
}
runDelegatedTasks(clientResult, clientEngine);
log(clientEngine.getHandshakeStatus().toString());
log("client.isInboundDone(): " + clientEngine.isInboundDone());
log("client.isOutboundDone(): " + clientEngine.isOutboundDone());
try {
serverResult = serverEngine.unwrap(cTOs, serverIn);
log("server unwrap: ", serverResult);
} catch (Exception e) {
serverException = e;
System.out.println("Server unwrap() threw: " + e.getMessage());
}
runDelegatedTasks(serverResult, serverEngine);
log(serverEngine.getHandshakeStatus().toString());
log("server.isInboundDone(): " + serverEngine.isInboundDone());
log("server.isOutboundDone(): " + serverEngine.isOutboundDone());
cTOs.compact();
sTOc.compact();
/*
* After we've transfered all application data between the client
* and server, we close the clientEngine's outbound stream.
* This generates a close_notify handshake message, which the
* server engine receives and responds by closing itself.
*/
if (!dataDone && (clientOut.limit() == serverIn.position())
&& (serverOut.limit() == clientIn.position())) {
/*
* A sanity check to ensure we got what was sent.
*/
checkTransfer(serverOut, clientIn);
checkTransfer(clientOut, serverIn);
log("\tClosing clientEngine's *OUTBOUND*...");
clientEngine.closeOutbound();
log(clientEngine.getHandshakeStatus().toString());
log("client.isInboundDone(): " + clientEngine.isInboundDone());
log("client.isOutboundDone(): " + clientEngine.isOutboundDone());
dataDone = true;
log("\tClosing serverEngine's *OUTBOUND*...");
// serverEngine.closeOutbound();
log(serverEngine.getHandshakeStatus().toString());
log("server.isInboundDone(): " + serverEngine.isInboundDone());
log("server.isOutboundDone(): " + serverEngine.isOutboundDone());
}
}
if (clientException == null) {
throw new Exception("Client Exception not seen");
}
if (serverException == null) {
throw new Exception("SErver Exception not seen");
}
System.out.println("Test Passed.");
}
/*
* Using the SSLContext created during object creation,
* create/configure the SSLEngines we'll use for this test.
*/
private void createSSLEngines() throws Exception {
/*
* Configure the serverEngine to act as a server in the SSL/TLS
* handshake. Also, require SSL client authentication.
*/
serverEngine = sslc.createSSLEngine();
serverEngine.setUseClientMode(false);
serverEngine.setNeedClientAuth(true);
SSLParameters paramsServer = serverEngine.getSSLParameters();
paramsServer.setApplicationProtocols(new String[]{"one"});
serverEngine.setSSLParameters(paramsServer);
/*
* Similar to above, but using client mode instead.
*/
clientEngine = sslc.createSSLEngine("client", 80);
clientEngine.setUseClientMode(true);
SSLParameters paramsClient = clientEngine.getSSLParameters();
paramsClient.setApplicationProtocols(new String[]{"one"});
clientEngine.setSSLParameters(paramsClient);
}
/*
* Create and size the buffers appropriately.
*/
private void createBuffers() {
/*
* We'll assume the buffer sizes are the same
* between client and server.
*/
SSLSession session = clientEngine.getSession();
int appBufferMax = session.getApplicationBufferSize();
int netBufferMax = session.getPacketBufferSize();
/*
* We'll make the input buffers a bit bigger than the max needed
* size, so that unwrap()s following a successful data transfer
* won't generate BUFFER_OVERFLOWS.
*
* We'll use a mix of direct and indirect ByteBuffers for
* tutorial purposes only. In reality, only use direct
* ByteBuffers when they give a clear performance enhancement.
*/
clientIn = ByteBuffer.allocate(appBufferMax + 50);
serverIn = ByteBuffer.allocate(appBufferMax + 50);
cTOs = ByteBuffer.allocateDirect(netBufferMax);
sTOc = ByteBuffer.allocateDirect(netBufferMax);
clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes());
serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes());
}
/*
* 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) {
log("\trunning delegated task...");
runnable.run();
}
HandshakeStatus hsStatus = engine.getHandshakeStatus();
if (hsStatus == HandshakeStatus.NEED_TASK) {
throw new Exception(
"handshake shouldn't need additional tasks");
}
log("\tnew HandshakeStatus: " + hsStatus);
}
}
private static boolean isEngineClosed(SSLEngine engine) {
return (engine.isOutboundDone() && engine.isInboundDone());
}
/*
* Simple check to make sure everything came across as expected.
*/
private static void checkTransfer(ByteBuffer a, ByteBuffer b)
throws Exception {
a.flip();
b.flip();
if (!a.equals(b)) {
throw new Exception("Data didn't transfer cleanly");
} else {
log("\tData transferred cleanly");
}
a.position(a.limit());
b.position(b.limit());
a.limit(a.capacity());
b.limit(b.capacity());
}
/*
* Logging code
*/
private static boolean resultOnce = true;
private static void log(String str, SSLEngineResult result) {
if (!logging) {
return;
}
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();
log(str
+ result.getStatus() + "/" + hsStatus + ", "
+ result.bytesConsumed() + "/" + result.bytesProduced()
+ " bytes");
if (hsStatus == HandshakeStatus.FINISHED) {
log("\t...ready for application data");
}
}
private static void log(String str) {
if (logging) {
System.out.println(str);
}
}
}
More information about the security-dev
mailing list