From 7fb2af52d956eea69316f7b6beeae8c502a48523 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 15 Jan 2020 22:34:42 +0100 Subject: [PATCH] Use hostname and port from SocketAddress for peer verification #1209 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. --- Makefile | 23 ++----- .../io/lettuce/core/AbstractRedisClient.java | 2 +- .../io/lettuce/core/ConnectionBuilder.java | 4 ++ .../io/lettuce/core/SslConnectionBuilder.java | 60 +++++++++++++++---- src/test/bash/create_certificates.sh | 9 ++- src/test/bash/openssl.cnf | 3 +- .../sentinel/SentinelSslIntegrationTests.java | 26 ++++++-- 7 files changed, 88 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 61a9e1d36e..05534af4a2 100644 --- a/Makefile +++ b/Makefile @@ -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 >> $@ @@ -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 >> $@ @@ -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 diff --git a/src/main/java/io/lettuce/core/AbstractRedisClient.java b/src/main/java/io/lettuce/core/AbstractRedisClient.java index d6fd0683a3..c586f8fa1b 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisClient.java +++ b/src/main/java/io/lettuce/core/AbstractRedisClient.java @@ -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); diff --git a/src/main/java/io/lettuce/core/ConnectionBuilder.java b/src/main/java/io/lettuce/core/ConnectionBuilder.java index dc82d41040..5503e28314 100644 --- a/src/main/java/io/lettuce/core/ConnectionBuilder.java +++ b/src/main/java/io/lettuce/core/ConnectionBuilder.java @@ -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 socketAddressSupplier) { this.socketAddressSupplier = socketAddressSupplier; return this; diff --git a/src/main/java/io/lettuce/core/SslConnectionBuilder.java b/src/main/java/io/lettuce/core/SslConnectionBuilder.java index d5457cbd13..8525a11202 100644 --- a/src/main/java/io/lettuce/core/SslConnectionBuilder.java +++ b/src/main/java/io/lettuce/core/SslConnectionBuilder.java @@ -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; @@ -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; @@ -75,10 +78,41 @@ protected List 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; } /** @@ -88,7 +122,9 @@ static class SslChannelInitializer extends io.netty.channel.ChannelInitializer> pingCommandSupplier; private final Supplier> 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; @@ -96,12 +132,14 @@ static class SslChannelInitializer extends io.netty.channel.ChannelInitializer initializedFuture = new CompletableFuture<>(); public SslChannelInitializer(Supplier> pingCommandSupplier, - Supplier> handlers, RedisURI redisURI, ClientResources clientResources, Duration timeout, - SslOptions sslOptions) { + Supplier> 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; @@ -114,10 +152,10 @@ 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); @@ -125,7 +163,9 @@ private void doInitialize(Channel channel) throws IOException, GeneralSecurityEx 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) { @@ -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) { diff --git a/src/test/bash/create_certificates.sh b/src/test/bash/create_certificates.sh index be98469501..28db1eb517 100755 --- a/src/test/bash/create_certificates.sh +++ b/src/test/bash/create_certificates.sh @@ -56,6 +56,7 @@ touch ${CA_DIR}/index.txt function generateKey { host=$1 + ip=$2 echo "[INFO] Generating server private key" openssl genrsa -aes256 \ @@ -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 \ @@ -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 \ diff --git a/src/test/bash/openssl.cnf b/src/test/bash/openssl.cnf index fdf9064d8e..721bd488db 100644 --- a/src/test/bash/openssl.cnf +++ b/src/test/bash/openssl.cnf @@ -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. @@ -103,4 +104,4 @@ nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment -extendedKeyUsage = serverAuth \ No newline at end of file +extendedKeyUsage = serverAuth diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java index 9ca7855147..cdfcf2f89e 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java @@ -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; @@ -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 @@ -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); @@ -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 connection = client.connect(redisURI); assertThat(connection.sync().ping()).isNotNull();