Skip to content

Commit 9ee1aaf

Browse files
committed
Throw SSLException if SSLEngine inbound is closed before outbound.
Fixes google#839. This should be _mostly_ uncontroversial as it is already documented to do so[1] but could cause app compat issues. A quick scan of AOSP suggests no major issues however there is a CTS test for the old behaviour[2] which will need changing. The bulk of this change is regression tests for the correct behaviour for the various possible orderings of close calls and TLS close alerts. The behaviour change test is closingInboundBeforeClosingOutboundShouldFail() in place of closingInboundShouldOnlyCloseInbound(). Changes outside ConscryptEngineTest are minimal. Close behaviour before handshaking starts is undefined and we differ from the RI, but I don't think that's problematic. Obviously also needs documenting in Conscrypt and Android release notes. This also means that STATE_CLOSED_INBOUND is never reached, which means it can be eliminated in a future CL allowing some minor simplifications. NB This can be merged independently of google#844 and I'll rebase that change on top of it. [1] https://developer.android.com/reference/javax/net/ssl/SSLEngine#closeInbound() [2] https://cs.android.com/android/platform/superproject/+/master:libcore/harmony-tests/src/test/java/org/apache/harmony/tests/javax/net/ssl/SSLEngineTest.java;l=611
1 parent e26f433 commit 9ee1aaf

File tree

4 files changed

+178
-9
lines changed

4 files changed

+178
-9
lines changed

common/src/main/java/org/conscrypt/ConscryptEngine.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ private void beginHandshakeInternal() throws SSLException {
457457
}
458458

459459
@Override
460-
public void closeInbound() {
460+
public void closeInbound() throws SSLException {
461461
synchronized (ssl) {
462462
if (state == STATE_CLOSED || state == STATE_CLOSED_INBOUND) {
463463
return;
@@ -466,7 +466,7 @@ public void closeInbound() {
466466
if (state == STATE_CLOSED_OUTBOUND) {
467467
transitionTo(STATE_CLOSED);
468468
} else {
469-
transitionTo(STATE_CLOSED_INBOUND);
469+
throw new SSLException("Closed SSLEngine inbound before outbound");
470470
}
471471
freeIfDone();
472472
} else {
@@ -1337,7 +1337,7 @@ private SSLEngineResult.Status getEngineStatus() {
13371337
}
13381338
}
13391339

1340-
private void closeAll() {
1340+
private void closeAll() throws SSLException {
13411341
closeOutbound();
13421342
closeInbound();
13431343
}

common/src/main/java/org/conscrypt/ConscryptEngineSocket.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,9 @@ public final void close() throws IOException {
451451
super.close();
452452
} finally {
453453
// Close the engine.
454-
engine.closeInbound();
455454
engine.closeOutbound();
456-
455+
engine.closeInbound();
456+
457457
// Release any resources we're holding
458458
if (in != null) {
459459
in.release();

openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java

+172-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static org.junit.Assert.assertFalse;
2727
import static org.junit.Assert.assertNull;
2828
import static org.junit.Assert.assertTrue;
29+
import static org.junit.Assert.fail;
2930
import static org.mockito.ArgumentMatchers.same;
3031
import static org.mockito.Mockito.when;
3132

@@ -176,22 +177,166 @@ public void closingOutboundAfterHandshakeShouldOnlyCloseOutbound() throws Except
176177
}
177178

178179
@Test
179-
public void closingInboundShouldOnlyCloseInbound() throws Exception {
180+
public void closingInboundBeforeHandshakeShouldCloseAll() throws Exception {
181+
setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
182+
assertFalse(clientEngine.isInboundDone());
183+
assertFalse(clientEngine.isOutboundDone());
184+
assertFalse(serverEngine.isInboundDone());
185+
assertFalse(serverEngine.isOutboundDone());
186+
187+
clientEngine.closeInbound();
188+
serverEngine.closeInbound();
189+
190+
assertTrue(clientEngine.isInboundDone());
191+
assertTrue(clientEngine.isOutboundDone());
192+
assertTrue(serverEngine.isInboundDone());
193+
assertTrue(serverEngine.isOutboundDone());
194+
}
195+
196+
@Test
197+
public void closingInboundBeforeClosingOutboundShouldFail() throws Exception {
180198
setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
181199
doHandshake(true);
182200

201+
try {
202+
clientEngine.closeInbound();
203+
fail();
204+
} catch (SSLException e) {
205+
// Expected
206+
}
207+
208+
try {
209+
serverEngine.closeInbound();
210+
fail();
211+
} catch (SSLException e) {
212+
// Expected
213+
}
214+
183215
assertFalse(clientEngine.isInboundDone());
184216
assertFalse(clientEngine.isOutboundDone());
185217
assertFalse(serverEngine.isInboundDone());
186218
assertFalse(serverEngine.isOutboundDone());
219+
}
220+
221+
@Test
222+
public void closingInboundAfterClosingOutboundShouldSucceed_NoAlerts() throws Exception {
223+
// Client and server both close inbound after closing outbound without seeing the other's
224+
// close alert (e.g. due to the transport being closed already).
225+
setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
226+
doHandshake(true);
187227

228+
clientEngine.closeOutbound();
229+
serverEngine.closeOutbound();
230+
// After closeOutbound, wrap should produce the TLS close alerts.
231+
assertTrue(wrapClosed(clientEngine).hasRemaining());
232+
assertTrue(wrapClosed(serverEngine).hasRemaining());
233+
234+
assertFalse(clientEngine.isInboundDone());
235+
assertTrue(clientEngine.isOutboundDone());
236+
assertFalse(serverEngine.isInboundDone());
237+
assertTrue(serverEngine.isOutboundDone());
238+
239+
// Closing inbound should now succeed and produce no new handshake data.
188240
clientEngine.closeInbound();
189241
serverEngine.closeInbound();
242+
assertFalse(wrapClosed(clientEngine).hasRemaining());
243+
assertFalse(wrapClosed(serverEngine).hasRemaining());
190244

245+
// Final state, everything closed.
191246
assertTrue(clientEngine.isInboundDone());
192-
assertFalse(clientEngine.isOutboundDone());
247+
assertTrue(clientEngine.isOutboundDone());
193248
assertTrue(serverEngine.isInboundDone());
194-
assertFalse(serverEngine.isOutboundDone());
249+
assertTrue(serverEngine.isOutboundDone());
250+
}
251+
252+
@Test
253+
public void closingInboundAfterClosingOutboundShouldSucceed_AlertsArriveBeforeCloseInbound()
254+
throws Exception {
255+
// Client and server both call closeOutbound simultaneously. The alerts produced
256+
// by this both arrive at the peer before it calls closeInbound, making it a no-op.
257+
setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
258+
doHandshake(true);
259+
260+
clientEngine.closeOutbound();
261+
// After closeOutbound, wrap should produce a single output buffer with the TLS close alert
262+
ByteBuffer clientOutput = wrapClosed(clientEngine);
263+
assertTrue(clientOutput.hasRemaining());
264+
265+
// Unwrapping that on the server engine should cause it to close and send its own alert
266+
// but produce no plaintext output.
267+
assertFalse(unwrapClosed(serverEngine, clientOutput).hasRemaining());
268+
ByteBuffer serverOutput = wrapClosed(serverEngine);
269+
assertTrue(serverOutput.hasRemaining());
270+
271+
// At his point, server should be fully closed after processing client's alert and sending
272+
// its own. Client inbound is still open.
273+
assertFalse(clientEngine.isInboundDone());
274+
assertTrue(clientEngine.isOutboundDone());
275+
assertTrue(serverEngine.isInboundDone());
276+
assertTrue(serverEngine.isOutboundDone());
277+
278+
// Explicitly closing the server outbound should produce no new extra handshake data
279+
serverEngine.closeOutbound();
280+
assertFalse(wrapClosed(serverEngine).hasRemaining());
281+
282+
// Unwrapping the server output on the client should cause it to close as above
283+
// but produce no new handshaking data and no new plaintext.
284+
assertFalse(unwrapClosed(clientEngine, serverOutput).hasRemaining());
285+
assertFalse(wrapClosed(clientEngine).hasRemaining());
286+
287+
// Everything should be fully closed by this point.
288+
assertTrue(clientEngine.isInboundDone());
289+
assertTrue(clientEngine.isOutboundDone());
290+
assertTrue(serverEngine.isInboundDone());
291+
assertTrue(serverEngine.isOutboundDone());
292+
293+
// Closing inbounds should now be a no-op and also produce no new handshake data.
294+
clientEngine.closeInbound();
295+
serverEngine.closeInbound();
296+
assertFalse(wrapClosed(clientEngine).hasRemaining());
297+
assertFalse(wrapClosed(serverEngine).hasRemaining());
298+
}
299+
300+
@Test
301+
public void closingInboundAfterClosingOutboundShouldSucceed_AlertsArriveAfterCloseInbound()
302+
throws Exception {
303+
// Client and server both call closeOutbound simultaneously. The alerts produced
304+
// by this both arrive at the peer after it calls closeInbound. Verify that
305+
// this produces no plainttext or further handshake data.
306+
setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
307+
doHandshake(true);
308+
309+
// After closeOutbound, wrap should produce a single output buffer with the TLS close alert
310+
clientEngine.closeOutbound();
311+
ByteBuffer clientOutput = wrapClosed(clientEngine);
312+
assertTrue(clientOutput.hasRemaining());
313+
serverEngine.closeOutbound();
314+
ByteBuffer serverOutput = wrapClosed(serverEngine);
315+
assertTrue(serverOutput.hasRemaining());
316+
317+
assertFalse(clientEngine.isInboundDone());
318+
assertTrue(clientEngine.isOutboundDone());
319+
assertFalse(serverEngine.isInboundDone());
320+
assertTrue(serverEngine.isOutboundDone());
321+
322+
// Closing inbounds should now succeed and produce no new handshake data.
323+
clientEngine.closeInbound();
324+
serverEngine.closeInbound();
325+
assertFalse(wrapClosed(clientEngine).hasRemaining());
326+
assertFalse(wrapClosed(serverEngine).hasRemaining());
327+
328+
// Everything should be fully closed by this point.
329+
assertTrue(clientEngine.isInboundDone());
330+
assertTrue(clientEngine.isOutboundDone());
331+
assertTrue(serverEngine.isInboundDone());
332+
assertTrue(serverEngine.isOutboundDone());
333+
334+
// Unwrapping the previous handshake data should also succeed, and produce no
335+
//new plaintext or handshake data
336+
assertFalse(unwrapClosed(serverEngine, clientOutput).hasRemaining());
337+
assertFalse(wrapClosed(serverEngine).hasRemaining());
338+
assertFalse(unwrapClosed(clientEngine, serverOutput).hasRemaining());
339+
assertFalse(wrapClosed(clientEngine).hasRemaining());
195340
}
196341

197342
@Test
@@ -493,6 +638,30 @@ private List<ByteBuffer> wrap(ByteBuffer input, SSLEngine engine) throws SSLExce
493638
return wrapped;
494639
}
495640

641+
private ByteBuffer wrapClosed(SSLEngine engine) throws SSLException {
642+
// Call wrap with empty input and return any handshaking data produced, asserting
643+
// the engine is already in a closed state.
644+
ByteBuffer emptyInput = bufferType.newBuffer(0);
645+
ByteBuffer encryptedBuffer =
646+
bufferType.newBuffer(engine.getSession().getPacketBufferSize());
647+
SSLEngineResult wrapResult = engine.wrap(emptyInput, encryptedBuffer);
648+
assertEquals(Status.CLOSED, wrapResult.getStatus());
649+
encryptedBuffer.flip();
650+
return encryptedBuffer;
651+
}
652+
653+
private ByteBuffer unwrapClosed(SSLEngine engine, ByteBuffer encrypted) throws SSLException {
654+
// Unwrap a single buffer and return the plaintext, asserting the engine is already in a
655+
// closed state.
656+
final ByteBuffer decryptedBuffer
657+
= bufferType.newBuffer(engine.getSession().getPacketBufferSize());
658+
659+
SSLEngineResult unwrapResult = engine.unwrap(encrypted, decryptedBuffer);
660+
assertEquals(Status.CLOSED, unwrapResult.getStatus());
661+
decryptedBuffer.flip();
662+
return decryptedBuffer;
663+
}
664+
496665
private byte[] unwrap(ByteBuffer[] encryptedBuffers, SSLEngine engine) throws IOException {
497666
ByteArrayOutputStream cleartextStream = new ByteArrayOutputStream();
498667
int decryptedBufferSize = 8192;

testing/src/main/java/org/conscrypt/javax/net/ssl/TestSSLEnginePair.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ public static void close(SSLEngine[] engines) {
146146
try {
147147
for (SSLEngine engine : engines) {
148148
if (engine != null) {
149-
engine.closeInbound();
150149
engine.closeOutbound();
150+
engine.closeInbound();
151151
}
152152
}
153153
} catch (Exception e) {

0 commit comments

Comments
 (0)