Skip to content

Commit

Permalink
Use hostname and port from SocketAddress for peer verification #1209
Browse files Browse the repository at this point in the history
Lettuce now uses the InetSocketAddress.hostString to verify the SSL host and for SNI instead of using the RedisURI. When using Redis Sentinel, the URI host was null and the port was zero which caused failures during the SSL handshake.
  • Loading branch information
mp911de committed Jan 15, 2020
1 parent a22864e commit 8a1a22e
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 48 deletions.
23 changes: 5 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ cluster-start: work/cluster-node-7379.pid work/cluster-node-7380.pid work/cluste
work/stunnel.conf:
@mkdir -p $(@D)

@echo cert=$(ROOT_DIR)/work/ca/certs/foo-host.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/foo-host.decrypted.key.pem >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
@echo capath=$(ROOT_DIR)/work/ca/certs/ca.cert.pem >> $@
@echo cafile=$(ROOT_DIR)/work/ca/certs/ca.cert.pem >> $@
@echo delay=yes >> $@
Expand All @@ -242,13 +242,11 @@ work/stunnel.conf:
@echo accept = 127.0.0.1:6443 >> $@
@echo connect = 127.0.0.1:6479 >> $@

@echo [stunnel-2] >> $@
@echo [foo-host] >> $@
@echo accept = 127.0.0.1:6444 >> $@
@echo connect = 127.0.0.1:6479 >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
@echo capath=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo cafile=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/foo-host.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/foo-host.decrypted.key.pem >> $@

@echo [ssl-cluster-node-1] >> $@
@echo accept = 127.0.0.1:7443 >> $@
Expand Down Expand Up @@ -285,26 +283,15 @@ work/stunnel.conf:
@echo [stunnel-client-cert] >> $@
@echo accept = 127.0.0.1:6445 >> $@
@echo connect = 127.0.0.1:6479 >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
@echo cafile=$(ROOT_DIR)/work/ca/certs/ca.cert.pem >> $@
@echo verify=2 >> $@

@echo [stunnel-master-slave-node-1] >> $@
@echo accept = 127.0.0.1:8443 >> $@
@echo connect = 127.0.0.1:6482 >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
@echo capath=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo cafile=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@

@echo [stunnel-master-slave-node-2] >> $@
@echo accept = 127.0.0.1:8444 >> $@
@echo connect = 127.0.0.1:6483 >> $@
@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
@echo capath=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
@echo cafile=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@

work/stunnel.pid: work/stunnel.conf ssl-keys
which stunnel4 >/dev/null 2>&1 && stunnel4 $(ROOT_DIR)/work/stunnel.conf || stunnel $(ROOT_DIR)/work/stunnel.conf
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/lettuce/core/AbstractRedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ private void initializeChannelAsync0(ConnectionBuilder connectionBuilder, Comple

Bootstrap redisBootstrap = connectionBuilder.bootstrap();

ChannelInitializer<Channel> initializer = connectionBuilder.build();
ChannelInitializer<Channel> initializer = connectionBuilder.build(redisAddress);
redisBootstrap.handler(initializer);

clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/lettuce/core/ConnectionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ protected ConnectionWatchdog createConnectionWatchdog() {
return watchdog;
}

public ChannelInitializer<Channel> build() {
public ChannelInitializer<Channel> build(SocketAddress socketAddress) {
return new PlainChannelInitializer(this::buildHandlers, clientResources);
}

Expand Down
61 changes: 43 additions & 18 deletions src/main/java/io/lettuce/core/SslConnectionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@
package io.lettuce.core;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.function.Supplier;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;

import io.lettuce.core.internal.HostAndPort;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.resource.ClientResources;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
Expand Down Expand Up @@ -61,55 +65,76 @@ protected List<ChannelHandler> buildHandlers() {
}

@Override
public ChannelInitializer<Channel> build() {
return new SslChannelInitializer(this::buildHandlers, redisURI, clientResources(), clientOptions().getSslOptions());
public ChannelInitializer<Channel> build(SocketAddress socketAddress) {
return new SslChannelInitializer(this::buildHandlers, toHostAndPort(socketAddress), redisURI.isVerifyPeer(),
redisURI.isStartTls(), clientResources(), clientOptions().getSslOptions());
}

static HostAndPort toHostAndPort(SocketAddress socketAddress) {

if (socketAddress instanceof InetSocketAddress) {

InetSocketAddress isa = (InetSocketAddress) socketAddress;

return HostAndPort.of(isa.getHostString(), isa.getPort());
}

return null;
}

static class SslChannelInitializer extends io.netty.channel.ChannelInitializer<Channel> {

private final Supplier<List<ChannelHandler>> handlers;
private final RedisURI redisURI;
private final HostAndPort hostAndPort;
private final boolean verifyPeer;
private final boolean startTls;
private final ClientResources clientResources;
private final SslOptions sslOptions;

public SslChannelInitializer(Supplier<List<ChannelHandler>> handlers, RedisURI redisURI,
ClientResources clientResources, SslOptions sslOptions) {
public SslChannelInitializer(Supplier<List<ChannelHandler>> handlers, HostAndPort hostAndPort, boolean verifyPeer,
boolean startTls, ClientResources clientResources, SslOptions sslOptions) {

this.handlers = handlers;
this.redisURI = redisURI;
this.hostAndPort = hostAndPort;
this.verifyPeer = verifyPeer;
this.startTls = startTls;
this.clientResources = clientResources;
this.sslOptions = sslOptions;
}

@Override
protected void initChannel(Channel channel) throws Exception {
doInitialize(channel);

SSLEngine sslEngine = initializeSSLEngine(channel.alloc());
SslHandler sslHandler = new SslHandler(sslEngine, startTls);
channel.pipeline().addLast(sslHandler);

for (ChannelHandler handler : handlers.get()) {
channel.pipeline().addLast(handler);
}

clientResources.nettyCustomizer().afterChannelInitialized(channel);
}

private void doInitialize(Channel channel) throws IOException, GeneralSecurityException {
private SSLEngine initializeSSLEngine(ByteBufAllocator alloc) throws IOException, GeneralSecurityException {

SSLParameters sslParams = sslOptions.createSSLParameters();
SslContextBuilder sslContextBuilder = sslOptions.createSslContextBuilder();

if (redisURI.isVerifyPeer()) {
if (verifyPeer) {
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
} else {
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
}

SslContext sslContext = sslContextBuilder.build();

SSLEngine sslEngine = sslContext.newEngine(channel.alloc(), redisURI.getHost(), redisURI.getPort());
SSLEngine sslEngine = hostAndPort != null
? sslContext.newEngine(alloc, hostAndPort.getHostText(), hostAndPort.getPort())
: sslContext.newEngine(alloc);
sslEngine.setSSLParameters(sslParams);

SslHandler sslHandler = new SslHandler(sslEngine, redisURI.isStartTls());
channel.pipeline().addLast(sslHandler);

for (ChannelHandler handler : handlers.get()) {
channel.pipeline().addLast(handler);
}

clientResources.nettyCustomizer().afterChannelInitialized(channel);
return sslEngine;
}
}
}
9 changes: 6 additions & 3 deletions src/test/bash/create_certificates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ touch ${CA_DIR}/index.txt
function generateKey {

host=$1
ip=$2

echo "[INFO] Generating server private key"
openssl genrsa -aes256 \
Expand All @@ -70,7 +71,9 @@ function generateKey {
chmod 400 ${CA_DIR}/private/${host}.decrypted.key.pem

echo "[INFO] Generating server certificate request"
openssl req -config ${DIR}/openssl.cnf \
openssl req -config <(cat ${DIR}/openssl.cnf \
<(printf "\n[SAN]\nsubjectAltName=DNS:${host},IP:${ip}")) \
-reqexts SAN \
-key ${CA_DIR}/private/${host}.key.pem \
-passin pass:changeit \
-new -sha256 -out ${CA_DIR}/csr/${host}.csr.pem \
Expand All @@ -85,8 +88,8 @@ function generateKey {
-out ${CA_DIR}/certs/${host}.cert.pem
}

generateKey "localhost"
generateKey "foo-host"
generateKey "localhost" "127.0.0.1"
generateKey "foo-host" "1.2.3.4"

echo "[INFO] Generating client auth private key"
openssl genrsa -aes256 \
Expand Down
3 changes: 2 additions & 1 deletion src/test/bash/openssl.cnf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_strict
copy_extensions = copy

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
Expand Down Expand Up @@ -103,4 +104,4 @@ nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
extendedKeyUsage = serverAuth
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@
*/
package io.lettuce.core.sentinel;

import static io.lettuce.test.settings.TestSettings.sslPort;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.File;

import javax.inject.Inject;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import io.lettuce.RedisBug;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.TestSupport;
import io.lettuce.core.*;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.internal.HostAndPort;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DnsResolver;
import io.lettuce.core.resource.MappingSocketAddressResolver;
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
import io.lettuce.test.CanConnect;
import io.lettuce.test.LettuceExtension;
import io.lettuce.test.resource.FastShutdown;
import io.lettuce.test.settings.TestSettings;
Expand All @@ -45,6 +49,8 @@
@RedisBug("https://github.com/antirez/redis/issues/6160")
class SentinelSslIntegrationTests extends TestSupport {

private static final File TRUSTSTORE_FILE = new File("work/truststore.jks");

private final ClientResources clientResources;

@Inject
Expand All @@ -56,10 +62,16 @@ class SentinelSslIntegrationTests extends TestSupport {
})).build();
}

@BeforeAll
static void beforeAll() {
assumeTrue(CanConnect.to(TestSettings.host(), sslPort()), "Assume that stunnel runs on port 6443");
assertThat(TRUSTSTORE_FILE).exists();
}

@Test
void shouldConnectSentinelDirectly() {

RedisURI redisURI = RedisURI.create("rediss://" + TestSettings.host() + ":26379");
RedisURI redisURI = RedisURI.create("rediss://" + TestSettings.host() + ":" + RedisURI.DEFAULT_SENTINEL_PORT);
redisURI.setVerifyPeer(false);

RedisClient client = RedisClient.create(clientResources);
Expand All @@ -74,10 +86,12 @@ void shouldConnectSentinelDirectly() {
@Test
void shouldConnectToMasterUsingSentinel() {

RedisURI redisURI = RedisURI.create("rediss-sentinel://" + TestSettings.host() + ":26379?sentinelMasterId=mymaster");
redisURI.setVerifyPeer(false);
RedisURI redisURI = RedisURI.create("rediss-sentinel://" + TestSettings.host() + ":" + RedisURI.DEFAULT_SENTINEL_PORT
+ "?sentinelMasterId=mymaster");
SslOptions options = SslOptions.builder().truststore(TRUSTSTORE_FILE).build();

RedisClient client = RedisClient.create(clientResources);
client.setOptions(ClientOptions.builder().sslOptions(options).build());
StatefulRedisConnection<String, String> connection = client.connect(redisURI);

assertThat(connection.sync().ping()).isNotNull();
Expand Down

0 comments on commit 8a1a22e

Please sign in to comment.