You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by je...@apache.org on 2021/09/14 15:45:05 UTC
[geode] branch develop updated: GEODE-9542: Enable SSL client
authentication for Radish (#6826)
This is an automated email from the ASF dual-hosted git repository.
jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 81153cf GEODE-9542: Enable SSL client authentication for Radish (#6826)
81153cf is described below
commit 81153cf6b1f4b99fc992ccceb06f287455b150d9
Author: Jens Deppe <jd...@vmware.com>
AuthorDate: Tue Sep 14 08:43:56 2021 -0700
GEODE-9542: Enable SSL client authentication for Radish (#6826)
- When Geode's ssl-require-authentication is enabled, Redis clients must
authenticate with a valid certificate.
- This all wraps the trust and key store in a `FileWatching*Manager` to enable
automatic cert rotation.
---
.../geode/redis/internal/ssl/TestSSLServer.java | 1491 ++++++++++++++++++++
.../java/org/apache/geode/redis/SSLDUnitTest.java | 336 +++++
.../java/org/apache/geode/redis/SSLTest.java | 80 --
.../redis/internal/netty/NettyRedisServer.java | 64 +-
.../org/apache/geode/internal/net/SSLConfig.java | 14 +
.../internal/net/filewatch/PollingFileWatcher.java | 4 +-
6 files changed, 1885 insertions(+), 104 deletions(-)
diff --git a/geode-apis-compatible-with-redis/src/commonTest/java/org/apache/geode/redis/internal/ssl/TestSSLServer.java b/geode-apis-compatible-with-redis/src/commonTest/java/org/apache/geode/redis/internal/ssl/TestSSLServer.java
new file mode 100644
index 0000000..dc72434
--- /dev/null
+++ b/geode-apis-compatible-with-redis/src/commonTest/java/org/apache/geode/redis/internal/ssl/TestSSLServer.java
@@ -0,0 +1,1491 @@
+
+package org.apache.geode.redis.internal.ssl;
+
+/*
+ * Command-line tool to test a SSL/TLS server for some vulnerabilities.
+ * =====================================================================
+ *
+ * This application connects to the provided SSL/TLS server (by name and
+ * port) and extracts the following information:
+ * - supported versions (SSL 2.0, SSL 3.0, TLS 1.0 to 1.2)
+ * - support of Deflate compression
+ * - list of supported cipher suites (for each protocol version)
+ * - BEAST/CRIME vulnerabilities.
+ *
+ * BEAST and CRIME are client-side attack, but the server can protect the
+ * client by refusing to use the feature combinations which can be
+ * attacked. For CRIME, the weakness is Deflate compression. For BEAST,
+ * the attack conditions are more complex: it works with CBC ciphers with
+ * SSL 3.0 and TLS 1.0. Hence, a server fails to protect the client against
+ * BEAST if it does not enforce usage of RC4 over CBC ciphers under these
+ * protocol versions, if given the choice.
+ *
+ * (The BEAST test considers only the cipher suites with strong
+ * encryption; if the server supports none, then there are bigger
+ * problems. We also assume that all clients support RC4-128; thus, the
+ * server protects the client if it selects RC4-128 even if some strong
+ * CBC-based ciphers are announced as supported by the client with a
+ * higher preference level.)
+ *
+ * ----------------------------------------------------------------------
+ * Copyright (c) 2012 Thomas Pornin <po...@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ * ----------------------------------------------------------------------
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+public class TestSSLServer {
+
+ private final Set<Integer> protocolIds = new TreeSet<>();
+ private final Map<Integer, Set<Integer>> cipherSuiteIds = new TreeMap<>();
+ private final Set<String> certificateIds = new TreeSet<>();
+ private String host;
+ private int port;
+
+ public TestSSLServer(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ static void usage() {
+ System.err.println("usage: TestSSLServer servername [ port ]");
+ System.exit(1);
+ }
+
+ public static void main(String[] args) throws IOException {
+ if (args.length == 0 || args.length > 2) {
+ usage();
+ }
+ String name = args[0];
+ int port = 443;
+ if (args.length == 2) {
+ try {
+ port = Integer.parseInt(args[1]);
+ } catch (NumberFormatException nfe) {
+ usage();
+ }
+ if (port <= 0 || port > 65535) {
+ usage();
+ }
+ }
+ InetSocketAddress isa = new InetSocketAddress(name, port);
+
+ Set<Integer> protocolVersions = new TreeSet<Integer>();
+ boolean compress = false;
+ for (int v = 0x0300; v <= 0x0303; v++) {
+ ServerHello serverHello = connect(isa, v, CIPHER_SUITES.keySet());
+ if (serverHello == null) {
+ continue;
+ }
+ protocolVersions.add(serverHello.protoVersion);
+ if (serverHello.compression == 1) {
+ compress = true;
+ }
+ }
+
+ ServerHelloSSLv2 serverHello2 = connectV2(isa);
+
+ if (serverHello2 != null) {
+ protocolVersions.add(0x0200);
+ }
+
+ if (protocolVersions.size() == 0) {
+ System.out.println("No SSL/TLS server at " + isa);
+ System.exit(1);
+ }
+ System.out.print("Supported versions:");
+ for (int v : protocolVersions) {
+ System.out.print(" ");
+ System.out.print(versionString(v));
+ }
+ System.out.println();
+ System.out.println("Deflate compression: "
+ + (compress ? "YES" : "no"));
+
+ System.out.println("Supported cipher suites"
+ + " (ORDER IS NOT SIGNIFICANT):");
+ Set<Integer> lastSuppCS = null;
+ Map<Integer, Set<Integer>> suppCS =
+ new TreeMap<Integer, Set<Integer>>();
+ Set<String> certID = new TreeSet<String>();
+
+ if (serverHello2 != null) {
+ System.out.println(" " + versionString(0x0200));
+ Set<Integer> vc2 = new TreeSet<Integer>();
+ for (int c : serverHello2.cipherSuites) {
+ vc2.add(c);
+ }
+ for (int c : vc2) {
+ System.out.println(" "
+ + cipherSuiteStringV2(c));
+ }
+ suppCS.put(0x0200, vc2);
+ if (serverHello2.serverCertName != null) {
+ certID.add(serverHello2.serverCertHash
+ + ": " + serverHello2.serverCertName);
+ }
+ }
+
+ for (int v : protocolVersions) {
+ if (v == 0x0200) {
+ continue;
+ }
+ Set<Integer> vsc = supportedSuites(isa, v, certID);
+ suppCS.put(v, vsc);
+ if (lastSuppCS == null || !lastSuppCS.equals(vsc)) {
+ System.out.println(" " + versionString(v));
+ for (int c : vsc) {
+ System.out.println(" "
+ + cipherSuiteString(c));
+ }
+ lastSuppCS = vsc;
+ } else {
+ System.out.println(" (" + versionString(v)
+ + ": idem)");
+ }
+ }
+ System.out.println("----------------------");
+ if (certID.size() == 0) {
+ System.out.println("No server certificate !");
+ } else {
+ System.out.println("Server certificate(s):");
+ for (String cc : certID) {
+ System.out.println(" " + cc);
+ }
+ }
+ System.out.println("----------------------");
+ int agMaxStrength = STRONG;
+ int agMinStrength = STRONG;
+ boolean vulnBEAST = false;
+ for (int v : protocolVersions) {
+ Set<Integer> vsc = suppCS.get(v);
+ agMaxStrength = Math.min(
+ maxStrength(vsc), agMaxStrength);
+ agMinStrength = Math.min(
+ minStrength(vsc), agMinStrength);
+ if (!vulnBEAST) {
+ vulnBEAST = testBEAST(isa, v, vsc);
+ }
+ }
+ System.out.println("Minimal encryption strength: "
+ + strengthString(agMinStrength));
+ System.out.println("Achievable encryption strength: "
+ + strengthString(agMaxStrength));
+ System.out.println("BEAST status: "
+ + (vulnBEAST ? "vulnerable" : "protected"));
+ System.out.println("CRIME status: "
+ + (compress ? "vulnerable" : "protected"));
+
+ }
+
+ public List<String> getProtocolNames() {
+ List<String> protocols = new ArrayList<>(protocolIds.size());
+ for (int pv : protocolIds) {
+ protocols.add(versionString(pv));
+ }
+
+ return protocols;
+ }
+
+ public List<String> getCipherSuiteNames() {
+ Set<String> ciphers = new HashSet<>();
+ for (Map.Entry<Integer, Set<Integer>> e : cipherSuiteIds.entrySet()) {
+ for (Integer cv : e.getValue()) {
+ if (e.getKey() == 0x0200) {
+ ciphers.add(cipherSuiteStringV2(cv));
+ } else {
+ ciphers.add(cipherSuiteString(cv));
+ }
+ }
+ }
+
+ return new ArrayList<>(ciphers);
+ }
+
+ public TestSSLServer execute() {
+ InetSocketAddress isa = new InetSocketAddress(host, port);
+
+ boolean compress = false;
+ for (int v = 0x0300; v <= 0x0303; v++) {
+ ServerHello serverHello = connect(isa, v, CIPHER_SUITES.keySet());
+ if (serverHello == null) {
+ continue;
+ }
+ protocolIds.add(serverHello.protoVersion);
+ if (serverHello.compression == 1) {
+ compress = true;
+ }
+ }
+
+ ServerHelloSSLv2 serverHello2 = connectV2(isa);
+
+ if (serverHello2 != null) {
+ protocolIds.add(0x0200);
+ }
+
+ if (protocolIds.size() == 0) {
+ return this;
+ }
+
+ if (serverHello2 != null) {
+ Set<Integer> vc2 = new TreeSet<>();
+ for (int c : serverHello2.cipherSuites) {
+ vc2.add(c);
+ }
+ cipherSuiteIds.put(0x0200, vc2);
+ if (serverHello2.serverCertName != null) {
+ certificateIds.add(serverHello2.serverCertHash + ": " + serverHello2.serverCertName);
+ }
+ }
+
+ for (int v : protocolIds) {
+ if (v == 0x0200) {
+ continue;
+ }
+ Set<Integer> vsc = supportedSuites(isa, v, certificateIds);
+ cipherSuiteIds.put(v, vsc);
+ }
+
+ return this;
+ }
+
+ /*
+ * Get cipher suites supported by the server. This is done by
+ * repeatedly contacting the server, each time removing from our
+ * list of supported suites the suite which the server just
+ * selected. We keep on until the server can no longer respond
+ * to us with a ServerHello.
+ */
+ static Set<Integer> supportedSuites(InetSocketAddress isa, int version,
+ Set<String> serverCertID) {
+ Set<Integer> cs = new TreeSet<Integer>(CIPHER_SUITES.keySet());
+ Set<Integer> rs = new TreeSet<Integer>();
+ for (;;) {
+ ServerHello sh = connect(isa, version, cs);
+ if (sh == null) {
+ break;
+ }
+ if (!cs.contains(sh.cipherSuite)) {
+ System.err.printf("[ERR: server wants to use"
+ + " cipher suite 0x%04X which client"
+ + " did not announce]", sh.cipherSuite);
+ System.err.println();
+ break;
+ }
+ cs.remove(sh.cipherSuite);
+ rs.add(sh.cipherSuite);
+ if (sh.serverCertName != null) {
+ serverCertID.add(sh.serverCertHash
+ + ": " + sh.serverCertName);
+ }
+ }
+ return rs;
+ }
+
+ static int minStrength(Set<Integer> supp) {
+ int m = STRONG;
+ for (int suite : supp) {
+ CipherSuite cs = CIPHER_SUITES.get(suite);
+ if (cs == null) {
+ continue;
+ }
+ if (cs.strength < m) {
+ m = cs.strength;
+ }
+ }
+ return m;
+ }
+
+ static int maxStrength(Set<Integer> supp) {
+ int m = CLEAR;
+ for (int suite : supp) {
+ CipherSuite cs = CIPHER_SUITES.get(suite);
+ if (cs == null) {
+ continue;
+ }
+ if (cs.strength > m) {
+ m = cs.strength;
+ }
+ }
+ return m;
+ }
+
+ static boolean testBEAST(InetSocketAddress isa,
+ int version, Set<Integer> supp) {
+ /*
+ * TLS 1.1+ is not vulnerable to BEAST.
+ * We do not test SSLv2 either.
+ */
+ if (version < 0x0300 || version > 0x0301) {
+ return false;
+ }
+
+ /*
+ * BEAST attack works if the server allows the client to
+ * use a CBC cipher. Existing clients also supports RC4,
+ * so we consider that a server protects the clients if
+ * it chooses RC4 over CBC streams when given the choice.
+ * We only consider strong cipher suites here.
+ */
+ List<Integer> strongCBC = new ArrayList<Integer>();
+ List<Integer> strongStream = new ArrayList<Integer>();
+ for (int suite : supp) {
+ CipherSuite cs = CIPHER_SUITES.get(suite);
+ if (cs == null) {
+ continue;
+ }
+ if (cs.strength < STRONG) {
+ continue;
+ }
+ if (cs.isCBC) {
+ strongCBC.add(suite);
+ } else {
+ strongStream.add(suite);
+ }
+ }
+ if (strongCBC.size() == 0) {
+ return false;
+ }
+ if (strongStream.size() == 0) {
+ return true;
+ }
+ List<Integer> ns = new ArrayList<Integer>(strongCBC);
+ ns.addAll(strongStream);
+ ServerHello sh = connect(isa, version, ns);
+ return !strongStream.contains(sh.cipherSuite);
+ }
+
+ static String versionString(int version) {
+ if (version == 0x0200) {
+ return "SSLv2";
+ } else if (version == 0x0300) {
+ return "SSLv3";
+ } else if ((version >>> 8) == 0x03) {
+ return "TLSv1." + ((version & 0xFF) - 1);
+ } else {
+ return String.format("UNKNOWN_VERSION:0x%04X", version);
+ }
+ }
+
+ /*
+ * Connect to the server, send a ClientHello, and decode the
+ * response (ServerHello). On error, null is returned.
+ */
+ static ServerHello connect(InetSocketAddress isa,
+ int version, Collection<Integer> cipherSuites) {
+ Socket s = null;
+ try {
+ s = new Socket();
+ try {
+ s.connect(isa);
+ } catch (IOException ioe) {
+ System.err.println("could not connect to "
+ + isa + ": " + ioe.toString());
+ return null;
+ }
+ byte[] ch = makeClientHello(version, cipherSuites);
+ OutputRecord orec = new OutputRecord(
+ s.getOutputStream());
+ orec.setType(HANDSHAKE);
+ orec.setVersion(version);
+ orec.write(ch);
+ orec.flush();
+ return new ServerHello(s.getInputStream());
+ } catch (IOException ioe) {
+ // ignored
+ } finally {
+ try {
+ s.close();
+ } catch (IOException ioe) {
+ // ignored
+ }
+ }
+ return null;
+ }
+
+ /*
+ * Connect to the server, send a SSLv2 CLIENT HELLO, and decode
+ * the response (SERVER HELLO). On error, null is returned.
+ */
+ static ServerHelloSSLv2 connectV2(InetSocketAddress isa) {
+ Socket s = null;
+ try {
+ s = new Socket();
+ try {
+ s.connect(isa);
+ } catch (IOException ioe) {
+ System.err.println("could not connect to "
+ + isa + ": " + ioe.toString());
+ return null;
+ }
+ s.getOutputStream().write(SSL2_CLIENT_HELLO);
+ return new ServerHelloSSLv2(s.getInputStream());
+ } catch (IOException ioe) {
+ // ignored
+ } finally {
+ try {
+ s.close();
+ } catch (IOException ioe) {
+ // ignored
+ }
+ }
+ return null;
+ }
+
+ static final void enc16be(int val, byte[] buf, int off) {
+ buf[off] = (byte) (val >>> 8);
+ buf[off + 1] = (byte) val;
+ }
+
+ static final void enc24be(int val, byte[] buf, int off) {
+ buf[off] = (byte) (val >>> 16);
+ buf[off + 1] = (byte) (val >>> 8);
+ buf[off + 2] = (byte) val;
+ }
+
+ static final void enc32be(int val, byte[] buf, int off) {
+ buf[off] = (byte) (val >>> 24);
+ buf[off + 1] = (byte) (val >>> 16);
+ buf[off + 2] = (byte) (val >>> 8);
+ buf[off + 3] = (byte) val;
+ }
+
+ static final int dec16be(byte[] buf, int off) {
+ return ((buf[off] & 0xFF) << 8)
+ | (buf[off + 1] & 0xFF);
+ }
+
+ static final int dec24be(byte[] buf, int off) {
+ return ((buf[off] & 0xFF) << 16)
+ | ((buf[off + 1] & 0xFF) << 8)
+ | (buf[off + 2] & 0xFF);
+ }
+
+ static final int dec32be(byte[] buf, int off) {
+ return ((buf[off] & 0xFF) << 24)
+ | ((buf[off + 1] & 0xFF) << 16)
+ | ((buf[off + 2] & 0xFF) << 8)
+ | (buf[off + 3] & 0xFF);
+ }
+
+ static final int CHANGE_CIPHER_SPEC = 20;
+ static final int ALERT = 21;
+ static final int HANDSHAKE = 22;
+ static final int APPLICATION = 23;
+
+ static final int MAX_RECORD_LEN = 16384;
+
+ /*
+ * A custom stream which encodes data bytes into SSL/TLS records
+ * (no encryption).
+ */
+ static class OutputRecord extends OutputStream {
+
+ private OutputStream out;
+ private byte[] buffer = new byte[MAX_RECORD_LEN + 5];
+ private int ptr;
+ private int version;
+ private int type;
+
+ OutputRecord(OutputStream out) {
+ this.out = out;
+ ptr = 5;
+ }
+
+ void setType(int type) {
+ this.type = type;
+ }
+
+ void setVersion(int version) {
+ this.version = version;
+ }
+
+ public void flush()
+ throws IOException {
+ buffer[0] = (byte) type;
+ enc16be(version, buffer, 1);
+ enc16be(ptr - 5, buffer, 3);
+ out.write(buffer, 0, ptr);
+ out.flush();
+ ptr = 5;
+ }
+
+ public void write(int b)
+ throws IOException {
+ buffer[ptr++] = (byte) b;
+ if (ptr == buffer.length) {
+ flush();
+ }
+ }
+
+ public void write(byte[] buf, int off, int len)
+ throws IOException {
+ while (len > 0) {
+ int clen = Math.min(buffer.length - ptr, len);
+ System.arraycopy(buf, off, buffer, ptr, clen);
+ ptr += clen;
+ off += clen;
+ len -= clen;
+ if (ptr == buffer.length) {
+ flush();
+ }
+ }
+ }
+ }
+
+ static void readFully(InputStream in, byte[] buf)
+ throws IOException {
+ readFully(in, buf, 0, buf.length);
+ }
+
+ static void readFully(InputStream in, byte[] buf, int off, int len)
+ throws IOException {
+ while (len > 0) {
+ int rlen = in.read(buf, off, len);
+ if (rlen < 0) {
+ throw new EOFException();
+ }
+ off += rlen;
+ len -= rlen;
+ }
+ }
+
+ /*
+ * A custom stream which expects SSL/TLS records (no encryption)
+ * and rebuilds the encoded data stream. Incoming records MUST
+ * have the expected type (e.g. "handshake"); alert messages
+ * are skipped.
+ */
+ static class InputRecord extends InputStream {
+
+ private InputStream in;
+ private byte[] buffer = new byte[MAX_RECORD_LEN + 5];
+ private int ptr, end;
+ private int version;
+ private int type;
+ private int expectedType;
+
+ InputRecord(InputStream in) {
+ this.in = in;
+ ptr = 0;
+ end = 0;
+ }
+
+ void setExpectedType(int expectedType) {
+ this.expectedType = expectedType;
+ }
+
+ int getVersion() {
+ return version;
+ }
+
+ private void refill()
+ throws IOException {
+ for (;;) {
+ readFully(in, buffer, 0, 5);
+ type = buffer[0] & 0xFF;
+ version = dec16be(buffer, 1);
+ end = dec16be(buffer, 3);
+ readFully(in, buffer, 0, end);
+ ptr = 0;
+ if (type != expectedType) {
+ if (type == ALERT) {
+ /*
+ * We just ignore alert
+ * messages.
+ */
+ continue;
+ }
+ throw new IOException(
+ "unexpected record type: "
+ + type);
+ }
+ return;
+ }
+ }
+
+ public int read()
+ throws IOException {
+ while (ptr == end) {
+ refill();
+ }
+ return buffer[ptr++] & 0xFF;
+ }
+
+ public int read(byte[] buf, int off, int len)
+ throws IOException {
+ while (ptr == end) {
+ refill();
+ }
+ int clen = Math.min(end - ptr, len);
+ System.arraycopy(buffer, ptr, buf, off, clen);
+ ptr += clen;
+ return clen;
+ }
+ }
+
+ private static final SecureRandom RNG = new SecureRandom();
+
+ /*
+ * Build a ClientHello message, with the specified maximum
+ * supported version, and list of cipher suites.
+ */
+ static byte[] makeClientHello(int version,
+ Collection<Integer> cipherSuites) {
+ try {
+ return makeClientHello0(version, cipherSuites);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ static byte[] makeClientHello0(int version,
+ Collection<Integer> cipherSuites)
+ throws IOException {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+
+ /*
+ * Message header:
+ * message type: one byte (1 = "ClientHello")
+ * message length: three bytes (this will be adjusted
+ * at the end of this method).
+ */
+ b.write(1);
+ b.write(0);
+ b.write(0);
+ b.write(0);
+
+ /*
+ * The maximum version that we intend to support.
+ */
+ b.write(version >>> 8);
+ b.write(version);
+
+ /*
+ * The client random has length 32 bytes, but begins with
+ * the client's notion of the current time, over 32 bits
+ * (seconds since 1970/01/01 00:00:00 UTC, not counting
+ * leap seconds).
+ */
+ byte[] rand = new byte[32];
+ RNG.nextBytes(rand);
+ enc32be((int) (System.currentTimeMillis() / 1000), rand, 0);
+ b.write(rand);
+
+ /*
+ * We send an empty session ID.
+ */
+ b.write(0);
+
+ /*
+ * The list of cipher suites (list of 16-bit values; the
+ * list length in bytes is written first).
+ */
+ int num = cipherSuites.size();
+ byte[] cs = new byte[2 + num * 2];
+ enc16be(num * 2, cs, 0);
+ int j = 2;
+ for (int s : cipherSuites) {
+ enc16be(s, cs, j);
+ j += 2;
+ }
+ b.write(cs);
+
+ /*
+ * Compression methods: we claim to support Deflate (1)
+ * and the standard no-compression (0), with Deflate
+ * being preferred.
+ */
+ b.write(2);
+ b.write(1);
+ b.write(0);
+
+ /*
+ * If we had extensions to add, they would go here.
+ */
+
+ /*
+ * We now get the message as a blob. The message length
+ * must be adjusted in the header.
+ */
+ byte[] msg = b.toByteArray();
+ enc24be(msg.length - 4, msg, 1);
+ return msg;
+ }
+
+ /*
+ * Compute the SHA-1 hash of some bytes, returning the hash
+ * value in hexadecimal.
+ */
+ static String doSHA1(byte[] buf) {
+ return doSHA1(buf, 0, buf.length);
+ }
+
+ static String doSHA1(byte[] buf, int off, int len) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+ md.update(buf, off, len);
+ byte[] hv = md.digest();
+ Formatter f = new Formatter();
+ for (byte b : hv) {
+ f.format("%02x", b & 0xFF);
+ }
+ return f.toString();
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new Error(nsae);
+ }
+ }
+
+ /*
+ * This class decodes a ServerHello message from the server. The
+ * fields we are interested in are stored in the
+ * package-accessible fields.
+ */
+ static class ServerHello {
+
+ int recordVersion;
+ int protoVersion;
+ long serverTime;
+ int cipherSuite;
+ int compression;
+ String serverCertName;
+ String serverCertHash;
+
+ ServerHello(InputStream in)
+ throws IOException {
+ InputRecord rec = new InputRecord(in);
+ rec.setExpectedType(HANDSHAKE);
+
+ /*
+ * First, get the handshake message header (4 bytes).
+ * First byte should be 2 ("ServerHello"), then
+ * comes the message size (over 3 bytes).
+ */
+ byte[] buf = new byte[4];
+ readFully(rec, buf);
+ recordVersion = rec.getVersion();
+ if (buf[0] != 2) {
+ throw new IOException("unexpected handshake"
+ + " message type: " + (buf[0] & 0xFF));
+ }
+ buf = new byte[dec24be(buf, 1)];
+
+ /*
+ * Read the complete message in RAM.
+ */
+ readFully(rec, buf);
+ int ptr = 0;
+
+ /*
+ * The protocol version which we will use.
+ */
+ if (ptr + 2 > buf.length) {
+ throw new IOException("invalid ServerHello");
+ }
+ protoVersion = dec16be(buf, 0);
+ ptr += 2;
+
+ /*
+ * The server random begins with the server's notion
+ * of the current time.
+ */
+ if (ptr + 32 > buf.length) {
+ throw new IOException("invalid ServerHello");
+ }
+ serverTime = 1000L * (dec32be(buf, ptr) & 0xFFFFFFFFL);
+ ptr += 32;
+
+ /*
+ * We skip the session ID.
+ */
+ if (ptr + 1 > buf.length) {
+ throw new IOException("invalid ServerHello");
+ }
+ ptr += 1 + (buf[ptr] & 0xFF);
+
+ /*
+ * The cipher suite and compression follow.
+ */
+ if (ptr + 3 > buf.length) {
+ throw new IOException("invalid ServerHello");
+ }
+ cipherSuite = dec16be(buf, ptr);
+ compression = buf[ptr + 2] & 0xFF;
+
+ /*
+ * The ServerHello could include some extensions
+ * here, which we ignore.
+ */
+
+ /*
+ * We now read a few extra messages, until we
+ * reach the server's Certificate message, or
+ * ServerHelloDone.
+ */
+ for (;;) {
+ buf = new byte[4];
+ readFully(rec, buf);
+ int mt = buf[0] & 0xFF;
+ buf = new byte[dec24be(buf, 1)];
+ readFully(rec, buf);
+ switch (mt) {
+ case 11:
+ processCertificate(buf);
+ return;
+ case 14:
+ // ServerHelloDone
+ return;
+ }
+ }
+ }
+
+ private void processCertificate(byte[] buf) {
+ if (buf.length <= 6) {
+ return;
+ }
+ int len1 = dec24be(buf, 0);
+ if (len1 != buf.length - 3) {
+ return;
+ }
+ int len2 = dec24be(buf, 3);
+ if (len2 > buf.length - 6) {
+ return;
+ }
+ byte[] ec = new byte[len2];
+ System.arraycopy(buf, 6, ec, 0, len2);
+ try {
+ CertificateFactory cf =
+ CertificateFactory.getInstance("X.509");
+ X509Certificate xc =
+ (X509Certificate) cf.generateCertificate(
+ new ByteArrayInputStream(ec));
+ serverCertName =
+ xc.getSubjectX500Principal().toString();
+ serverCertHash = doSHA1(ec);
+ } catch (CertificateException e) {
+ // ignored
+ return;
+ }
+ }
+ }
+
+ /*
+ * A constant SSLv2 CLIENT-HELLO message. Only one connection
+ * is needed for SSLv2, since the server response will contain
+ * _all_ the cipher suites that the server is willing to
+ * support.
+ *
+ * Note: when (mis)interpreted as a SSLv3+ record, this message
+ * apparently encodes some data of (invalid) 0x80 type, using
+ * protocol version TLS 44.1, and record length of 2 bytes.
+ * Thus, the receiving part will quickly conclude that it will
+ * not support that, instead of stalling for more data from the
+ * client.
+ */
+ private static final byte[] SSL2_CLIENT_HELLO = {
+ (byte) 0x80, (byte) 0x2E, // header (record length)
+ (byte) 0x01, // message type (CLIENT HELLO)
+ (byte) 0x00, (byte) 0x02, // version (0x0002)
+ (byte) 0x00, (byte) 0x15, // cipher specs list length
+ (byte) 0x00, (byte) 0x00, // session ID length
+ (byte) 0x00, (byte) 0x10, // challenge length
+ 0x01, 0x00, (byte) 0x80, // SSL_CK_RC4_128_WITH_MD5
+ 0x02, 0x00, (byte) 0x80, // SSL_CK_RC4_128_EXPORT40_WITH_MD5
+ 0x03, 0x00, (byte) 0x80, // SSL_CK_RC2_128_CBC_WITH_MD5
+ 0x04, 0x00, (byte) 0x80, // SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5
+ 0x05, 0x00, (byte) 0x80, // SSL_CK_IDEA_128_CBC_WITH_MD5
+ 0x06, 0x00, (byte) 0x40, // SSL_CK_DES_64_CBC_WITH_MD5
+ 0x07, 0x00, (byte) 0xC0, // SSL_CK_DES_192_EDE3_CBC_WITH_MD5
+ 0x54, 0x54, 0x54, 0x54, // challenge data (16 bytes)
+ 0x54, 0x54, 0x54, 0x54,
+ 0x54, 0x54, 0x54, 0x54,
+ 0x54, 0x54, 0x54, 0x54
+ };
+
+ /*
+ * This class represents the response of a server which knows
+ * $ SSLv2. It includes the list of cipher suites, and the
+ * identification of the server certificate.
+ */
+ static class ServerHelloSSLv2 {
+
+ int[] cipherSuites;
+ String serverCertName;
+ String serverCertHash;
+
+ ServerHelloSSLv2(InputStream in)
+ throws IOException {
+ // Record length
+ byte[] buf = new byte[2];
+ readFully(in, buf);
+ int len = dec16be(buf, 0);
+ if ((len & 0x8000) == 0) {
+ throw new IOException("not a SSLv2 record");
+ }
+ len &= 0x7FFF;
+ if (len < 11) {
+ throw new IOException(
+ "not a SSLv2 server hello");
+ }
+ buf = new byte[11];
+ readFully(in, buf);
+ if (buf[0] != 0x04) {
+ throw new IOException(
+ "not a SSLv2 server hello");
+ }
+ int certLen = dec16be(buf, 5);
+ int csLen = dec16be(buf, 7);
+ int connIdLen = dec16be(buf, 9);
+ if (len != 11 + certLen + csLen + connIdLen) {
+ throw new IOException(
+ "not a SSLv2 server hello");
+ }
+ if (csLen == 0 || csLen % 3 != 0) {
+ throw new IOException(
+ "not a SSLv2 server hello");
+ }
+ byte[] cert = new byte[certLen];
+ readFully(in, cert);
+ byte[] cs = new byte[csLen];
+ readFully(in, cs);
+ byte[] connId = new byte[connIdLen];
+ readFully(in, connId);
+ cipherSuites = new int[csLen / 3];
+ for (int i = 0, j = 0; i < csLen; i += 3, j++) {
+ cipherSuites[j] = dec24be(cs, i);
+ }
+ try {
+ CertificateFactory cf =
+ CertificateFactory.getInstance("X.509");
+ X509Certificate xc =
+ (X509Certificate) cf.generateCertificate(
+ new ByteArrayInputStream(cert));
+ serverCertName =
+ xc.getSubjectX500Principal().toString();
+ serverCertHash = doSHA1(cert);
+ } catch (CertificateException e) {
+ // ignored
+ }
+ }
+ }
+
+ static Map<Integer, CipherSuite> CIPHER_SUITES =
+ new TreeMap<Integer, CipherSuite>();
+
+ static class CipherSuite {
+
+ int suite;
+ String name;
+ boolean isCBC;
+ int strength;
+ }
+
+ static final int CLEAR = 0; // no encryption
+ static final int WEAK = 1; // weak encryption: 40-bit key
+ static final int MEDIUM = 2; // medium encryption: 56-bit key
+ static final int STRONG = 3; // strong encryption
+
+ static final String strengthString(int strength) {
+ switch (strength) {
+ case CLEAR:
+ return "no encryption";
+ case WEAK:
+ return "weak encryption (40-bit)";
+ case MEDIUM:
+ return "medium encryption (56-bit)";
+ case STRONG:
+ return "strong encryption (96-bit or more)";
+ default:
+ throw new Error("strange strength: " + strength);
+ }
+ }
+
+ static final String cipherSuiteString(int suite) {
+ CipherSuite cs = CIPHER_SUITES.get(suite);
+ if (cs == null) {
+ return String.format("UNKNOWN_SUITE:0x%04X", cs);
+ } else {
+ return cs.name;
+ }
+ }
+
+ static final String cipherSuiteStringV2(int suite) {
+ CipherSuite cs = CIPHER_SUITES.get(suite);
+ if (cs == null) {
+ return String.format("UNKNOWN_SUITE:%02X,%02X,%02X",
+ suite >> 16, (suite >> 8) & 0xFF, suite & 0XFF);
+ } else {
+ return cs.name;
+ }
+ }
+
+ private static final void makeCS(int suite, String name,
+ boolean isCBC, int strength) {
+ CipherSuite cs = new CipherSuite();
+ cs.suite = suite;
+ cs.name = name;
+ cs.isCBC = isCBC;
+ cs.strength = strength;
+ CIPHER_SUITES.put(suite, cs);
+
+ /*
+ * Consistency test: the strength and CBC status can normally
+ * be inferred from the name itself.
+ */
+ boolean inferredCBC = name.contains("_CBC_");
+ int inferredStrength;
+ if (name.contains("_NULL_")) {
+ inferredStrength = CLEAR;
+ } else if (name.contains("DES40") || name.contains("_40_")
+ || name.contains("EXPORT40")) {
+ inferredStrength = WEAK;
+ } else if ((name.contains("_DES_") || name.contains("DES_64"))
+ && !name.contains("DES_192")) {
+ inferredStrength = MEDIUM;
+ } else {
+ inferredStrength = STRONG;
+ }
+ if (inferredStrength != strength || inferredCBC != isCBC) {
+ throw new RuntimeException(
+ "wrong classification: " + name);
+ }
+ }
+
+ private static final void N(int suite, String name) {
+ makeCS(suite, name, false, CLEAR);
+ }
+
+ private static final void S4(int suite, String name) {
+ makeCS(suite, name, false, WEAK);
+ }
+
+ private static final void S8(int suite, String name) {
+ makeCS(suite, name, false, STRONG);
+ }
+
+ private static final void B4(int suite, String name) {
+ makeCS(suite, name, true, WEAK);
+ }
+
+ private static final void B5(int suite, String name) {
+ makeCS(suite, name, true, MEDIUM);
+ }
+
+ private static final void B8(int suite, String name) {
+ makeCS(suite, name, true, STRONG);
+ }
+
+ static {
+ /*
+ * SSLv2 cipher suites.
+ */
+ S8(0x010080, "RC4_128_WITH_MD5");
+ S4(0x020080, "RC4_128_EXPORT40_WITH_MD5");
+ B8(0x030080, "RC2_128_CBC_WITH_MD5");
+ B4(0x040080, "RC2_128_CBC_EXPORT40_WITH_MD5");
+ B8(0x050080, "IDEA_128_CBC_WITH_MD5");
+ B5(0x060040, "DES_64_CBC_WITH_MD5");
+ B8(0x0700C0, "DES_192_EDE3_CBC_WITH_MD5");
+
+ /*
+ * Original suites (SSLv3, TLS 1.0).
+ */
+ N(0x0000, "NULL_WITH_NULL_NULL");
+ N(0x0001, "RSA_WITH_NULL_MD5");
+ N(0x0002, "RSA_WITH_NULL_SHA");
+ S4(0x0003, "RSA_EXPORT_WITH_RC4_40_MD5");
+ S8(0x0004, "RSA_WITH_RC4_128_MD5");
+ S8(0x0005, "RSA_WITH_RC4_128_SHA");
+ B4(0x0006, "RSA_EXPORT_WITH_RC2_CBC_40_MD5");
+ B8(0x0007, "RSA_WITH_IDEA_CBC_SHA");
+ B4(0x0008, "RSA_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x0009, "RSA_WITH_DES_CBC_SHA");
+ B8(0x000A, "RSA_WITH_3DES_EDE_CBC_SHA");
+ B4(0x000B, "DH_DSS_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x000C, "DH_DSS_WITH_DES_CBC_SHA");
+ B8(0x000D, "DH_DSS_WITH_3DES_EDE_CBC_SHA");
+ B4(0x000E, "DH_RSA_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x000F, "DH_RSA_WITH_DES_CBC_SHA");
+ B8(0x0010, "DH_RSA_WITH_3DES_EDE_CBC_SHA");
+ B4(0x0011, "DHE_DSS_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x0012, "DHE_DSS_WITH_DES_CBC_SHA");
+ B8(0x0013, "DHE_DSS_WITH_3DES_EDE_CBC_SHA");
+ B4(0x0014, "DHE_RSA_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x0015, "DHE_RSA_WITH_DES_CBC_SHA");
+ B8(0x0016, "DHE_RSA_WITH_3DES_EDE_CBC_SHA");
+ S4(0x0017, "DH_anon_EXPORT_WITH_RC4_40_MD5");
+ S8(0x0018, "DH_anon_WITH_RC4_128_MD5");
+ B4(0x0019, "DH_anon_EXPORT_WITH_DES40_CBC_SHA");
+ B5(0x001A, "DH_anon_WITH_DES_CBC_SHA");
+ B8(0x001B, "DH_anon_WITH_3DES_EDE_CBC_SHA");
+
+ /*
+ * FORTEZZA suites (SSLv3 only; see RFC 6101).
+ */
+ N(0x001C, "FORTEZZA_KEA_WITH_NULL_SHA");
+ B8(0x001D, "FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA");
+
+ /*
+ * This one is deactivated since it conflicts with
+ * one of the Kerberos cipher suites.
+ * S8(0x001E, "FORTEZZA_KEA_WITH_RC4_128_SHA" );
+ */
+
+ /*
+ * Kerberos cipher suites (RFC 2712).
+ */
+ B5(0x001E, "KRB5_WITH_DES_CBC_SHA");
+ B8(0x001F, "KRB5_WITH_3DES_EDE_CBC_SHA");
+ S8(0x0020, "KRB5_WITH_RC4_128_SHA");
+ B8(0x0021, "KRB5_WITH_IDEA_CBC_SHA");
+ B5(0x0022, "KRB5_WITH_DES_CBC_MD5");
+ B8(0x0023, "KRB5_WITH_3DES_EDE_CBC_MD5");
+ S8(0x0024, "KRB5_WITH_RC4_128_MD5");
+ B8(0x0025, "KRB5_WITH_IDEA_CBC_MD5");
+ B4(0x0026, "KRB5_EXPORT_WITH_DES_CBC_40_SHA");
+ B4(0x0027, "KRB5_EXPORT_WITH_RC2_CBC_40_SHA");
+ S4(0x0028, "KRB5_EXPORT_WITH_RC4_40_SHA");
+ B4(0x0029, "KRB5_EXPORT_WITH_DES_CBC_40_MD5");
+ B4(0x002A, "KRB5_EXPORT_WITH_RC2_CBC_40_MD5");
+ S4(0x002B, "KRB5_EXPORT_WITH_RC4_40_MD5");
+
+ /*
+ * Pre-shared key, no encryption cipher suites (RFC 4785).
+ */
+ N(0x002C, "PSK_WITH_NULL_SHA");
+ N(0x002D, "DHE_PSK_WITH_NULL_SHA");
+ N(0x002E, "RSA_PSK_WITH_NULL_SHA");
+
+ /*
+ * AES-based suites (TLS 1.1).
+ */
+ B8(0x002F, "RSA_WITH_AES_128_CBC_SHA");
+ B8(0x0030, "DH_DSS_WITH_AES_128_CBC_SHA");
+ B8(0x0031, "DH_RSA_WITH_AES_128_CBC_SHA");
+ B8(0x0032, "DHE_DSS_WITH_AES_128_CBC_SHA");
+ B8(0x0033, "DHE_RSA_WITH_AES_128_CBC_SHA");
+ B8(0x0034, "DH_anon_WITH_AES_128_CBC_SHA");
+ B8(0x0035, "RSA_WITH_AES_256_CBC_SHA");
+ B8(0x0036, "DH_DSS_WITH_AES_256_CBC_SHA");
+ B8(0x0037, "DH_RSA_WITH_AES_256_CBC_SHA");
+ B8(0x0038, "DHE_DSS_WITH_AES_256_CBC_SHA");
+ B8(0x0039, "DHE_RSA_WITH_AES_256_CBC_SHA");
+ B8(0x003A, "DH_anon_WITH_AES_256_CBC_SHA");
+
+ /*
+ * Suites with SHA-256 (TLS 1.2).
+ */
+ N(0x003B, "RSA_WITH_NULL_SHA256");
+ B8(0x003C, "RSA_WITH_AES_128_CBC_SHA256");
+ B8(0x003D, "RSA_WITH_AES_256_CBC_SHA256");
+ B8(0x003E, "DH_DSS_WITH_AES_128_CBC_SHA256");
+ B8(0x003F, "DH_RSA_WITH_AES_128_CBC_SHA256");
+ B8(0x0040, "DHE_DSS_WITH_AES_128_CBC_SHA256");
+ B8(0x0067, "DHE_RSA_WITH_AES_128_CBC_SHA256");
+ B8(0x0068, "DH_DSS_WITH_AES_256_CBC_SHA256");
+ B8(0x0069, "DH_RSA_WITH_AES_256_CBC_SHA256");
+ B8(0x006A, "DHE_DSS_WITH_AES_256_CBC_SHA256");
+ B8(0x006B, "DHE_RSA_WITH_AES_256_CBC_SHA256");
+ B8(0x006C, "DH_anon_WITH_AES_128_CBC_SHA256");
+ B8(0x006D, "DH_anon_WITH_AES_256_CBC_SHA256");
+
+ /*
+ * Camellia cipher suites (RFC 5932).
+ */
+ B8(0x0041, "RSA_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0042, "DH_DSS_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0043, "DH_RSA_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0044, "DHE_DSS_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0045, "DHE_RSA_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0046, "DH_anon_WITH_CAMELLIA_128_CBC_SHA");
+ B8(0x0084, "RSA_WITH_CAMELLIA_256_CBC_SHA");
+ B8(0x0085, "DH_DSS_WITH_CAMELLIA_256_CBC_SHA");
+ B8(0x0086, "DH_RSA_WITH_CAMELLIA_256_CBC_SHA");
+ B8(0x0087, "DHE_DSS_WITH_CAMELLIA_256_CBC_SHA");
+ B8(0x0088, "DHE_RSA_WITH_CAMELLIA_256_CBC_SHA");
+ B8(0x0089, "DH_anon_WITH_CAMELLIA_256_CBC_SHA");
+
+ /*
+ * Unsorted (yet), from the IANA TLS registry:
+ * http://www.iana.org/assignments/tls-parameters/
+ */
+ S8(0x008A, "TLS_PSK_WITH_RC4_128_SHA");
+ B8(0x008B, "TLS_PSK_WITH_3DES_EDE_CBC_SHA");
+ B8(0x008C, "TLS_PSK_WITH_AES_128_CBC_SHA");
+ B8(0x008D, "TLS_PSK_WITH_AES_256_CBC_SHA");
+ S8(0x008E, "TLS_DHE_PSK_WITH_RC4_128_SHA");
+ B8(0x008F, "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA");
+ B8(0x0090, "TLS_DHE_PSK_WITH_AES_128_CBC_SHA");
+ B8(0x0091, "TLS_DHE_PSK_WITH_AES_256_CBC_SHA");
+ S8(0x0092, "TLS_RSA_PSK_WITH_RC4_128_SHA");
+ B8(0x0093, "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA");
+ B8(0x0094, "TLS_RSA_PSK_WITH_AES_128_CBC_SHA");
+ B8(0x0095, "TLS_RSA_PSK_WITH_AES_256_CBC_SHA");
+ B8(0x0096, "TLS_RSA_WITH_SEED_CBC_SHA");
+ B8(0x0097, "TLS_DH_DSS_WITH_SEED_CBC_SHA");
+ B8(0x0098, "TLS_DH_RSA_WITH_SEED_CBC_SHA");
+ B8(0x0099, "TLS_DHE_DSS_WITH_SEED_CBC_SHA");
+ B8(0x009A, "TLS_DHE_RSA_WITH_SEED_CBC_SHA");
+ B8(0x009B, "TLS_DH_anon_WITH_SEED_CBC_SHA");
+ S8(0x009C, "TLS_RSA_WITH_AES_128_GCM_SHA256");
+ S8(0x009D, "TLS_RSA_WITH_AES_256_GCM_SHA384");
+ S8(0x009E, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256");
+ S8(0x009F, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384");
+ S8(0x00A0, "TLS_DH_RSA_WITH_AES_128_GCM_SHA256");
+ S8(0x00A1, "TLS_DH_RSA_WITH_AES_256_GCM_SHA384");
+ S8(0x00A2, "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256");
+ S8(0x00A3, "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384");
+ S8(0x00A4, "TLS_DH_DSS_WITH_AES_128_GCM_SHA256");
+ S8(0x00A5, "TLS_DH_DSS_WITH_AES_256_GCM_SHA384");
+ S8(0x00A6, "TLS_DH_anon_WITH_AES_128_GCM_SHA256");
+ S8(0x00A7, "TLS_DH_anon_WITH_AES_256_GCM_SHA384");
+ S8(0x00A8, "TLS_PSK_WITH_AES_128_GCM_SHA256");
+ S8(0x00A9, "TLS_PSK_WITH_AES_256_GCM_SHA384");
+ S8(0x00AA, "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256");
+ S8(0x00AB, "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384");
+ S8(0x00AC, "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256");
+ S8(0x00AD, "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384");
+ B8(0x00AE, "TLS_PSK_WITH_AES_128_CBC_SHA256");
+ B8(0x00AF, "TLS_PSK_WITH_AES_256_CBC_SHA384");
+ N(0x00B0, "TLS_PSK_WITH_NULL_SHA256");
+ N(0x00B1, "TLS_PSK_WITH_NULL_SHA384");
+ B8(0x00B2, "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256");
+ B8(0x00B3, "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384");
+ N(0x00B4, "TLS_DHE_PSK_WITH_NULL_SHA256");
+ N(0x00B5, "TLS_DHE_PSK_WITH_NULL_SHA384");
+ B8(0x00B6, "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256");
+ B8(0x00B7, "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384");
+ N(0x00B8, "TLS_RSA_PSK_WITH_NULL_SHA256");
+ N(0x00B9, "TLS_RSA_PSK_WITH_NULL_SHA384");
+ B8(0x00BA, "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00BB, "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00BC, "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00BD, "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00BE, "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00BF, "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0x00C0, "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256");
+ B8(0x00C1, "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256");
+ B8(0x00C2, "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256");
+ B8(0x00C3, "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256");
+ B8(0x00C4, "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256");
+ B8(0x00C5, "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256");
+ /*
+ * This one is a fake cipher suite which marks a
+ * renegotiation.
+ * N(0x00FF, "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" );
+ */
+ N(0xC001, "TLS_ECDH_ECDSA_WITH_NULL_SHA");
+ S8(0xC002, "TLS_ECDH_ECDSA_WITH_RC4_128_SHA");
+ B8(0xC003, "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC004, "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA");
+ B8(0xC005, "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA");
+ N(0xC006, "TLS_ECDHE_ECDSA_WITH_NULL_SHA");
+ S8(0xC007, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA");
+ B8(0xC008, "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC009, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA");
+ B8(0xC00A, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA");
+ N(0xC00B, "TLS_ECDH_RSA_WITH_NULL_SHA");
+ S8(0xC00C, "TLS_ECDH_RSA_WITH_RC4_128_SHA");
+ B8(0xC00D, "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC00E, "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA");
+ B8(0xC00F, "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA");
+ N(0xC010, "TLS_ECDHE_RSA_WITH_NULL_SHA");
+ S8(0xC011, "TLS_ECDHE_RSA_WITH_RC4_128_SHA");
+ B8(0xC012, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC013, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
+ B8(0xC014, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA");
+ N(0xC015, "TLS_ECDH_anon_WITH_NULL_SHA");
+ S8(0xC016, "TLS_ECDH_anon_WITH_RC4_128_SHA");
+ B8(0xC017, "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC018, "TLS_ECDH_anon_WITH_AES_128_CBC_SHA");
+ B8(0xC019, "TLS_ECDH_anon_WITH_AES_256_CBC_SHA");
+ B8(0xC01A, "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC01B, "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC01C, "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC01D, "TLS_SRP_SHA_WITH_AES_128_CBC_SHA");
+ B8(0xC01E, "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA");
+ B8(0xC01F, "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA");
+ B8(0xC020, "TLS_SRP_SHA_WITH_AES_256_CBC_SHA");
+ B8(0xC021, "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA");
+ B8(0xC022, "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA");
+ B8(0xC023, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256");
+ B8(0xC024, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384");
+ B8(0xC025, "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256");
+ B8(0xC026, "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384");
+ B8(0xC027, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256");
+ B8(0xC028, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384");
+ B8(0xC029, "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256");
+ B8(0xC02A, "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384");
+ S8(0xC02B, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
+ S8(0xC02C, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
+ S8(0xC02D, "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256");
+ S8(0xC02E, "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384");
+ S8(0xC02F, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
+ S8(0xC030, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
+ S8(0xC031, "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256");
+ S8(0xC032, "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384");
+ S8(0xC033, "TLS_ECDHE_PSK_WITH_RC4_128_SHA");
+ B8(0xC034, "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA");
+ B8(0xC035, "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA");
+ B8(0xC036, "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA");
+ B8(0xC037, "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256");
+ B8(0xC038, "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384");
+ N(0xC039, "TLS_ECDHE_PSK_WITH_NULL_SHA");
+ N(0xC03A, "TLS_ECDHE_PSK_WITH_NULL_SHA256");
+ N(0xC03B, "TLS_ECDHE_PSK_WITH_NULL_SHA384");
+ B8(0xC03C, "TLS_RSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC03D, "TLS_RSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC03E, "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC03F, "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC040, "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC041, "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC042, "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC043, "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC044, "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC045, "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC046, "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC047, "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC048, "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC049, "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC04A, "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC04B, "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC04C, "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC04D, "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC04E, "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC04F, "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384");
+ S8(0xC050, "TLS_RSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC051, "TLS_RSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC052, "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC053, "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC054, "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC055, "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC056, "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC057, "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC058, "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC059, "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC05A, "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC05B, "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC05C, "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC05D, "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC05E, "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC05F, "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC060, "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC061, "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC062, "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC063, "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384");
+ B8(0xC064, "TLS_PSK_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC065, "TLS_PSK_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC066, "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC067, "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC068, "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC069, "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384");
+ S8(0xC06A, "TLS_PSK_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC06B, "TLS_PSK_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC06C, "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC06D, "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384");
+ S8(0xC06E, "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256");
+ S8(0xC06F, "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384");
+ B8(0xC070, "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256");
+ B8(0xC071, "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384");
+ B8(0xC072, "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC073, "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC074, "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC075, "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC076, "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC077, "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC078, "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC079, "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384");
+ S8(0xC07A, "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC07B, "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC07C, "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC07D, "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC07E, "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC07F, "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC080, "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC081, "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC082, "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC083, "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC084, "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC085, "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC086, "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC087, "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC088, "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC089, "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC08A, "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC08B, "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC08C, "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC08D, "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC08E, "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC08F, "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC090, "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC091, "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384");
+ S8(0xC092, "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256");
+ S8(0xC093, "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384");
+ B8(0xC094, "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC095, "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC096, "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC097, "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC098, "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC099, "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384");
+ B8(0xC09A, "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256");
+ B8(0xC09B, "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384");
+ S8(0xC09C, "TLS_RSA_WITH_AES_128_CCM");
+ S8(0xC09D, "TLS_RSA_WITH_AES_256_CCM");
+ S8(0xC09E, "TLS_DHE_RSA_WITH_AES_128_CCM");
+ S8(0xC09F, "TLS_DHE_RSA_WITH_AES_256_CCM");
+ S8(0xC0A0, "TLS_RSA_WITH_AES_128_CCM_8");
+ S8(0xC0A1, "TLS_RSA_WITH_AES_256_CCM_8");
+ S8(0xC0A2, "TLS_DHE_RSA_WITH_AES_128_CCM_8");
+ S8(0xC0A3, "TLS_DHE_RSA_WITH_AES_256_CCM_8");
+ S8(0xC0A4, "TLS_PSK_WITH_AES_128_CCM");
+ S8(0xC0A5, "TLS_PSK_WITH_AES_256_CCM");
+ S8(0xC0A6, "TLS_DHE_PSK_WITH_AES_128_CCM");
+ S8(0xC0A7, "TLS_DHE_PSK_WITH_AES_256_CCM");
+ S8(0xC0A8, "TLS_PSK_WITH_AES_128_CCM_8");
+ S8(0xC0A9, "TLS_PSK_WITH_AES_256_CCM_8");
+ S8(0xC0AA, "TLS_PSK_DHE_WITH_AES_128_CCM_8");
+ S8(0xC0AB, "TLS_PSK_DHE_WITH_AES_256_CCM_8");
+ }
+}
diff --git a/geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java b/geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
new file mode 100644
index 0000000..95d6391
--- /dev/null
+++ b/geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.redis;
+
+import static org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.handler.ssl.NotSslRecordException;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.internal.net.filewatch.PollingFileWatcher;
+import org.apache.geode.redis.internal.ssl.TestSSLServer;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class SSLDUnitTest {
+
+ @Rule
+ public RedisClusterStartupRule cluster = new RedisClusterStartupRule();
+
+ private static final String commonPassword = "password";
+ private static final SANCapturingHostnameVerifier hostnameVerifier =
+ new SANCapturingHostnameVerifier();
+
+ private int redisPort;
+ private CertificateMaterial ca;
+ private String serverKeyStoreFilename;
+ private String serverTrustStoreFilename;
+
+ private void setup() throws Exception {
+ setup(p -> {
+ });
+ }
+
+ private void setup(Consumer<Properties> propertyConfigurer) throws Exception {
+ ca = new CertificateBuilder()
+ .commonName("Test CA")
+ .isCA()
+ .generate();
+
+ CertificateMaterial serverCertificate = new CertificateBuilder()
+ .commonName("server")
+ .issuedBy(ca)
+ .generate();
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCertificate);
+ serverStore.trust("ca", ca);
+ Properties serverProperties = serverStore.propertiesWith("all", true, false);
+
+ propertyConfigurer.accept(serverProperties);
+
+ MemberVM server = cluster.startRedisVM(0, x -> x.withProperties(serverProperties));
+
+ redisPort = cluster.getRedisPort(server);
+ serverKeyStoreFilename = serverProperties.getProperty("ssl-keystore");
+ serverTrustStoreFilename = serverProperties.getProperty("ssl-truststore");
+ }
+
+ @Test
+ public void givenMutualAuthentication_clientCanConnect() throws Exception {
+ setup();
+
+ try (Jedis jedis = createClient(true, false)) {
+ assertThat(jedis.ping()).isEqualTo("PONG");
+ }
+ }
+
+ @Test
+ public void givenServerHasCipherAndProtocol_clientCanConnect() throws Exception {
+ SSLContext ssl = SSLContext.getInstance("TLSv1.2");
+ ssl.init(null, null, new java.security.SecureRandom());
+ String[] cipherSuites = ssl.getServerSocketFactory().getSupportedCipherSuites();
+ String rsaCipher = Arrays.stream(cipherSuites).filter(c -> c.contains("RSA")).findFirst().get();
+
+ setup(p -> {
+ p.setProperty("ssl-protocols", "TLSv1.2");
+ p.setProperty("ssl-ciphers", rsaCipher);
+ });
+
+ try (Jedis jedis = createClient(true, false)) {
+ assertThat(jedis.ping()).isEqualTo("PONG");
+ }
+
+ IgnoredException.addIgnoredException(SSLHandshakeException.class);
+ IgnoredException.addIgnoredException(NotSslRecordException.class);
+
+ TestSSLServer tServer = new TestSSLServer("localhost", redisPort).execute();
+
+ assertThat(tServer.getCipherSuiteNames()).containsExactly(rsaCipher);
+ }
+
+ @Test
+ public void givenMutualAuthentication_clientErrorsWithoutKeystore() throws Exception {
+ setup();
+
+ IgnoredException.addIgnoredException(SSLHandshakeException.class);
+ IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+ // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+ assertThatThrownBy(() -> {
+ Jedis jedis = createClient(false, false);
+ jedis.ping();
+ }).satisfiesAnyOf(
+ e -> assertThat(e).isInstanceOf(JedisConnectionException.class),
+ e -> assertThat(e.getMessage()).contains("SocketException"),
+ e -> assertThat(e.getMessage()).contains("SSLException"),
+ e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ @Test
+ public void givenMutualAuthentication_clientErrorsWithSelfSignedCert() throws Exception {
+ setup();
+
+ IgnoredException.addIgnoredException(SSLHandshakeException.class);
+ IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+ // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+ assertThatThrownBy(() -> {
+ Jedis jedis = createClient(true, true);
+ jedis.ping();
+ }).satisfiesAnyOf(
+ e -> assertThat(e).isInstanceOf(JedisConnectionException.class),
+ e -> assertThat(e.getMessage()).contains("SocketException"),
+ e -> assertThat(e.getMessage()).contains("SSLException"),
+ e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ @Test
+ public void givenSslEnabled_clientErrors_whenUsingCleartext() throws Exception {
+ setup();
+
+ IgnoredException.addIgnoredException(NotSslRecordException.class);
+
+ try (Jedis jedis = new Jedis(BIND_ADDRESS, redisPort)) {
+ assertThatThrownBy(jedis::ping)
+ .isInstanceOf(JedisConnectionException.class);
+ }
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ @Test
+ public void givenServerCertificateIsRotated_clientCanStillConnect() throws Exception {
+ setup();
+
+ String newServerName = "updated-server";
+
+ try (Jedis jedis = createClient(true, false)) {
+ assertThat(jedis.ping()).isEqualTo("PONG");
+ }
+
+ // create a new certificate for the server
+ CertificateMaterial serverCertificate = new CertificateBuilder()
+ .commonName(newServerName)
+ .issuedBy(ca)
+ .sanDnsName(newServerName)
+ .generate();
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCertificate);
+ serverStore.trust("ca", ca);
+
+ // Wait for one second since file timestamp granularity may only be seconds depending on the
+ // platform.
+ Thread.sleep(1000);
+ serverStore.createKeyStore(serverKeyStoreFilename, commonPassword);
+
+ // Try long enough for the file change to be detected
+ GeodeAwaitility.await().atMost(Duration.ofSeconds(PollingFileWatcher.PERIOD_SECONDS * 3))
+ .untilAsserted(() -> {
+ try (Jedis jedis = createClient(true, false)) {
+ jedis.ping();
+ assertThat(hostnameVerifier.getSubjectAltNames()).contains(newServerName);
+ }
+ });
+ }
+
+ @Test
+ public void givenServerCAandKeyIsRotated_clientCannotConnect() throws Exception {
+ setup();
+
+ try (Jedis jedis = createClient(true, false)) {
+ assertThat(jedis.ping()).isEqualTo("PONG");
+ }
+
+ CertificateMaterial newCA = new CertificateBuilder()
+ .commonName("New Test CA")
+ .isCA()
+ .generate();
+
+ CertificateMaterial serverCertificate = new CertificateBuilder()
+ .commonName("server")
+ .issuedBy(newCA)
+ .generate();
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCertificate);
+ serverStore.trust("ca", newCA);
+
+ // Wait for one second since file timestamp granularity may only be seconds depending on the
+ // platform.
+ Thread.sleep(1000);
+ serverStore.createKeyStore(serverKeyStoreFilename, commonPassword);
+ serverStore.createTrustStore(serverTrustStoreFilename, commonPassword);
+
+ IgnoredException.addIgnoredException(SSLHandshakeException.class);
+ IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+ // Try long enough for the file change to be detected
+ GeodeAwaitility.await().atMost(Duration.ofSeconds(PollingFileWatcher.PERIOD_SECONDS * 3))
+ .untilAsserted(() -> assertThatThrownBy(() -> createClient(true, false))
+ .isInstanceOf(JedisConnectionException.class));
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ private Jedis createClient(boolean mutualAuthentication, boolean isSelfSigned) throws Exception {
+ CertificateMaterial clientCertificate = new CertificateBuilder()
+ .commonName("redis-client")
+ .issuedBy(isSelfSigned ? null : ca)
+ .generate();
+
+ CertStores clientStore = CertStores.clientStore();
+ clientStore.withCertificate("redis-client", clientCertificate);
+ clientStore.trust("ca", ca);
+
+ Properties clientProperties = clientStore.propertiesWith("all");
+
+ KeyManager[] keyManagers = null;
+
+ if (mutualAuthentication) {
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(new FileInputStream(clientProperties.getProperty("ssl-keystore")),
+ commonPassword.toCharArray());
+
+ KeyManagerFactory keyManagerFactory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, commonPassword.toCharArray());
+ keyManagers = keyManagerFactory.getKeyManagers();
+ }
+
+ KeyStore trustStore = KeyStore.getInstance("JKS");
+ trustStore.load(new FileInputStream(clientProperties.getProperty("ssl-truststore")),
+ commonPassword.toCharArray());
+
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
+
+ return new Jedis(BIND_ADDRESS, redisPort, true, sslContext.getSocketFactory(),
+ sslContext.getSupportedSSLParameters(), hostnameVerifier);
+ }
+
+ /**
+ * {@link HostnameVerifier} that captures the Subject Alternative Names (SANs).
+ */
+ private static class SANCapturingHostnameVerifier implements HostnameVerifier {
+
+ private final List<String> subjectAltNames = new ArrayList<>();
+
+ @Override
+ public boolean verify(String s, SSLSession sslSession) {
+ subjectAltNames.clear();
+
+ try {
+ Certificate[] certs = sslSession.getPeerCertificates();
+ X509Certificate x509 = (X509Certificate) certs[0];
+ Collection<List<?>> entries = x509.getSubjectAlternativeNames();
+
+ for (List<?> entry : entries) {
+ if (entry.size() > 1) {
+ subjectAltNames.add((String) entry.get(1));
+ }
+ }
+ } catch (Exception ignored) {
+ }
+
+ return true;
+ }
+
+ public List<String> getSubjectAltNames() {
+ return subjectAltNames;
+ }
+ }
+
+}
diff --git a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/SSLTest.java b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/SSLTest.java
deleted file mode 100644
index 08f1182..0000000
--- a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/SSLTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License. You may obtain a
- * copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package org.apache.geode.redis;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.contrib.java.lang.system.RestoreSystemProperties;
-import org.junit.experimental.categories.Category;
-import redis.clients.jedis.Jedis;
-
-import org.apache.geode.distributed.internal.InternalDistributedSystem;
-import org.apache.geode.internal.net.SSLConfig;
-import org.apache.geode.internal.net.SSLConfigurationFactory;
-import org.apache.geode.internal.security.SecurableCommunicationChannel;
-import org.apache.geode.test.junit.categories.RedisTest;
-import org.apache.geode.test.junit.rules.ServerStarterRule;
-
-@Category({RedisTest.class})
-public class SSLTest {
-
- @Rule
- public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
-
- @ClassRule
- public static ServerStarterRule sslEnabledServer = new ServerStarterRule()
- .withSSL("server", true, true)
- .withProperty("compatible-with-redis-bind-address", "localhost")
- .withProperty("compatible-with-redis-port", "11211")
- .withProperty("compatible-with-redis-enabled", "true")
- .withAutoStart();
-
-
- @Test
- public void canConnectOverSSL() {
- SSLConfig sslConfigForComponent = SSLConfigurationFactory.getSSLConfigForComponent(
- ((InternalDistributedSystem) sslEnabledServer.getCache().getDistributedSystem())
- .getConfig(),
- SecurableCommunicationChannel.SERVER);
-
- System.setProperty("javax.net.ssl.trustStore", sslConfigForComponent.getTruststore());
- System.setProperty("javax.net.ssl.trustStoreType", "JKS");
-
- Jedis localhost = new Jedis("localhost", 11211, true);
-
- assertThat(localhost.ping()).isEqualTo("PONG");
- }
-
- @Test
- public void cannotConnectOverCleartext() {
- SSLConfig sslConfigForComponent = SSLConfigurationFactory.getSSLConfigForComponent(
- ((InternalDistributedSystem) sslEnabledServer.getCache().getDistributedSystem())
- .getConfig(),
- SecurableCommunicationChannel.ALL);
-
- System.setProperty("javax.net.ssl.trustStore", sslConfigForComponent.getTruststore());
- System.setProperty("javax.net.ssl.trustStoreType", "JKS");
-
- Jedis localhost = new Jedis("localhost", 11211, false);
-
- assertThatThrownBy(() -> localhost.ping());
- }
-
-}
diff --git a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java
index b68639e..664d644 100644
--- a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java
+++ b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java
@@ -18,20 +18,17 @@ package org.apache.geode.redis.internal.netty;
import static org.apache.geode.redis.internal.netty.Coder.stringToBytes;
-import java.io.FileInputStream;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
+import java.util.Arrays;
import java.util.concurrent.ThreadFactory;
import java.util.function.Supplier;
import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
@@ -44,8 +41,11 @@ import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.util.KeyManagerFactoryWrapper;
+import io.netty.handler.ssl.util.TrustManagerFactoryWrapper;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.util.concurrent.Future;
import org.apache.commons.lang3.StringUtils;
@@ -56,6 +56,8 @@ import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.internal.inet.LocalHostUtil;
import org.apache.geode.internal.net.SSLConfig;
import org.apache.geode.internal.net.SSLConfigurationFactory;
+import org.apache.geode.internal.net.filewatch.FileWatchingX509ExtendedKeyManager;
+import org.apache.geode.internal.net.filewatch.FileWatchingX509ExtendedTrustManager;
import org.apache.geode.internal.security.SecurableCommunicationChannel;
import org.apache.geode.logging.internal.executors.LoggingThreadFactory;
import org.apache.geode.logging.internal.log4j.api.LogService;
@@ -180,30 +182,48 @@ public class NettyRedisServer {
private void addSSLIfEnabled(SocketChannel ch, ChannelPipeline p) {
- SSLConfig sslConfigForComponent =
+ SSLConfig sslConfigForServer =
SSLConfigurationFactory.getSSLConfigForComponent(configSupplier.get(),
SecurableCommunicationChannel.SERVER);
- if (!sslConfigForComponent.isEnabled()) {
+ if (!sslConfigForServer.isEnabled()) {
return;
}
+ if (sslConfigForServer.getKeystore() == null) {
+ throw new IllegalStateException(
+ "Cannot start netty as no key manager is configured. Please ensure that the GemFire property 'ssl-keystore' is set.");
+ }
+
SslContext sslContext;
- try (FileInputStream fileInputStream =
- new FileInputStream(sslConfigForComponent.getKeystore())) {
- KeyStore ks = KeyStore.getInstance("JKS");
- ks.load(fileInputStream, sslConfigForComponent.getKeystorePassword().toCharArray());
- // Set up key manager factory to use our key store
- KeyManagerFactory kmf =
- KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(ks, sslConfigForComponent.getKeystorePassword().toCharArray());
-
- SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(kmf);
- sslContext = sslContextBuilder.build();
+ try {
+ KeyManagerFactory keyManagerFactory = new KeyManagerFactoryWrapper(
+ FileWatchingX509ExtendedKeyManager.newFileWatchingKeyManager(sslConfigForServer));
+
+ TrustManagerFactory trustManagerFactory = null;
+ if (sslConfigForServer.getTruststore() != null) {
+ trustManagerFactory = new TrustManagerFactoryWrapper(
+ FileWatchingX509ExtendedTrustManager.newFileWatchingTrustManager(sslConfigForServer));
+ }
+
+ SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(keyManagerFactory);
+ sslContextBuilder.trustManager(trustManagerFactory);
- } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException
- | CertificateException e) {
- throw new RuntimeException(e);
+ if (!sslConfigForServer.isAnyCiphers()) {
+ sslContextBuilder.ciphers(Arrays.asList(sslConfigForServer.getCiphersAsStringArray()));
+ }
+
+ if (!sslConfigForServer.isAnyProtocols()) {
+ sslContextBuilder.protocols(
+ Arrays.asList(sslConfigForServer.getProtocolsAsStringArray()));
+ }
+
+ if (sslConfigForServer.isRequireAuth()) {
+ sslContextBuilder.clientAuth(ClientAuth.REQUIRE);
+ }
+ sslContext = sslContextBuilder.build();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
}
p.addLast(sslContext.newHandler(ch.alloc()));
}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/net/SSLConfig.java b/geode-core/src/main/java/org/apache/geode/internal/net/SSLConfig.java
index a27c040..cc9056c 100755
--- a/geode-core/src/main/java/org/apache/geode/internal/net/SSLConfig.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/net/SSLConfig.java
@@ -174,6 +174,20 @@ public class SSLConfig {
return sslParameterExtension;
}
+ /**
+ * Returns true if ciphers is either null, empty or is set to "any" (ignoring case)
+ */
+ public boolean isAnyCiphers() {
+ return StringUtils.isBlank(ciphers) || "any".equalsIgnoreCase(ciphers);
+ }
+
+ /**
+ * Returns true if protocols is either null, empty or is set to "any" (ignoring case)
+ */
+ public boolean isAnyProtocols() {
+ return StringUtils.isBlank(protocols) || "any".equalsIgnoreCase(protocols);
+ }
+
@Override
public String toString() {
return "SSLConfig{" + "enabled=" + enabled + ", protocols='" + protocols + '\'' + ", ciphers='"
diff --git a/geode-core/src/main/java/org/apache/geode/internal/net/filewatch/PollingFileWatcher.java b/geode-core/src/main/java/org/apache/geode/internal/net/filewatch/PollingFileWatcher.java
index 5548ccd..5c135db 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/net/filewatch/PollingFileWatcher.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/net/filewatch/PollingFileWatcher.java
@@ -36,8 +36,8 @@ import org.apache.geode.logging.internal.log4j.api.LogService;
* and remote file systems.
* </p>
*/
-final class PollingFileWatcher {
- private static final long PERIOD_SECONDS = 10;
+public final class PollingFileWatcher {
+ public static final long PERIOD_SECONDS = 10;
private static final Logger logger = LogService.getLogger();
private final Path path;