diff --git a/src/java.base/share/classes/sun/security/ssl/SSLEngineImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLEngineImpl.java index cf09a6bcc52..a3db3ad0e10 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLEngineImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLEngineImpl.java @@ -47,7 +47,7 @@ import javax.net.ssl.SSLSession; /** - * Implementation of an non-blocking SSLEngine. + * Implementation of a non-blocking SSLEngine. * * @author Brad Wetmore */ @@ -250,7 +250,7 @@ private SSLEngineResult writeRecord( if (ciphertext == null && !conContext.isNegotiated && conContext.isInboundClosed() && hsStatus == HandshakeStatus.NEED_WRAP) { - // Even the outboud is open, no futher data could be wrapped as: + // Even the outbound is open, no further data could be wrapped as: // 1. the outbound is empty // 2. no negotiated connection // 3. the inbound has closed, cannot complete the handshake @@ -724,17 +724,19 @@ public synchronized void closeInbound() throws SSLException { SSLLogger.finest("Closing inbound of SSLEngine"); } - // Is it ready to close inbound? - // - // No need to throw exception if the initial handshake is not started. - if (!conContext.isInputCloseNotified && - (conContext.isNegotiated || conContext.handshakeContext != null)) { - throw conContext.fatal(Alert.INTERNAL_ERROR, - "closing inbound before receiving peer's close_notify"); + try { + // Is it ready to close inbound? + // + // No need to throw exception if the initial handshake is not started. + if (!conContext.isInputCloseNotified && + (conContext.isNegotiated || conContext.handshakeContext != null)) { + throw new SSLException( + "closing inbound before receiving peer's close_notify"); + } + } finally { + conContext.closeInbound(); } - - conContext.closeInbound(); } @Override diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java index fa85e2013c7..1222a4f4a17 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java @@ -839,9 +839,10 @@ private void shutdownInput( // No need to throw exception if the initial handshake is not started. try { if (checkCloseNotify && !conContext.isInputCloseNotified && - (conContext.isNegotiated || conContext.handshakeContext != null)) { - throw new SSLException( - "closing inbound before receiving peer's close_notify"); + (conContext.isNegotiated || + conContext.handshakeContext != null)) { + throw new SSLException( + "closing inbound before receiving peer's close_notify"); } } finally { conContext.closeInbound(); diff --git a/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketSSLEngineCloseInbound.java b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketSSLEngineCloseInbound.java new file mode 100644 index 00000000000..abf1571cadc --- /dev/null +++ b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketSSLEngineCloseInbound.java @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2011, 2022, 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 8273553 8253368 + * @summary sun.security.ssl.SSLEngineImpl.closeInbound also has similar error + * of JDK-8253368 + * @run main/othervm SSLSocketSSLEngineCloseInbound TLSv1.3 + * @run main/othervm SSLSocketSSLEngineCloseInbound TLSv1.2 + * @run main/othervm SSLSocketSSLEngineCloseInbound TLSv1.1 + * @run main/othervm SSLSocketSSLEngineCloseInbound TLSv1 + * @run main/othervm SSLSocketSSLEngineCloseInbound TLS + */ + +/** + * A SSLSocket/SSLEngine interop test case. This is not the way to + * code SSLEngine-based servers, but works for what we need to do here, + * which is to make sure that SSLEngine/SSLSockets can talk to each other. + * SSLEngines can use direct or indirect buffers, and different code + * is used to get at the buffer contents internally, so we test that here. + * + * The test creates one SSLSocket (client) and one SSLEngine (server). + * The SSLSocket talks to a raw ServerSocket, and the server code + * does the translation between byte [] and ByteBuffers that the SSLEngine + * can use. The "transport" layer consists of a Socket Input/OutputStream + * and two byte buffers for the SSLEngines: think of them + * as directly connected pipes. + * + * Again, 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 + * ====== ====== ======= + * write() ... ClientHello + * ... unwrap() ClientHello + * ... wrap() ServerHello/Certificate + * read() ... ServerHello/Certificate + * write() ... ClientKeyExchange + * write() ... ChangeCipherSpec + * write() ... Finished + * ... unwrap() ClientKeyExchange + * ... unwrap() ChangeCipherSpec + * ... unwrap() Finished + * ... wrap() ChangeCipherSpec + * ... wrap() Finished + * read() ... ChangeCipherSpec + * read() ... Finished + */ +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; +import java.io.*; +import java.net.*; +import java.security.*; +import java.nio.*; + +public class SSLSocketSSLEngineCloseInbound { + + /* + * Enables logging of the SSL/TLS 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 = false; + private final SSLContext sslc; + private SSLEngine serverEngine; // server-side SSLEngine + private SSLSocket clientSocket; + + private final byte[] serverMsg = + "Hi there Client, I'm a Server.".getBytes(); + private final byte[] clientMsg = + "Hello Server, I'm a Client! Pleased to meet you!".getBytes(); + + private ByteBuffer serverOut; // write side of serverEngine + private ByteBuffer serverIn; // read side of serverEngine + + private volatile Exception clientException; + private volatile Exception serverException; + + /* + * For data transport, this example uses local ByteBuffers. + */ + private ByteBuffer cTOs; // "reliable" transport client->server + private ByteBuffer sTOc; // "reliable" transport server->client + + /* + * The following is to set up the keystores/trust material. + */ + 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 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 { + String protocol = args[0]; + + // reset security properties to make sure that the algorithms + // and keys used in this test are not disabled. + Security.setProperty("jdk.tls.disabledAlgorithms", ""); + Security.setProperty("jdk.certpath.disabledAlgorithms", ""); + + if (debug) { + System.setProperty("javax.net.debug", "all"); + } + + /* + * Run the tests with direct and indirect buffers. + */ + SSLSocketSSLEngineCloseInbound test = + new SSLSocketSSLEngineCloseInbound(protocol); + log("-------------------------------------"); + log("Testing " + protocol + " for direct buffers ..."); + test.runTest(true); + + log("---------------------------------------"); + log("Testing " + protocol + " for indirect buffers ..."); + test.runTest(false); + + log("Test Passed."); + } + + /* + * Create an initialized SSLContext to use for these tests. + */ + public SSLSocketSSLEngineCloseInbound(String protocol) throws Exception { + + KeyStore ks = KeyStore.getInstance("JKS"); + KeyStore ts = KeyStore.getInstance("JKS"); + + char[] passphrase = "passphrase".toCharArray(); + + try (FileInputStream keyFile = new FileInputStream(keyFilename); + FileInputStream trustFile = new FileInputStream(trustFilename)) { + ks.load(keyFile, passphrase); + ts.load(trustFile, passphrase); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + + SSLContext sslCtx = SSLContext.getInstance(protocol); + + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + sslc = sslCtx; + } + + /* + * Run the test. + * + * Sit in a tight loop, with the server engine calling wrap/unwrap + * regardless of whether data is available or not. We do this until + * we get the application data. Then we shutdown and go to the next one. + * + * The main loop handles all 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(boolean direct) throws Exception { + clientSocket = null; + + // generates the server-side Socket + try (ServerSocket serverSocket = new ServerSocket()) { + serverSocket.setReuseAddress(false); + serverSocket.bind(null); + int port = serverSocket.getLocalPort(); + log("Port: " + port); + Thread thread = createClientThread(port); + + createSSLEngine(); + createBuffers(direct); + + // server-side socket that will read + try (Socket socket = serverSocket.accept()) { + socket.setSoTimeout(500); + + InputStream is = socket.getInputStream(); + OutputStream os = socket.getOutputStream(); + + SSLEngineResult serverResult; // results from last operation + + /* + * 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. + */ + byte[] inbound = new byte[8192]; + byte[] outbound = new byte[8192]; + + while (!isEngineClosed(serverEngine)) { + int len; + + // Inbound data + log("================"); + + // Try reading Client side, even if it's already closed. + try { + len = is.read(inbound); + if (len > 0) { + cTOs.put(inbound, 0, len); + } + } catch (IOException e) { + /* + * swallow IO/SocketTimeoutExceptions. We'll do + * the testing/exit after the unwraps. + */ + } + + cTOs.flip(); + + serverResult = serverEngine.unwrap(cTOs, serverIn); + log("server unwrap: ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + cTOs.compact(); + + // Outbound data + log("----"); + + // After we've received our app bytes, close input side + // and see what happens. Exit the test at the end. + if (serverIn.position() != 0) { + try { + serverEngine.closeInbound(); + throw new Exception( + "No error shutting down client's input"); + } catch (SSLException e) { + System.out.println( + "Server caught the right Exception"); + } + + if (serverEngine.getSession().isValid()) { + System.out.println("Server session is still valid"); + } else { + throw new Exception("Server session is not valid"); + } + + return; + } + + serverResult = serverEngine.wrap(serverOut, sTOc); + log("server wrap: ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + + sTOc.flip(); + + if ((len = sTOc.remaining()) != 0) { + sTOc.get(outbound, 0, len); + os.write(outbound, 0, len); + // Give the other side a chance to process + } + + sTOc.compact(); + } + } catch (Exception e) { + serverException = e; + } finally { + // Wait for the client to join up with us. + if (thread != null) { + thread.join(); + } + } + } finally { + if (serverException != null) { + if (clientException != null) { + serverException.initCause(clientException); + } + throw serverException; + } + if (clientException != null) { + if (serverException != null) { + clientException.initCause(serverException); + } + throw clientException; + } + } + } + + /* + * Create a client thread which does simple SSLSocket operations. + * We'll write and read one data packet. + */ + private Thread createClientThread(final int port) { + + Thread t = new Thread("ClientThread") { + + @Override + public void run() { + // client-side socket + try (SSLSocket sslSocket = (SSLSocket)sslc.getSocketFactory(). + createSocket("localhost", port)) { + clientSocket = sslSocket; + + OutputStream os = sslSocket.getOutputStream(); + + // write(byte[]) goes in one shot. + os.write(clientMsg); + os.flush(); + + try { + sslSocket.shutdownInput(); + throw new Exception( + "No error shutting down client's input"); + } catch (SSLException e) { + System.out.println("Client caught the right Exception"); + } + + if (sslSocket.getSession().isValid()) { + System.out.println("Client session is still valid"); + } else { + throw new Exception("Client's session is not valid"); + } + + // Give server a chance to read before we shutdown via + // the try-with-resources block. + Thread.sleep(2000); + } catch (Exception e) { + clientException = e; + } + } + }; + t.start(); + return t; + } + + /* + * Using the SSLContext created during object creation, + * create/configure the SSLEngines we'll use for this test. + */ + private void createSSLEngine() { + /* + * Configure the serverEngine to act as a server in the SSL/TLS + * handshake. + */ + serverEngine = sslc.createSSLEngine(); + serverEngine.setUseClientMode(false); + serverEngine.getNeedClientAuth(); + } + + /* + * Create and size the buffers appropriately. + */ + private void createBuffers(boolean direct) { + + SSLSession session = serverEngine.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. + */ + if (direct) { + serverIn = ByteBuffer.allocateDirect(appBufferMax + 50); + cTOs = ByteBuffer.allocateDirect(netBufferMax); + sTOc = ByteBuffer.allocateDirect(netBufferMax); + } else { + serverIn = ByteBuffer.allocate(appBufferMax + 50); + cTOs = ByteBuffer.allocate(netBufferMax); + sTOc = ByteBuffer.allocate(netBufferMax); + } + + serverOut = ByteBuffer.wrap(serverMsg); + } + + /* + * 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()); + } + + /* + * Logging code + */ + private static boolean resultOnce = true; + + private static void log(String str, SSLEngineResult result) { + if (!logging) { + return; + } + if (resultOnce) { + resultOnce = false; + log("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) { + if (debug) { + System.err.println(str); + } else { + System.out.println(str); + } + } + } +}