changeset 51808:a0de9a3a6766

8206929: Check session context for TLS 1.3 session resumption Summary: additional checks to prevent TLS 1.3 sessions from being resumed when they shouldn't Reviewed-by: xuelei
author apetcher
date Tue, 17 Jul 2018 13:04:40 -0400
parents e15cd424736d
children 610d15624bdd
files src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java src/java.base/share/classes/sun/security/ssl/PreSharedKeyExtension.java src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java test/jdk/sun/security/ssl/SSLSessionImpl/ResumeChecksClient.java test/jdk/sun/security/ssl/SSLSessionImpl/ResumeChecksServer.java
diffstat 5 files changed, 676 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java	Tue Jul 17 19:25:45 2018 +0300
+++ b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java	Tue Jul 17 13:04:40 2018 -0400
@@ -27,6 +27,7 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -46,6 +47,9 @@
                 "Post-handshake not supported in " + negotiatedProtocol.name);
         }
 
+        this.localSupportedSignAlgs = new ArrayList<SignatureScheme>(
+            context.conSession.getLocalSupportedSignatureSchemes());
+
         handshakeConsumers = new LinkedHashMap<>(consumers);
         handshakeFinished = true;
     }
--- a/src/java.base/share/classes/sun/security/ssl/PreSharedKeyExtension.java	Tue Jul 17 19:25:45 2018 +0300
+++ b/src/java.base/share/classes/sun/security/ssl/PreSharedKeyExtension.java	Tue Jul 17 13:04:40 2018 -0400
@@ -33,8 +33,11 @@
 import java.util.Locale;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.Collection;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import static sun.security.ssl.ClientAuthType.CLIENT_AUTH_REQUIRED;
 import sun.security.ssl.ClientHello.ClientHelloMessage;
 import sun.security.ssl.SSLExtension.ExtensionConsumer;
 import sun.security.ssl.SSLExtension.SSLExtensionSpec;
@@ -167,7 +170,7 @@
 
         int getIdsEncodedLength() {
             int idEncodedLength = 0;
-            for(PskIdentity curId : identities) {
+            for (PskIdentity curId : identities) {
                 idEncodedLength += curId.getEncodedLength();
             }
 
@@ -190,7 +193,7 @@
             byte[] buffer = new byte[encodedLength];
             ByteBuffer m = ByteBuffer.wrap(buffer);
             Record.putInt16(m, idsEncodedLength);
-            for(PskIdentity curId : identities) {
+            for (PskIdentity curId : identities) {
                 curId.writeEncoded(m);
             }
             Record.putInt16(m, bindersEncodedLength);
@@ -220,7 +223,7 @@
 
         String identitiesString() {
             StringBuilder result = new StringBuilder();
-            for(PskIdentity curId : identities) {
+            for (PskIdentity curId : identities) {
                 result.append(curId.toString() + "\n");
             }
 
@@ -229,7 +232,7 @@
 
         String bindersString() {
             StringBuilder result = new StringBuilder();
-            for(byte[] curBinder : binders) {
+            for (byte[] curBinder : binders) {
                 result.append("{" + Utilities.toHexString(curBinder) + "}\n");
             }
 
@@ -328,6 +331,7 @@
         public void consume(ConnectionContext context,
                             HandshakeMessage message,
                             ByteBuffer buffer) throws IOException {
+            ClientHelloMessage clientHello = (ClientHelloMessage) message;
             ServerHandshakeContext shc = (ServerHandshakeContext)context;
             // Is it a supported and enabled extension?
             if (!shc.sslConfig.isAvailable(SSLExtension.CH_PRE_SHARED_KEY)) {
@@ -367,8 +371,7 @@
                 int idIndex = 0;
                 for (PskIdentity requestedId : pskSpec.identities) {
                     SSLSessionImpl s = sessionCache.get(requestedId.identity);
-                    if (s != null && s.isRejoinable() &&
-                            s.getPreSharedKey().isPresent()) {
+                    if (s != null && canRejoin(clientHello, shc, s)) {
                         if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
                             SSLLogger.fine("Resuming session: ", s);
                         }
@@ -392,10 +395,68 @@
 
             // update the context
             shc.handshakeExtensions.put(
-                    SSLExtension.CH_PRE_SHARED_KEY, pskSpec);
+                SSLExtension.CH_PRE_SHARED_KEY, pskSpec);
         }
     }
 
+    private static boolean canRejoin(ClientHelloMessage clientHello,
+        ServerHandshakeContext shc, SSLSessionImpl s) {
+
+        boolean result = s.isRejoinable() && s.getPreSharedKey().isPresent();
+
+        // Check protocol version
+        if (result && s.getProtocolVersion() != shc.negotiatedProtocol) {
+            if (SSLLogger.isOn &&
+                SSLLogger.isOn("ssl,handshake,verbose")) {
+
+                SSLLogger.finest("Can't resume, incorrect protocol version");
+            }
+            result = false;
+        }
+
+        // Validate the required client authentication.
+        if (result &&
+            (shc.sslConfig.clientAuthType == CLIENT_AUTH_REQUIRED)) {
+            try {
+                s.getPeerPrincipal();
+            } catch (SSLPeerUnverifiedException e) {
+                if (SSLLogger.isOn &&
+                        SSLLogger.isOn("ssl,handshake,verbose")) {
+                    SSLLogger.finest(
+                        "Can't resume, " +
+                        "client authentication is required");
+                }
+                result = false;
+            }
+
+            // Make sure the list of supported signature algorithms matches
+            Collection<SignatureScheme> sessionSigAlgs =
+                s.getLocalSupportedSignatureSchemes();
+            if (result &&
+                !shc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) {
+
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Can't resume. Session uses different " +
+                        "signature algorithms");
+                }
+                result = false;
+            }
+        }
+
+        // Ensure cipher suite can be negotiated
+        if (result && (!shc.isNegotiable(s.getSuite()) ||
+            !clientHello.cipherSuites.contains(s.getSuite()))) {
+            if (SSLLogger.isOn &&
+                    SSLLogger.isOn("ssl,handshake,verbose")) {
+                SSLLogger.finest(
+                    "Can't resume, unavailable session cipher suite");
+            }
+            result = false;
+        }
+
+        return result;
+    }
+
     private static final
             class CHPreSharedKeyUpdate implements HandshakeConsumer {
         // Prevent instantiation of this class.
@@ -547,6 +608,18 @@
                 return null;
             }
 
+            // Make sure the list of supported signature algorithms matches
+            Collection<SignatureScheme> sessionSigAlgs =
+                chc.resumingSession.getLocalSupportedSignatureSchemes();
+            if (!chc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Existing session uses different " +
+                        "signature algorithms");
+                }
+                return null;
+            }
+
+            // The session must have a pre-shared key
             Optional<SecretKey> pskOpt = chc.resumingSession.getPreSharedKey();
             if (!pskOpt.isPresent()) {
                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
@@ -658,7 +731,7 @@
             } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
                 throw new IOException(ex);
             }
-        } catch(GeneralSecurityException ex) {
+        } catch (GeneralSecurityException ex) {
             throw new IOException(ex);
         }
     }
--- a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java	Tue Jul 17 19:25:45 2018 +0300
+++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java	Tue Jul 17 13:04:40 2018 -0400
@@ -96,7 +96,7 @@
     private boolean             invalidated;
     private X509Certificate[]   localCerts;
     private PrivateKey          localPrivateKey;
-    private final String[]      localSupportedSignAlgs;
+    private final Collection<SignatureScheme>     localSupportedSignAlgs;
     private String[]            peerSupportedSignAlgs;      // for certificate
     private boolean             useDefaultPeerSignAlgs = false;
     private List<byte[]>        statusResponses;
@@ -144,7 +144,7 @@
         this.sessionId = new SessionId(false, null);
         this.host = null;
         this.port = -1;
-        this.localSupportedSignAlgs = new String[0];
+        this.localSupportedSignAlgs = Collections.emptySet();
         this.serverNameIndication = null;
         this.requestedServerNames = Collections.<SNIServerName>emptyList();
         this.useExtendedMasterSecret = false;
@@ -179,8 +179,9 @@
         this.sessionId = id;
         this.host = hc.conContext.transport.getPeerHost();
         this.port = hc.conContext.transport.getPeerPort();
-        this.localSupportedSignAlgs =
-                SignatureScheme.getAlgorithmNames(hc.localSupportedSignAlgs);
+        this.localSupportedSignAlgs = hc.localSupportedSignAlgs == null ?
+                Collections.emptySet() :
+                Collections.unmodifiableCollection(hc.localSupportedSignAlgs);
         this.serverNameIndication = hc.negotiatedServerName;
         this.requestedServerNames = Collections.<SNIServerName>unmodifiableList(
                 hc.getRequestedServerNames());
@@ -969,16 +970,20 @@
     }
 
     /**
-     * Gets an array of supported signature algorithms that the local side is
-     * willing to verify.
+     * Gets an array of supported signature algorithm names that the local
+     * side is willing to verify.
      */
     @Override
     public String[] getLocalSupportedSignatureAlgorithms() {
-        if (localSupportedSignAlgs != null) {
-            return localSupportedSignAlgs.clone();
-        }
+        return SignatureScheme.getAlgorithmNames(localSupportedSignAlgs);
+    }
 
-        return new String[0];
+    /**
+     * Gets an array of supported signature schemes that the local side is
+     * willing to verify.
+     */
+    public Collection<SignatureScheme> getLocalSupportedSignatureSchemes() {
+        return localSupportedSignAlgs;
     }
 
     /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/ssl/SSLSessionImpl/ResumeChecksClient.java	Tue Jul 17 13:04:40 2018 -0400
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 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.
+ */
+
+/*
+ * @test
+ * @bug 8206929
+ * @summary ensure that client only resumes a session if certain properties
+ *    of the session are compatible with the new connection
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksClient BASIC
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient BASIC
+ * @run main/othervm ResumeChecksClient BASIC
+ * @run main/othervm ResumeChecksClient VERSION_2_TO_3
+ * @run main/othervm ResumeChecksClient VERSION_3_TO_2
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient CIPHER_SUITE
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient SIGNATURE_SCHEME
+ *
+ */
+
+import javax.net.*;
+import javax.net.ssl.*;
+import java.io.*;
+import java.security.*;
+import java.net.*;
+import java.util.*;
+
+public class ResumeChecksClient {
+
+    static String pathToStores = "../../../../javax/net/ssl/etc";
+    static String keyStoreFile = "keystore";
+    static String trustStoreFile = "truststore";
+    static String passwd = "passphrase";
+
+    enum TestMode {
+        BASIC,
+        VERSION_2_TO_3,
+        VERSION_3_TO_2,
+        CIPHER_SUITE,
+        SIGNATURE_SCHEME
+    }
+
+    public static void main(String[] args) throws Exception {
+
+        TestMode mode = TestMode.valueOf(args[0]);
+
+        String keyFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + keyStoreFile;
+        String trustFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + trustStoreFile;
+
+        System.setProperty("javax.net.ssl.keyStore", keyFilename);
+        System.setProperty("javax.net.ssl.keyStorePassword", passwd);
+        System.setProperty("javax.net.ssl.trustStore", trustFilename);
+        System.setProperty("javax.net.ssl.trustStorePassword", passwd);
+
+        Server server = startServer();
+        server.signal();
+        SSLContext sslContext = SSLContext.getDefault();
+        while (!server.started) {
+            Thread.yield();
+        }
+        connect(sslContext, server.port, mode, false);
+
+        server.signal();
+        long secondStartTime = System.currentTimeMillis();
+        Thread.sleep(10);
+        SSLSession secondSession = connect(sslContext, server.port, mode, true);
+
+        server.go = false;
+        server.signal();
+
+        switch (mode) {
+        case BASIC:
+            // fail if session is not resumed
+            if (secondSession.getCreationTime() > secondStartTime) {
+                throw new RuntimeException("Session was not reused");
+            }
+            break;
+        case VERSION_2_TO_3:
+        case VERSION_3_TO_2:
+        case CIPHER_SUITE:
+        case SIGNATURE_SCHEME:
+            // fail if a new session is not created
+            if (secondSession.getCreationTime() <= secondStartTime) {
+                throw new RuntimeException("Existing session was used");
+            }
+            break;
+        default:
+            throw new RuntimeException("unknown mode: " + mode);
+        }
+    }
+
+    private static class NoSig implements AlgorithmConstraints {
+
+        private final String alg;
+
+        NoSig(String alg) {
+            this.alg = alg;
+        }
+
+
+        private boolean test(String a) {
+            return !a.toLowerCase().contains(alg.toLowerCase());
+        }
+
+        public boolean permits(Set<CryptoPrimitive> primitives, Key key) {
+            return true;
+        }
+        public boolean permits(Set<CryptoPrimitive> primitives,
+            String algorithm, AlgorithmParameters parameters) {
+
+            return test(algorithm);
+        }
+        public boolean permits(Set<CryptoPrimitive> primitives,
+            String algorithm, Key key, AlgorithmParameters parameters) {
+
+            return test(algorithm);
+        }
+    }
+
+    private static SSLSession connect(SSLContext sslContext, int port,
+        TestMode mode, boolean second) {
+
+        try {
+            SSLSocket sock = (SSLSocket)
+                sslContext.getSocketFactory().createSocket();
+            SSLParameters params = sock.getSSLParameters();
+
+            switch (mode) {
+            case BASIC:
+                // do nothing to ensure resumption works
+                break;
+            case VERSION_2_TO_3:
+                if (second) {
+                    params.setProtocols(new String[] {"TLSv1.3"});
+                } else {
+                    params.setProtocols(new String[] {"TLSv1.2"});
+                }
+                break;
+            case VERSION_3_TO_2:
+                if (second) {
+                    params.setProtocols(new String[] {"TLSv1.2"});
+                } else {
+                    params.setProtocols(new String[] {"TLSv1.3"});
+                }
+                break;
+            case CIPHER_SUITE:
+                if (second) {
+                    params.setCipherSuites(
+                        new String[] {"TLS_AES_256_GCM_SHA384"});
+                } else {
+                    params.setCipherSuites(
+                        new String[] {"TLS_AES_128_GCM_SHA256"});
+                }
+                break;
+            case SIGNATURE_SCHEME:
+                AlgorithmConstraints constraints =
+                    params.getAlgorithmConstraints();
+                if (second) {
+                    params.setAlgorithmConstraints(new NoSig("ecdsa"));
+                } else {
+                    params.setAlgorithmConstraints(new NoSig("rsa"));
+                }
+                break;
+            default:
+                throw new RuntimeException("unknown mode: " + mode);
+            }
+            sock.setSSLParameters(params);
+            sock.connect(new InetSocketAddress("localhost", port));
+            PrintWriter out = new PrintWriter(
+                new OutputStreamWriter(sock.getOutputStream()));
+            out.println("message");
+            out.flush();
+            BufferedReader reader = new BufferedReader(
+                new InputStreamReader(sock.getInputStream()));
+            String inMsg = reader.readLine();
+            System.out.println("Client received: " + inMsg);
+            SSLSession result = sock.getSession();
+            sock.close();
+            return result;
+        } catch (Exception ex) {
+            // unexpected exception
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private static Server startServer() {
+        Server server = new Server();
+        new Thread(server).start();
+        return server;
+    }
+
+    private static class Server implements Runnable {
+
+        public volatile boolean go = true;
+        private boolean signal = false;
+        public volatile int port = 0;
+        public volatile boolean started = false;
+
+        private synchronized void waitForSignal() {
+            while (!signal) {
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                    // do nothing
+                }
+            }
+            signal = false;
+        }
+        public synchronized void signal() {
+            signal = true;
+            notify();
+        }
+
+        public void run() {
+            try {
+
+                SSLContext sc = SSLContext.getDefault();
+                ServerSocketFactory fac = sc.getServerSocketFactory();
+                SSLServerSocket ssock = (SSLServerSocket)
+                    fac.createServerSocket(0);
+                this.port = ssock.getLocalPort();
+
+                waitForSignal();
+                started = true;
+                while (go) {
+                    try {
+                        System.out.println("Waiting for connection");
+                        Socket sock = ssock.accept();
+                        BufferedReader reader = new BufferedReader(
+                            new InputStreamReader(sock.getInputStream()));
+                        String line = reader.readLine();
+                        System.out.println("server read: " + line);
+                        PrintWriter out = new PrintWriter(
+                            new OutputStreamWriter(sock.getOutputStream()));
+                        out.println(line);
+                        out.flush();
+                        waitForSignal();
+                    } catch (Exception ex) {
+                        ex.printStackTrace();
+                    }
+                }
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/ssl/SSLSessionImpl/ResumeChecksServer.java	Tue Jul 17 13:04:40 2018 -0400
@@ -0,0 +1,307 @@
+/*
+ * Copyright (c) 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.
+ */
+
+/*
+ * @test
+ * @bug 8206929
+ * @summary ensure that server only resumes a session if certain properties
+ *    of the session are compatible with the new connection
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksServer BASIC
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer BASIC
+ * @run main/othervm ResumeChecksServer BASIC
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksServer CLIENT_AUTH
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer CLIENT_AUTH
+ * @run main/othervm ResumeChecksServer CLIENT_AUTH
+ * @run main/othervm ResumeChecksServer VERSION_2_TO_3
+ * @run main/othervm ResumeChecksServer VERSION_3_TO_2
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer CIPHER_SUITE
+ * @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer SIGNATURE_SCHEME
+ *
+ */
+
+import javax.net.*;
+import javax.net.ssl.*;
+import java.io.*;
+import java.security.*;
+import java.net.*;
+import java.util.*;
+
+public class ResumeChecksServer {
+
+    static String pathToStores = "../../../../javax/net/ssl/etc";
+    static String keyStoreFile = "keystore";
+    static String trustStoreFile = "truststore";
+    static String passwd = "passphrase";
+
+    enum TestMode {
+        BASIC,
+        CLIENT_AUTH,
+        VERSION_2_TO_3,
+        VERSION_3_TO_2,
+        CIPHER_SUITE,
+        SIGNATURE_SCHEME
+    }
+
+    public static void main(String[] args) throws Exception {
+
+        TestMode mode = TestMode.valueOf(args[0]);
+
+        String keyFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + keyStoreFile;
+        String trustFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + trustStoreFile;
+
+        System.setProperty("javax.net.ssl.keyStore", keyFilename);
+        System.setProperty("javax.net.ssl.keyStorePassword", passwd);
+        System.setProperty("javax.net.ssl.trustStore", trustFilename);
+        System.setProperty("javax.net.ssl.trustStorePassword", passwd);
+
+        SSLSession secondSession = null;
+
+        SSLContext sslContext = SSLContext.getDefault();
+        ServerSocketFactory fac = sslContext.getServerSocketFactory();
+        SSLServerSocket ssock = (SSLServerSocket)
+            fac.createServerSocket(0);
+
+        Client client = startClient(ssock.getLocalPort());
+
+        try {
+            connect(client, ssock, mode, false);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+
+        long secondStartTime = System.currentTimeMillis();
+        Thread.sleep(10);
+        try {
+            secondSession = connect(client, ssock, mode, true);
+        } catch (SSLHandshakeException ex) {
+            // this is expected
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+
+        client.go = false;
+        client.signal();
+
+        switch (mode) {
+        case BASIC:
+            // fail if session is not resumed
+            if (secondSession.getCreationTime() > secondStartTime) {
+                throw new RuntimeException("Session was not reused");
+            }
+            break;
+        case CLIENT_AUTH:
+            // throws an exception if the client is not authenticated
+            secondSession.getPeerCertificates();
+            break;
+        case VERSION_2_TO_3:
+        case VERSION_3_TO_2:
+        case CIPHER_SUITE:
+        case SIGNATURE_SCHEME:
+            // fail if a new session is not created
+            if (secondSession.getCreationTime() <= secondStartTime) {
+                throw new RuntimeException("Existing session was used");
+            }
+            break;
+        default:
+            throw new RuntimeException("unknown mode: " + mode);
+        }
+    }
+
+    private static class NoSig implements AlgorithmConstraints {
+
+        private final String alg;
+
+        NoSig(String alg) {
+            this.alg = alg;
+        }
+
+
+        private boolean test(String a) {
+            return !a.toLowerCase().contains(alg.toLowerCase());
+        }
+
+        public boolean permits(Set<CryptoPrimitive> primitives, Key key) {
+            return true;
+        }
+        public boolean permits(Set<CryptoPrimitive> primitives,
+            String algorithm, AlgorithmParameters parameters) {
+
+            return test(algorithm);
+        }
+        public boolean permits(Set<CryptoPrimitive> primitives,
+            String algorithm, Key key, AlgorithmParameters parameters) {
+
+            return test(algorithm);
+        }
+    }
+
+    private static SSLSession connect(Client client, SSLServerSocket ssock,
+        TestMode mode, boolean second) throws Exception {
+
+        try {
+            client.signal();
+            System.out.println("Waiting for connection");
+            SSLSocket sock = (SSLSocket) ssock.accept();
+            SSLParameters params = sock.getSSLParameters();
+
+            switch (mode) {
+            case BASIC:
+                // do nothing to ensure resumption works
+                break;
+            case CLIENT_AUTH:
+                if (second) {
+                    params.setNeedClientAuth(true);
+                } else {
+                    params.setNeedClientAuth(false);
+                }
+                break;
+            case VERSION_2_TO_3:
+                if (second) {
+                    params.setProtocols(new String[] {"TLSv1.3"});
+                } else {
+                    params.setProtocols(new String[] {"TLSv1.2"});
+                }
+                break;
+            case VERSION_3_TO_2:
+                if (second) {
+                    params.setProtocols(new String[] {"TLSv1.2"});
+                } else {
+                    params.setProtocols(new String[] {"TLSv1.3"});
+                }
+                break;
+            case CIPHER_SUITE:
+                if (second) {
+                    params.setCipherSuites(
+                        new String[] {"TLS_AES_128_GCM_SHA256"});
+                } else {
+                    params.setCipherSuites(
+                        new String[] {"TLS_AES_256_GCM_SHA384"});
+                }
+                break;
+            case SIGNATURE_SCHEME:
+                params.setNeedClientAuth(true);
+                AlgorithmConstraints constraints =
+                    params.getAlgorithmConstraints();
+                if (second) {
+                    params.setAlgorithmConstraints(new NoSig("ecdsa"));
+                } else {
+                    params.setAlgorithmConstraints(new NoSig("rsa"));
+                }
+                break;
+            default:
+                throw new RuntimeException("unknown mode: " + mode);
+            }
+            sock.setSSLParameters(params);
+            BufferedReader reader = new BufferedReader(
+                new InputStreamReader(sock.getInputStream()));
+            String line = reader.readLine();
+            System.out.println("server read: " + line);
+            PrintWriter out = new PrintWriter(
+                new OutputStreamWriter(sock.getOutputStream()));
+            out.println(line);
+            out.flush();
+            out.close();
+            SSLSession result = sock.getSession();
+            sock.close();
+            return result;
+        } catch (SSLHandshakeException ex) {
+            if (!second) {
+                throw ex;
+            }
+        }
+        return null;
+    }
+
+    private static Client startClient(int port) {
+        Client client = new Client(port);
+        new Thread(client).start();
+        return client;
+    }
+
+    private static class Client implements Runnable {
+
+        public volatile boolean go = true;
+        private boolean signal = false;
+        private final int port;
+
+        Client(int port) {
+            this.port = port;
+        }
+
+        private synchronized void waitForSignal() {
+            while (!signal) {
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                    // do nothing
+                }
+            }
+            signal = false;
+
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException ex) {
+                // do nothing
+            }
+        }
+        public synchronized void signal() {
+            signal = true;
+            notify();
+        }
+
+        public void run() {
+            try {
+
+                SSLContext sc = SSLContext.getDefault();
+
+                waitForSignal();
+                while (go) {
+                    try {
+                        SSLSocket sock = (SSLSocket)
+                            sc.getSocketFactory().createSocket();
+                        sock.connect(new InetSocketAddress("localhost", port));
+                        PrintWriter out = new PrintWriter(
+                            new OutputStreamWriter(sock.getOutputStream()));
+                        out.println("message");
+                        out.flush();
+                        BufferedReader reader = new BufferedReader(
+                            new InputStreamReader(sock.getInputStream()));
+                        String inMsg = reader.readLine();
+                        System.out.println("Client received: " + inMsg);
+                        out.close();
+                        sock.close();
+                        waitForSignal();
+                    } catch (Exception ex) {
+                        ex.printStackTrace();
+                    }
+                }
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+    }
+}