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 4e8c362 commit 7fb2af5
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 39 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 @@ -305,7 +305,7 @@ private void initializeChannelAsync0(ConnectionBuilder connectionBuilder, Comple

Bootstrap redisBootstrap = connectionBuilder.bootstrap();

RedisChannelInitializer initializer = connectionBuilder.build();
RedisChannelInitializer initializer = connectionBuilder.build(redisAddress);
redisBootstrap.handler(initializer);

clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/lettuce/core/ConnectionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ public RedisChannelInitializer build() {
return new PlainChannelInitializer(pingCommandSupplier, this::buildHandlers, clientResources, timeout);
}

public RedisChannelInitializer build(SocketAddress redisAddress) {
return build();
}

public ConnectionBuilder socketAddressSupplier(Mono<SocketAddress> socketAddressSupplier) {
this.socketAddressSupplier = socketAddressSupplier;
return this;
Expand Down
60 changes: 50 additions & 10 deletions src/main/java/io/lettuce/core/SslConnectionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import static io.lettuce.core.PlainChannelInitializer.pingBeforeActivate;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.List;
Expand All @@ -34,6 +36,7 @@
import io.lettuce.core.event.connection.ConnectedEvent;
import io.lettuce.core.event.connection.ConnectionActivatedEvent;
import io.lettuce.core.event.connection.DisconnectedEvent;
import io.lettuce.core.internal.HostAndPort;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.protocol.AsyncCommand;
import io.lettuce.core.resource.ClientResources;
Expand Down Expand Up @@ -75,10 +78,41 @@ protected List<ChannelHandler> buildHandlers() {
}

@Override
@Deprecated
public RedisChannelInitializer build() {

return new SslChannelInitializer(getPingCommandSupplier(), this::buildHandlers, redisURI, clientResources(),
getTimeout(), clientOptions().getSslOptions());
return new SslChannelInitializer(getPingCommandSupplier(), this::buildHandlers, toHostAndPort(redisURI),
redisURI.isVerifyPeer(), redisURI.isStartTls(), clientResources(), getTimeout(),
clientOptions().getSslOptions());
}

@Override
public RedisChannelInitializer build(SocketAddress socketAddress) {

return new SslChannelInitializer(getPingCommandSupplier(), this::buildHandlers, toHostAndPort(socketAddress),
redisURI.isVerifyPeer(), redisURI.isStartTls(), clientResources(), getTimeout(),
clientOptions().getSslOptions());
}

static HostAndPort toHostAndPort(RedisURI redisURI) {

if (LettuceStrings.isNotEmpty(redisURI.getHost())) {
return HostAndPort.of(redisURI.getHost(), redisURI.getPort());
}

return null;
}

static HostAndPort toHostAndPort(SocketAddress socketAddress) {

if (socketAddress instanceof InetSocketAddress) {

InetSocketAddress isa = (InetSocketAddress) socketAddress;

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

return null;
}

/**
Expand All @@ -88,20 +122,24 @@ static class SslChannelInitializer extends io.netty.channel.ChannelInitializer<C

private final Supplier<AsyncCommand<?, ?, ?>> pingCommandSupplier;
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 Duration timeout;
private final SslOptions sslOptions;

private volatile CompletableFuture<Boolean> initializedFuture = new CompletableFuture<>();

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

this.pingCommandSupplier = pingCommandSupplier;
this.handlers = handlers;
this.redisURI = redisURI;
this.hostAndPort = hostAndPort;
this.verifyPeer = verifyPeer;
this.startTls = startTls;
this.clientResources = clientResources;
this.timeout = timeout;
this.sslOptions = sslOptions;
Expand All @@ -114,18 +152,20 @@ protected void initChannel(Channel channel) throws Exception {

private void doInitialize(Channel channel) throws IOException, GeneralSecurityException {

SSLParameters sslParams = sslOptions.createSSLParameters();
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(channel.alloc(), hostAndPort.getHostText(), hostAndPort.getPort())
: sslContext.newEngine(channel.alloc());
sslEngine.setSSLParameters(sslParams);

if (channel.pipeline().get("first") == null) {
Expand All @@ -145,7 +185,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
});
}

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

if (channel.pipeline().get("channelActivator") == null) {
Expand Down
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,22 +15,26 @@
*/
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.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 @@ -43,6 +47,8 @@
@ExtendWith(LettuceExtension.class)
class SentinelSslIntegrationTests extends TestSupport {

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

private final ClientResources clientResources;

@Inject
Expand All @@ -54,10 +60,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 @@ -72,10 +84,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 7fb2af5

Please sign in to comment.