Skip to content

Commit bcc3dc0

Browse files
committed
Proper defaulting to system truststore for downstream connections
1 parent 456dd54 commit bcc3dc0

File tree

8 files changed

+196
-65
lines changed

8 files changed

+196
-65
lines changed

proxybase/src/main/java/com/dajudge/proxybase/DownstreamSslHandlerFactory.java

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@
2424
import io.netty.channel.Channel;
2525
import io.netty.channel.ChannelHandler;
2626
import io.netty.handler.ssl.SslContext;
27-
import io.netty.handler.ssl.SslContextBuilder;
2827

29-
import javax.net.ssl.*;
28+
import javax.net.ssl.SSLException;
3029
import java.security.KeyManagementException;
3130
import java.security.NoSuchAlgorithmException;
3231
import java.util.Optional;
3332
import java.util.function.Function;
3433
import java.util.function.Supplier;
3534

3635
import static com.dajudge.proxybase.HostnameCheck.NULL_VERIFIER;
37-
import static com.dajudge.proxybase.SslUtils.*;
36+
import static com.dajudge.proxybase.SslUtils.createClientSslContext;
3837
import static com.dajudge.proxybase.certs.ReloadingKeyStoreManager.createReloader;
3938

4039
public class DownstreamSslHandlerFactory {
@@ -50,46 +49,23 @@ public static Function<Channel, ChannelHandler> createDownstreamSslHandler(
5049
return createDownstreamSslHandler(
5150
hostnameCheck,
5251
createReloader(config.getTrustStore(), clock, filesystem),
53-
config.getKeyStore().map(it -> createReloader(it, clock, filesystem)),
54-
Optional.of(downstreamEndpoint)
52+
createReloader(config.getKeyStore(), clock, filesystem),
53+
downstreamEndpoint
5554
);
5655
}
5756

5857
public static Function<Channel, ChannelHandler> createDownstreamSslHandler(
5958
final HostnameCheck hostnameCheck,
60-
final KeyStoreManager trustStoreManager,
61-
final Optional<KeyStoreManager> keyStoreManager,
62-
final Optional<Endpoint> peerEndpoint
59+
final Optional<? extends KeyStoreManager> trustStoreManager,
60+
final Optional<? extends KeyStoreManager> keyStoreManager,
61+
final Endpoint peerEndpoint
6362
) {
6463
try {
65-
final SSLContext clientContext = SSLContext.getInstance("TLS");
66-
final HostCheckingTrustManager trustManager = new HostCheckingTrustManager(
67-
createTrustManagers(trustStoreManager),
68-
hostnameCheck
69-
);
70-
final X509TrustManager[] trustManagers = {
71-
trustManager
72-
};
73-
final X509KeyManager[] keyManagers = createKeyManagers(keyStoreManager);
74-
clientContext.init(keyManagers, trustManagers, null);
75-
final SSLEngine engine = peerEndpoint
76-
.map(it -> clientContext.createSSLEngine(it.getHost(), it.getPort()))
77-
.orElse(clientContext.createSSLEngine());
78-
engine.setUseClientMode(true);
79-
final SslContext context = SslContextBuilder.forClient()
80-
.keyManager(createKeyManagerFactory(keyStoreManager))
81-
.trustManager(trustManager)
82-
.build();
83-
if (peerEndpoint.isPresent()) {
84-
return ch -> {
85-
final Endpoint endpoint = peerEndpoint.get();
86-
return context.newHandler(ch.alloc(), endpoint.getHost(), endpoint.getPort());
87-
};
88-
} else {
89-
return ch -> context.newHandler(ch.alloc());
90-
}
64+
final SslContext context = createClientSslContext(hostnameCheck, trustStoreManager, keyStoreManager);
65+
return ch -> context.newHandler(ch.alloc(), peerEndpoint.getHost(), peerEndpoint.getPort());
9166
} catch (final NoSuchAlgorithmException | KeyManagementException | SSLException e) {
9267
throw new RuntimeException("Failed to initialize downstream SSL handler", e);
9368
}
9469
}
70+
9571
}

proxybase/src/main/java/com/dajudge/proxybase/SslUtils.java

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,36 @@
1919

2020
import com.dajudge.proxybase.certs.KeyStoreManager;
2121
import com.dajudge.proxybase.certs.KeyStoreWrapper;
22+
import com.dajudge.proxybase.config.Endpoint;
23+
import io.netty.handler.ssl.SslContext;
24+
import io.netty.handler.ssl.SslContextBuilder;
2225

23-
import javax.net.ssl.KeyManagerFactory;
24-
import javax.net.ssl.TrustManagerFactory;
25-
import javax.net.ssl.X509KeyManager;
26-
import javax.net.ssl.X509TrustManager;
27-
import java.security.KeyStoreException;
28-
import java.security.NoSuchAlgorithmException;
29-
import java.security.UnrecoverableKeyException;
26+
import javax.net.ssl.*;
27+
import java.security.*;
3028
import java.util.Arrays;
29+
import java.util.List;
3130
import java.util.Optional;
3231

32+
import static java.util.Arrays.asList;
33+
import static java.util.Arrays.stream;
3334
import static javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm;
3435

3536
class SslUtils {
3637
static X509TrustManager[] createTrustManagers(final KeyStoreManager trustStoreManager) {
3738
return createTrustManagers(Optional.of(trustStoreManager));
3839
}
3940

40-
static X509TrustManager[] createTrustManagers(final Optional<KeyStoreManager> trustStoreManager) {
41+
static X509TrustManager[] createTrustManagers(final Optional<? extends KeyStoreManager> trustStoreManager) {
4142
try {
43+
final TrustManagerFactory factory = TrustManagerFactory.getInstance(getDefaultAlgorithm());
4244
if (!trustStoreManager.isPresent()) {
43-
return new X509TrustManager[]{};
45+
factory.init((KeyStore) null);
46+
return stream(factory.getTrustManagers())
47+
.map(it -> (X509TrustManager) it)
48+
.toArray(X509TrustManager[]::new);
4449
}
45-
final TrustManagerFactory factory = TrustManagerFactory.getInstance(getDefaultAlgorithm());
4650
factory.init(trustStoreManager.get().getKeyStore().getKeyStore());
47-
return Arrays.stream(factory.getTrustManagers())
51+
return stream(factory.getTrustManagers())
4852
.filter(it -> it instanceof X509TrustManager)
4953
.map(it -> (X509TrustManager) it)
5054
.toArray(X509TrustManager[]::new);
@@ -57,17 +61,17 @@ static X509KeyManager[] createKeyManagers(final KeyStoreManager keyStoreManager)
5761
return createKeyManagers(Optional.of(keyStoreManager));
5862
}
5963

60-
static X509KeyManager[] createKeyManagers(final Optional<KeyStoreManager> keyStoreManager) {
64+
static X509KeyManager[] createKeyManagers(final Optional<? extends KeyStoreManager> keyStoreManager) {
6165
if (!keyStoreManager.isPresent()) {
6266
return new X509KeyManager[]{};
6367
}
64-
return Arrays.stream(createKeyManagerFactory(keyStoreManager).getKeyManagers())
68+
return stream(createKeyManagerFactory(keyStoreManager).getKeyManagers())
6569
.filter(it -> it instanceof X509KeyManager)
6670
.map(it -> (X509KeyManager) it)
6771
.toArray(X509KeyManager[]::new);
6872
}
6973

70-
public static KeyManagerFactory createKeyManagerFactory(final Optional<KeyStoreManager> keyStoreManager) {
74+
static KeyManagerFactory createKeyManagerFactory(final Optional<? extends KeyStoreManager> keyStoreManager) {
7175
try {
7276
final KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
7377
if (keyStoreManager.isPresent()) {
@@ -82,4 +86,25 @@ public static KeyManagerFactory createKeyManagerFactory(final Optional<KeyStoreM
8286
throw new RuntimeException("Failed to setup key manager", e);
8387
}
8488
}
89+
90+
static SslContext createClientSslContext(
91+
final HostnameCheck hostnameCheck,
92+
final Optional<? extends KeyStoreManager> trustStoreManager,
93+
final Optional<? extends KeyStoreManager> keyStoreManager
94+
) throws NoSuchAlgorithmException, KeyManagementException, SSLException {
95+
final SSLContext clientContext = SSLContext.getInstance("TLS");
96+
final HostCheckingTrustManager trustManager = new HostCheckingTrustManager(
97+
createTrustManagers(trustStoreManager),
98+
hostnameCheck
99+
);
100+
final X509TrustManager[] trustManagers = {
101+
trustManager
102+
};
103+
final X509KeyManager[] keyManagers = createKeyManagers(keyStoreManager);
104+
clientContext.init(keyManagers, trustManagers, null);
105+
return SslContextBuilder.forClient()
106+
.keyManager(createKeyManagerFactory(keyStoreManager))
107+
.trustManager(trustManager)
108+
.build();
109+
}
85110
}

proxybase/src/main/java/com/dajudge/proxybase/certs/ReloadingKeyStoreManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.security.KeyStoreException;
2525
import java.security.NoSuchAlgorithmException;
2626
import java.security.cert.CertificateException;
27+
import java.util.Optional;
2728
import java.util.concurrent.atomic.AtomicBoolean;
2829
import java.util.function.Supplier;
2930

@@ -48,6 +49,14 @@ public ReloadingKeyStoreManager(
4849
this.updateIntervalMsecs = updateIntervalMsecs;
4950
}
5051

52+
public static Optional<ReloadingKeyStoreManager> createReloader(
53+
final Optional<KeyStoreConfig> keystore,
54+
final Supplier<Long> clock,
55+
final Filesystem filesystem
56+
) {
57+
return keystore.map(it -> createReloader(it, clock, filesystem));
58+
}
59+
5160
public static ReloadingKeyStoreManager createReloader(
5261
final KeyStoreConfig keystore,
5362
final Supplier<Long> clock,

proxybase/src/main/java/com/dajudge/proxybase/config/DownstreamSslConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
import java.util.Optional;
2323

2424
public class DownstreamSslConfig {
25-
private final KeyStoreConfig trustStore;
25+
private final Optional<KeyStoreConfig> trustStore;
2626
private final Optional<KeyStoreConfig> keyStore;
2727
private final boolean hostnameVerificationEnabled;
2828

2929
public DownstreamSslConfig(
30-
final KeyStoreConfig trustStore,
30+
final Optional<KeyStoreConfig> trustStore,
3131
final Optional<KeyStoreConfig> keyStore,
3232
final boolean hostnameVerificationEnabled
3333
) {
@@ -36,7 +36,7 @@ public DownstreamSslConfig(
3636
this.hostnameVerificationEnabled = hostnameVerificationEnabled;
3737
}
3838

39-
public KeyStoreConfig getTrustStore() {
39+
public Optional<KeyStoreConfig> getTrustStore() {
4040
return trustStore;
4141
}
4242

proxybase/src/test/java/com/dajudge/proxybase/BaseProxyTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,19 @@ protected void withProxy(
123123
withDownstreamServer(downstreamSslConfig, downstreamServer -> {
124124
final int port = freePort();
125125
try (final ProxyApplication ignored = new ProxyApplication(factory -> {
126+
final Endpoint upstreamEndpoint = new Endpoint("127.0.0.1", port);
127+
final Endpoint downstreamEndpoint = new Endpoint("127.0.0.1", downstreamServer.getLocalPort());
126128
final ProxyChannelInitializer initializer = (upstreamChannel, downstreamChannel) -> {
127129
upstreamChannel.pipeline()
128130
.addLast(new RelayingChannelInboundHandler("downstream", downstreamChannel));
129131
upstreamSslConfig.configureUpstreamPipeline(upstreamChannel.pipeline());
130132
downstreamChannel.pipeline()
131133
.addLast(new RelayingChannelInboundHandler("upstream", upstreamChannel));
132-
downstreamSslConfig.configureDownstreamPipeline(downstreamChannel.pipeline());
134+
downstreamSslConfig.configureDownstreamPipeline(downstreamChannel.pipeline(), downstreamEndpoint);
133135
};
134136
factory.createProxyChannel(
135-
new Endpoint("127.0.0.1", port),
136-
new Endpoint("127.0.0.1", downstreamServer.getLocalPort()),
137+
upstreamEndpoint,
138+
downstreamEndpoint,
137139
initializer
138140
);
139141
})) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.dajudge.proxybase;
2+
3+
import io.netty.bootstrap.Bootstrap;
4+
import io.netty.channel.*;
5+
import io.netty.channel.nio.NioEventLoopGroup;
6+
import io.netty.channel.socket.SocketChannel;
7+
import io.netty.channel.socket.nio.NioSocketChannel;
8+
import io.netty.handler.codec.DecoderException;
9+
import io.netty.handler.codec.http.*;
10+
import io.netty.handler.ssl.SslContext;
11+
import io.netty.handler.ssl.SslHandler;
12+
import org.junit.Test;
13+
14+
import javax.net.ssl.SSLException;
15+
import java.security.KeyManagementException;
16+
import java.security.NoSuchAlgorithmException;
17+
import java.util.Optional;
18+
import java.util.concurrent.atomic.AtomicReference;
19+
import java.util.function.Function;
20+
21+
import static com.dajudge.proxybase.HostnameCheck.NULL_VERIFIER;
22+
import static com.dajudge.proxybase.SslUtils.createClientSslContext;
23+
import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
24+
import static io.netty.handler.codec.http.HttpMethod.GET;
25+
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
26+
import static org.junit.Assert.*;
27+
28+
public class DownstreamSslContextTest {
29+
30+
@Test
31+
public void default_accepts_valid_certificate() throws Throwable {
32+
httpsGet("www.example.com", 443);
33+
}
34+
35+
@Test(expected = DecoderException.class)
36+
public void default_rejects_invalid_certificate() throws Throwable {
37+
httpsGet("self-signed.badssl.com", 443);
38+
}
39+
40+
private static void httpsGet(final String hostname, final int port) throws Throwable {
41+
final Function<Channel, SslHandler> handlerFactory = chan -> createContext()
42+
.newHandler(chan.alloc(), hostname, port);
43+
httpGet(handlerFactory, hostname, port);
44+
}
45+
46+
private static void httpGet(
47+
final Function<Channel, SslHandler> sslHandlerFactory,
48+
final String host,
49+
final int port
50+
) throws Throwable {
51+
final AtomicReference<Throwable> channelThrowable = new AtomicReference<>();
52+
final ChannelHandler clientHandler = new SimpleChannelInboundHandler<HttpObject>() {
53+
@Override
54+
protected void channelRead0(final ChannelHandlerContext ctx, final HttpObject msg) {
55+
if (msg instanceof HttpResponse) {
56+
final HttpResponse response = (HttpResponse) msg;
57+
assertEquals(200, response.status().code());
58+
}
59+
if (msg instanceof HttpContent) {
60+
final HttpContent content = (HttpContent) msg;
61+
if (content instanceof LastHttpContent) {
62+
ctx.close();
63+
}
64+
}
65+
}
66+
67+
@Override
68+
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) {
69+
channelThrowable.set(cause);
70+
}
71+
};
72+
final ChannelInitializer<SocketChannel> initializer = new ChannelInitializer<SocketChannel>() {
73+
@Override
74+
protected void initChannel(final SocketChannel ch) {
75+
final ChannelPipeline p = ch.pipeline();
76+
p.addLast(sslHandlerFactory.apply(ch));
77+
p.addLast(new HttpClientCodec());
78+
p.addLast(new HttpContentDecompressor());
79+
p.addLast(clientHandler);
80+
}
81+
};
82+
final EventLoopGroup group = new NioEventLoopGroup();
83+
try {
84+
final Channel ch = new Bootstrap()
85+
.group(group)
86+
.channel(NioSocketChannel.class)
87+
.handler(initializer)
88+
.connect(host, port)
89+
.sync()
90+
.channel();
91+
final HttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/", EMPTY_BUFFER);
92+
request.headers().set(HttpHeaderNames.HOST, host);
93+
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
94+
ch.writeAndFlush(request);
95+
ch.closeFuture().sync();
96+
} finally {
97+
group.shutdownGracefully();
98+
}
99+
100+
if (channelThrowable.get() != null) {
101+
throw channelThrowable.get();
102+
}
103+
}
104+
105+
private static SslContext createContext() {
106+
try {
107+
return createClientSslContext(
108+
NULL_VERIFIER,
109+
Optional.empty(),
110+
Optional.empty()
111+
);
112+
} catch (final NoSuchAlgorithmException | KeyManagementException | SSLException e) {
113+
throw new RuntimeException(e);
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)