Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reloadable WebServer TLS during runtime #2900

Merged
merged 6 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import io.helidon.common.http.Http;
import io.helidon.config.Config;
import io.helidon.config.ConfigSources;
import io.helidon.webclient.WebClient;
import io.helidon.webserver.Routing;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.WebServerTls;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -71,29 +73,29 @@ public void testAccessSuccessful() {
.await();

assertThat(result, is("Hello world unsecured!"));
assertThat(callSecured(webClient, webServer.port("secured")), is("Hello Helidon-client!"));
assertThat(executeRequest(webClient, "https", webServer.port("secured")), is("Hello Helidon-client!"));
}

@Test
public void testNoClientCert() {
WebClient webClient = createWebClient(CONFIG.get("no-client-cert"));

CompletionException exception = assertThrows(CompletionException.class,
() -> callSecured(webClient,
webServer.port("secured")));
() -> executeRequest(webClient, "https",
webServer.port("secured")));
assertThat(exception.getCause().getMessage(), containsString("Received fatal alert: bad_certificate"));
}

@Test
public void testOptionalAuthentication() {
WebClient webClient = createWebClient(CONFIG.get("no-client-cert"));

assertThat(callSecured(webClient,
webServer.port("optional")), is("Hello Unknown CN!"));
assertThat(executeRequest(webClient, "https",
webServer.port("optional")), is("Hello Unknown CN!"));

webClient = createWebClient(CONFIG.get("success"));
assertThat(callSecured(webClient,
webServer.port("optional")), is("Hello Helidon-client!"));
assertThat(executeRequest(webClient, "https",
webServer.port("optional")), is("Hello Helidon-client!"));
}

@Test
Expand All @@ -102,11 +104,11 @@ public void testServerCertInvalidCn() {
WebClient webClientOne = createWebClient(CONFIG.get("server-cert-invalid-cn"));

CompletionException exception = assertThrows(CompletionException.class,
() -> callSecured(webClientOne, port));
() -> executeRequest(webClientOne, "https", port));
assertThat(exception.getCause().getMessage(), is("No name matching localhost found"));

WebClient webClientTwo = createWebClient(CONFIG.get("client-disable-hostname-verification"));
assertThat(callSecured(webClientTwo, port), is("Hello Helidon-client!"));
assertThat(executeRequest(webClientTwo, "https", port), is("Hello Helidon-client!"));
}

@Test
Expand All @@ -115,20 +117,52 @@ public void testClientNoCa() {
WebClient webClientOne = createWebClient(CONFIG.get("client-no-ca"));

CompletionException exception = assertThrows(CompletionException.class,
() -> callSecured(webClientOne, port));
() -> executeRequest(webClientOne, "https", port));
assertThat(exception.getCause().getMessage(), endsWith("unable to find valid certification path to requested target"));

WebClient webClientTwo = createWebClient(CONFIG.get("client-trust-all"));
assertThat(callSecured(webClientTwo, port), is("Hello Helidon-client!"));
assertThat(executeRequest(webClientTwo, "https", port), is("Hello Helidon-client!"));
}

@Test
public void testClientAndServerUpdateTls() {
int portSecured = webServer.port("secured");
int portDefault = webServer.port();
WebClient webClientFirst = createWebClient(CONFIG.get("success"));
WebClient webClientSecond = createWebClient(CONFIG.get("client-second-valid"));

assertThat(executeRequest(webClientFirst, "https", portSecured), is("Hello Helidon-client!"));
CompletionException exception = assertThrows(CompletionException.class,
() -> executeRequest(webClientSecond, "https", portSecured));
assertThat(exception.getCause().getMessage(), endsWith("unable to find valid certification path to requested target"));
String response = webClientFirst.get().uri("http://localhost:" + portDefault + "/reload").request(String.class).await();
assertThat(response, is("SslContext reloaded. Affected named socket: secured"));

assertThat(executeRequest(webClientSecond, "https", portSecured), is("Hello Oracle-client!"));

exception = assertThrows(CompletionException.class,
() -> executeRequest(webClientFirst, "https", portSecured));
assertThat(exception.getCause().getMessage(), endsWith("unable to find valid certification path to requested target"));

response = webClientFirst.get().uri("http://localhost:" + portDefault + "/reload").request(String.class).await();
assertThat(response, is("SslContext reloaded. Affected named socket: secured"));

assertThat(executeRequest(webClientFirst, "https", portSecured), is("Hello Helidon-client!"));
exception = assertThrows(CompletionException.class,
() -> executeRequest(webClientSecond, "https", portSecured));
assertThat(exception.getCause().getMessage(), endsWith("unable to find valid certification path to requested target"));
}

private WebClient createWebClient(Config config) {
return WebClient.create(config);
return WebClient.builder()
.config(config)
.keepAlive(false)
.build();
}

private String callSecured(WebClient webClient, int port) {
private String executeRequest(WebClient webClient, String schema, int port) {
return webClient.get()
.uri("https://localhost:" + port)
.uri(schema + "://localhost:" + port)
.request(String.class)
.await();
}
Expand Down Expand Up @@ -170,8 +204,18 @@ private static WebServer createWebServer(Config config) {
}

private static Routing createPlainRouting() {
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
return Routing.builder()
.get("/", (req, res) -> res.send("Hello world unsecured!"))
.get("/reload",
(req, res) -> {
String configName = atomicBoolean.getAndSet(!atomicBoolean.get())
? "server-second-valid.tls"
: "server.sockets.0.ssl";
req.webServer().updateTls(WebServerTls.create(CONFIG.get(configName)),
"secured");
res.send("SslContext reloaded. Affected named socket: secured");
})
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2020 Oracle and/or its affiliates.
# Copyright (c) 2020, 2021 Oracle and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -160,3 +160,31 @@ client-trust-all:
resource:
resource-path: "client-no-ca/client.p12"

server-second-valid:
tls:
client-auth: "REQUIRE"
trust:
keystore:
passphrase: "password"
trust-store: true
resource:
resource-path: "second-valid/server.p12"
private-key:
keystore:
passphrase: "password"
resource:
resource-path: "second-valid/server.p12"

client-second-valid:
tls:
server:
keystore:
passphrase: "password"
trust-store: true
resource:
resource-path: "second-valid/client.p12"
client:
keystore:
passphrase: "password"
resource:
resource-path: "second-valid/client.p12"
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* Copyright (c) 2017, 2021 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,7 @@
import io.helidon.media.common.MessageBodyWriterContext;
import io.helidon.webserver.ServerConfiguration;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.WebServerTls;

/**
* Kind of WebServer mock for tests.
Expand Down Expand Up @@ -100,4 +101,14 @@ public MessageBodyWriterContext writerContext() {
public int port(String name) {
return 0;
}

@Override
public void updateTls(WebServerTls tls) {

}

@Override
public void updateTls(WebServerTls tls, String socketName) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

import static io.helidon.webserver.HttpInitializer.CERTIFICATE_NAME;
import static io.helidon.webserver.HttpInitializer.CLIENT_CERTIFICATE_NAME;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE;
Expand Down Expand Up @@ -157,7 +157,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) {

// Certificate management
request.headers().remove(Http.Header.X_HELIDON_CN);
Optional.ofNullable(ctx.channel().attr(CERTIFICATE_NAME).get())
Optional.ofNullable(ctx.channel().attr(CLIENT_CERTIFICATE_NAME).get())
.ifPresent(name -> request.headers().set(Http.Header.X_HELIDON_CN, name));

// Context, publisher and DataChunk queue for this request/response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@
*/
class HttpInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger LOGGER = Logger.getLogger(HttpInitializer.class.getName());
static final AttributeKey<String> CERTIFICATE_NAME = AttributeKey.valueOf("certificate_name");
static final AttributeKey<String> CLIENT_CERTIFICATE_NAME = AttributeKey.valueOf("client_certificate_name");
static final AttributeKey<X509Certificate> CLIENT_CERTIFICATE = AttributeKey.valueOf("client_certificate");
static final AttributeKey<Certificate[]> CLIENT_CERTIFICATE_CHAIN = AttributeKey.valueOf("client_certificate_chain");

private final SslContext sslContext;
private final NettyWebServer webServer;
private final SocketConfiguration soConfig;
private final Routing routing;
private final AtomicBoolean clearLock = new AtomicBoolean();
private volatile SslContext sslContext;

/**
* Reference queue that collects ReferenceHoldingQueue's when they become
Expand Down Expand Up @@ -131,6 +133,13 @@ void queuesShutdown() {
});
}

void updateSslContext(SslContext context) {
if (sslContext == null) {
throw new IllegalStateException("Current TLS context is not set, update not allowed");
}
sslContext = context;
}

/**
* Initializes pipeline for new socket channel.
*
Expand All @@ -141,8 +150,9 @@ public void initChannel(SocketChannel ch) {
final ChannelPipeline p = ch.pipeline();

SSLEngine sslEngine = null;
if (sslContext != null) {
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
SslContext context = sslContext;
if (context != null) {
SslHandler sslHandler = context.newHandler(ch.alloc());
sslEngine = sslHandler.engine();
p.addLast(sslHandler);
sslHandler.handshakeFuture().addListener(future -> obtainClientCN(future, ch, sslHandler));
Expand Down Expand Up @@ -217,7 +227,9 @@ private void obtainClientCN(Future<? super Channel> future, SocketChannel ch, Ss
tmpName = tmpName.substring(0, end);
}
}
ch.attr(CERTIFICATE_NAME).set(tmpName);
ch.attr(CLIENT_CERTIFICATE_NAME).set(tmpName);
ch.attr(CLIENT_CERTIFICATE).set(cert);
ch.attr(CLIENT_CERTIFICATE_CHAIN).set(peerCertificates);
}
} catch (SSLPeerUnverifiedException ignored) {
//User not authenticated. Client authentication probably set to OPTIONAL or NONE
Expand Down
Loading