Path: blob/master/test/jdk/javax/net/ssl/SSLSession/ResumeTLS13withSNI.java
41152 views
/*1* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223// SunJSSE does not support dynamic system properties, no way to re-use24// system properties in samevm/agentvm mode.2526/*27* @test28* @bug 821180629* @summary TLS 1.3 handshake server name indication is missing on a session resume30* @run main/othervm ResumeTLS13withSNI31*/3233import javax.net.ssl.*;34import javax.net.ssl.SSLEngineResult.*;35import java.io.*;36import java.security.*;37import java.nio.*;38import java.util.List;3940public class ResumeTLS13withSNI {4142/*43* Enables logging of the SSLEngine operations.44*/45private static final boolean logging = false;4647/*48* Enables the JSSE system debugging system property:49*50* -Djavax.net.debug=ssl:handshake51*52* This gives a lot of low-level information about operations underway,53* including specific handshake messages, and might be best examined54* after gaining some familiarity with this application.55*/56private static final boolean debug = true;5758private static final ByteBuffer clientOut =59ByteBuffer.wrap("Hi Server, I'm Client".getBytes());60private static final ByteBuffer serverOut =61ByteBuffer.wrap("Hello Client, I'm Server".getBytes());6263/*64* The following is to set up the keystores.65*/66private static final String pathToStores = "../etc";67private static final String keyStoreFile = "keystore";68private static final String trustStoreFile = "truststore";69private static final char[] passphrase = "passphrase".toCharArray();7071private static final String keyFilename =72System.getProperty("test.src", ".") + "/" + pathToStores +73"/" + keyStoreFile;74private static final String trustFilename =75System.getProperty("test.src", ".") + "/" + pathToStores +76"/" + trustStoreFile;7778private static final String HOST_NAME = "arf.yak.foo";79private static final SNIHostName SNI_NAME = new SNIHostName(HOST_NAME);80private static final SNIMatcher SNI_MATCHER =81SNIHostName.createSNIMatcher("arf\\.yak\\.foo");8283/*84* Main entry point for this test.85*/86public static void main(String args[]) throws Exception {87if (debug) {88System.setProperty("javax.net.debug", "ssl:handshake");89}9091KeyManagerFactory kmf = makeKeyManagerFactory(keyFilename,92passphrase);93TrustManagerFactory tmf = makeTrustManagerFactory(trustFilename,94passphrase);9596SSLContext sslCtx = SSLContext.getInstance("TLS");97sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);9899// Make client and server engines, then customize as needed100SSLEngine clientEngine = makeEngine(sslCtx, kmf, tmf, true);101SSLParameters cliSSLParams = clientEngine.getSSLParameters();102cliSSLParams.setServerNames(List.of(SNI_NAME));103clientEngine.setSSLParameters(cliSSLParams);104clientEngine.setEnabledProtocols(new String[] { "TLSv1.3" });105106SSLEngine serverEngine = makeEngine(sslCtx, kmf, tmf, false);107SSLParameters servSSLParams = serverEngine.getSSLParameters();108servSSLParams.setSNIMatchers(List.of(SNI_MATCHER));109serverEngine.setSSLParameters(servSSLParams);110111initialHandshake(clientEngine, serverEngine);112113// Create a new client-side engine which can initiate TLS session114// resumption115SSLEngine newCliEngine = makeEngine(sslCtx, kmf, tmf, true);116newCliEngine.setEnabledProtocols(new String[] { "TLSv1.3" });117ByteBuffer resCliHello = getResumptionClientHello(newCliEngine);118119dumpBuffer("Resumed ClientHello Data", resCliHello);120121// Parse the client hello message and make sure it is a resumption122// hello and has SNI in it.123checkResumedClientHelloSNI(resCliHello);124}125126/*127* Run the test.128*129* Sit in a tight loop, both engines calling wrap/unwrap regardless130* of whether data is available or not. We do this until both engines131* report back they are closed.132*133* The main loop handles all of the I/O phases of the SSLEngine's134* lifetime:135*136* initial handshaking137* application data transfer138* engine closing139*140* One could easily separate these phases into separate141* sections of code.142*/143private static void initialHandshake(SSLEngine clientEngine,144SSLEngine serverEngine) throws Exception {145boolean dataDone = false;146147// Create all the buffers148SSLSession session = clientEngine.getSession();149int appBufferMax = session.getApplicationBufferSize();150int netBufferMax = session.getPacketBufferSize();151ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50);152ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax + 50);153ByteBuffer cTOs = ByteBuffer.allocateDirect(netBufferMax);154ByteBuffer sTOc = ByteBuffer.allocateDirect(netBufferMax);155156// results from client's last operation157SSLEngineResult clientResult;158159// results from server's last operation160SSLEngineResult serverResult;161162/*163* Examining the SSLEngineResults could be much more involved,164* and may alter the overall flow of the application.165*166* For example, if we received a BUFFER_OVERFLOW when trying167* to write to the output pipe, we could reallocate a larger168* pipe, but instead we wait for the peer to drain it.169*/170Exception clientException = null;171Exception serverException = null;172173while (!dataDone) {174log("================");175176try {177clientResult = clientEngine.wrap(clientOut, cTOs);178log("client wrap: ", clientResult);179} catch (Exception e) {180clientException = e;181System.err.println("Client wrap() threw: " + e.getMessage());182}183logEngineStatus(clientEngine);184runDelegatedTasks(clientEngine);185186log("----");187188try {189serverResult = serverEngine.wrap(serverOut, sTOc);190log("server wrap: ", serverResult);191} catch (Exception e) {192serverException = e;193System.err.println("Server wrap() threw: " + e.getMessage());194}195logEngineStatus(serverEngine);196runDelegatedTasks(serverEngine);197198cTOs.flip();199sTOc.flip();200201log("--------");202203try {204clientResult = clientEngine.unwrap(sTOc, clientIn);205log("client unwrap: ", clientResult);206} catch (Exception e) {207clientException = e;208System.err.println("Client unwrap() threw: " + e.getMessage());209}210logEngineStatus(clientEngine);211runDelegatedTasks(clientEngine);212213log("----");214215try {216serverResult = serverEngine.unwrap(cTOs, serverIn);217log("server unwrap: ", serverResult);218} catch (Exception e) {219serverException = e;220System.err.println("Server unwrap() threw: " + e.getMessage());221}222logEngineStatus(serverEngine);223runDelegatedTasks(serverEngine);224225cTOs.compact();226sTOc.compact();227228/*229* After we've transfered all application data between the client230* and server, we close the clientEngine's outbound stream.231* This generates a close_notify handshake message, which the232* server engine receives and responds by closing itself.233*/234if (!dataDone && (clientOut.limit() == serverIn.position()) &&235(serverOut.limit() == clientIn.position())) {236237/*238* A sanity check to ensure we got what was sent.239*/240checkTransfer(serverOut, clientIn);241checkTransfer(clientOut, serverIn);242243dataDone = true;244}245}246}247248/**249* The goal of this function is to start a simple TLS session resumption250* and get the client hello message data back so it can be inspected.251*252* @param clientEngine253*254* @return a ByteBuffer consisting of the ClientHello TLS record.255*256* @throws Exception if any processing goes wrong.257*/258private static ByteBuffer getResumptionClientHello(SSLEngine clientEngine)259throws Exception {260// Create all the buffers261SSLSession session = clientEngine.getSession();262int appBufferMax = session.getApplicationBufferSize();263int netBufferMax = session.getPacketBufferSize();264ByteBuffer cTOs = ByteBuffer.allocateDirect(netBufferMax);265Exception clientException = null;266267// results from client's last operation268SSLEngineResult clientResult;269270// results from server's last operation271SSLEngineResult serverResult;272273log("================");274275// Start by having the client create a new ClientHello. It should276// contain PSK info that allows it to attempt session resumption.277try {278clientResult = clientEngine.wrap(clientOut, cTOs);279log("client wrap: ", clientResult);280} catch (Exception e) {281clientException = e;282System.err.println("Client wrap() threw: " + e.getMessage());283}284logEngineStatus(clientEngine);285runDelegatedTasks(clientEngine);286287log("----");288289cTOs.flip();290return cTOs;291}292293/**294* This method walks a ClientHello TLS record, looking for a matching295* server_name hostname value from the original handshake and a PSK296* extension, which indicates (in the context of this test) that this297* is a resumed handshake.298*299* @param resCliHello a ByteBuffer consisting of a complete TLS handshake300* record that is a ClientHello message. The position of the buffer301* must be at the beginning of the TLS record header.302*303* @throws Exception if any of the consistency checks for the TLS record,304* or handshake message fails. It will also throw an exception if305* either the server_name extension doesn't have a matching hostname306* field or the pre_shared_key extension is not present.307*/308private static void checkResumedClientHelloSNI(ByteBuffer resCliHello)309throws Exception {310boolean foundMatchingSNI = false;311boolean foundPSK = false;312313// Advance past the following fields:314// TLS Record header (5 bytes)315resCliHello.position(resCliHello.position() + 5);316317// Get the next byte and make sure it is a Client Hello318byte hsMsgType = resCliHello.get();319if (hsMsgType != 0x01) {320throw new Exception("Message is not a ClientHello, MsgType = " +321hsMsgType);322}323324// Skip past the length (3 bytes)325resCliHello.position(resCliHello.position() + 3);326327// Protocol version should be TLSv1.2 (0x03, 0x03)328short chProto = resCliHello.getShort();329if (chProto != 0x0303) {330throw new Exception(331"Client Hello protocol version is not TLSv1.2: Got " +332String.format("0x%04X", chProto));333}334335// Skip 32-bytes of random data336resCliHello.position(resCliHello.position() + 32);337338// Get the legacy session length and skip that many bytes339int sessIdLen = Byte.toUnsignedInt(resCliHello.get());340resCliHello.position(resCliHello.position() + sessIdLen);341342// Skip over all the cipher suites343int csLen = Short.toUnsignedInt(resCliHello.getShort());344resCliHello.position(resCliHello.position() + csLen);345346// Skip compression methods347int compLen = Byte.toUnsignedInt(resCliHello.get());348resCliHello.position(resCliHello.position() + compLen);349350// Parse the extensions. Get length first, then walk the extensions351// List and look for the presence of the PSK extension and server_name.352// For server_name, make sure it is the same as what was provided353// in the original handshake.354System.err.println("ClientHello Extensions Check");355int extListLen = Short.toUnsignedInt(resCliHello.getShort());356while (extListLen > 0) {357// Get the Extension type and length358int extType = Short.toUnsignedInt(resCliHello.getShort());359int extLen = Short.toUnsignedInt(resCliHello.getShort());360switch (extType) {361case 0: // server_name362System.err.println("* Found server_name Extension");363int snListLen = Short.toUnsignedInt(resCliHello.getShort());364while (snListLen > 0) {365int nameType = Byte.toUnsignedInt(resCliHello.get());366if (nameType == 0) { // host_name367int hostNameLen =368Short.toUnsignedInt(resCliHello.getShort());369byte[] hostNameData = new byte[hostNameLen];370resCliHello.get(hostNameData);371String hostNameStr = new String(hostNameData);372System.err.println("\tHostname: " + hostNameStr);373if (hostNameStr.equals(HOST_NAME)) {374foundMatchingSNI = true;375}376snListLen -= 3 + hostNameLen; // type, len, data377} else { // something else378// We don't support anything else and cannot379// know how to advance. Throw an exception380throw new Exception("Unknown server name type: " +381nameType);382}383}384break;385case 41: // pre_shared_key386// We're not going to bother checking the value. The387// presence of the extension in the context of this test388// is good enough to tell us this is a resumed ClientHello.389foundPSK = true;390System.err.println("* Found pre_shared_key Extension");391resCliHello.position(resCliHello.position() + extLen);392break;393default:394System.err.format("* Found extension %d (%d bytes)\n",395extType, extLen);396resCliHello.position(resCliHello.position() + extLen);397break;398}399extListLen -= extLen + 4; // Ext type(2), length(2), data(var.)400}401402// At the end of all the extension processing, either we've found403// both extensions and the server_name matches our expected value404// or we throw an exception.405if (!foundMatchingSNI) {406throw new Exception("Could not find a matching server_name");407} else if (!foundPSK) {408throw new Exception("Missing PSK extension, not a resumption?");409}410}411412/**413* Create a TrustManagerFactory from a given keystore.414*415* @param tsPath the path to the trust store file.416* @param pass the password for the trust store.417*418* @return a new TrustManagerFactory built from the trust store provided.419*420* @throws GeneralSecurityException if any processing errors occur421* with the Keystore instantiation or TrustManagerFactory creation.422* @throws IOException if any loading error with the trust store occurs.423*/424private static TrustManagerFactory makeTrustManagerFactory(String tsPath,425char[] pass) throws GeneralSecurityException, IOException {426TrustManagerFactory tmf;427KeyStore ts = KeyStore.getInstance("JKS");428429try (FileInputStream fsIn = new FileInputStream(tsPath)) {430ts.load(fsIn, pass);431tmf = TrustManagerFactory.getInstance("SunX509");432tmf.init(ts);433}434return tmf;435}436437/**438* Create a KeyManagerFactory from a given keystore.439*440* @param ksPath the path to the keystore file.441* @param pass the password for the keystore.442*443* @return a new TrustManagerFactory built from the keystore provided.444*445* @throws GeneralSecurityException if any processing errors occur446* with the Keystore instantiation or KeyManagerFactory creation.447* @throws IOException if any loading error with the keystore occurs448*/449private static KeyManagerFactory makeKeyManagerFactory(String ksPath,450char[] pass) throws GeneralSecurityException, IOException {451KeyManagerFactory kmf;452KeyStore ks = KeyStore.getInstance("JKS");453454try (FileInputStream fsIn = new FileInputStream(ksPath)) {455ks.load(fsIn, pass);456kmf = KeyManagerFactory.getInstance("SunX509");457kmf.init(ks, pass);458}459return kmf;460}461462/**463* Create an SSLEngine instance from a given protocol specifier,464* KeyManagerFactory and TrustManagerFactory.465*466* @param ctx the SSLContext used to create the SSLEngine467* @param kmf an initialized KeyManagerFactory. May be null.468* @param tmf an initialized TrustManagerFactory. May be null.469* @param isClient true if it intended to create a client engine, false470* for a server engine.471*472* @return an SSLEngine instance configured as a server and with client473* authentication disabled.474*475* @throws GeneralSecurityException if any errors occur during the476* creation of the SSLEngine.477*/478private static SSLEngine makeEngine(SSLContext ctx,479KeyManagerFactory kmf, TrustManagerFactory tmf, boolean isClient)480throws GeneralSecurityException {481ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);482SSLEngine ssle = ctx.createSSLEngine("localhost", 8443);483ssle.setUseClientMode(isClient);484ssle.setNeedClientAuth(false);485return ssle;486}487488private static void logEngineStatus(SSLEngine engine) {489log("\tCurrent HS State " + engine.getHandshakeStatus().toString());490log("\tisInboundDone(): " + engine.isInboundDone());491log("\tisOutboundDone(): " + engine.isOutboundDone());492}493494/*495* If the result indicates that we have outstanding tasks to do,496* go ahead and run them in this thread.497*/498private static void runDelegatedTasks(SSLEngine engine) throws Exception {499500if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {501Runnable runnable;502while ((runnable = engine.getDelegatedTask()) != null) {503log(" running delegated task...");504runnable.run();505}506HandshakeStatus hsStatus = engine.getHandshakeStatus();507if (hsStatus == HandshakeStatus.NEED_TASK) {508throw new Exception(509"handshake shouldn't need additional tasks");510}511logEngineStatus(engine);512}513}514515private static boolean isEngineClosed(SSLEngine engine) {516return (engine.isOutboundDone() && engine.isInboundDone());517}518519/*520* Simple check to make sure everything came across as expected.521*/522private static void checkTransfer(ByteBuffer a, ByteBuffer b)523throws Exception {524a.flip();525b.flip();526527if (!a.equals(b)) {528throw new Exception("Data didn't transfer cleanly");529} else {530log("\tData transferred cleanly");531}532533a.position(a.limit());534b.position(b.limit());535a.limit(a.capacity());536b.limit(b.capacity());537}538539/*540* Logging code541*/542private static boolean resultOnce = true;543544private static void log(String str, SSLEngineResult result) {545if (!logging) {546return;547}548if (resultOnce) {549resultOnce = false;550System.err.println("The format of the SSLEngineResult is: \n" +551"\t\"getStatus() / getHandshakeStatus()\" +\n" +552"\t\"bytesConsumed() / bytesProduced()\"\n");553}554HandshakeStatus hsStatus = result.getHandshakeStatus();555log(str +556result.getStatus() + "/" + hsStatus + ", " +557result.bytesConsumed() + "/" + result.bytesProduced() +558" bytes");559if (hsStatus == HandshakeStatus.FINISHED) {560log("\t...ready for application data");561}562}563564private static void log(String str) {565if (logging) {566System.err.println(str);567}568}569570private static void dumpBuffer(String header, ByteBuffer data) {571data.mark();572System.err.format("========== %s ==========\n", header);573int i = 0;574while (data.remaining() > 0) {575if (i != 0 && i % 16 == 0) {576System.err.print("\n");577}578System.err.format("%02X ", data.get());579i++;580}581System.err.println();582data.reset();583}584585}586587588