Skip to content

Commit

Permalink
Reloadable WebServer TLS during runtime (helidon-io#2900)
Browse files Browse the repository at this point in the history
Reloadable WebServer TLS during runtime

Signed-off-by: David Kral <david.k.kral@oracle.com>
  • Loading branch information
Verdent authored and aseovic committed Apr 26, 2021
1 parent a5a0e5e commit 9b6c6e1
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 56 deletions.
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 @@ -50,7 +50,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 @@ -158,7 +158,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

0 comments on commit 9b6c6e1

Please sign in to comment.