From a9dc83312e330dea08007495ab91bcd617b05ac2 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Thu, 17 Oct 2024 14:43:22 -0400 Subject: [PATCH 1/6] Fix incorrect duration for THREE_MINUTES from 1 minute to 3 minutes Signed-off-by: Bhanu Pulluri --- .../besu/tests/acceptance/bftsoak/BftMiningSoakTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java index 878e503ba39..9098fcbb7de 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java @@ -41,7 +41,7 @@ public class BftMiningSoakTest extends ParameterizedBftTestBase { private static final long ONE_MINUTE = Duration.of(1, ChronoUnit.MINUTES).toMillis(); - private static final long THREE_MINUTES = Duration.of(1, ChronoUnit.MINUTES).toMillis(); + private static final long THREE_MINUTES = Duration.of(3, ChronoUnit.MINUTES).toMillis(); private static final long TEN_SECONDS = Duration.of(10, ChronoUnit.SECONDS).toMillis(); From 7c1469fbf8ca55220b9e9b859d53e0d4c5426e09 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Mon, 4 Nov 2024 13:39:26 -0500 Subject: [PATCH 2/6] Add options to enable TLS/mTLS for websocket connections Signed-off-by: Bhanu Pulluri --- .../options/stable/RpcWebsocketOptions.java | 56 +++ .../websocket/WebSocketConfiguration.java | 112 ++++++ .../jsonrpc/websocket/WebSocketService.java | 84 +++- .../websocket/WebSocketServiceTLSTest.java | 363 ++++++++++++++++++ 4 files changed, 605 insertions(+), 10 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java index 40edeecc0da..ed1c86faf62 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java @@ -120,6 +120,53 @@ public class RpcWebsocketOptions { arity = "1") private final File rpcWsAuthenticationPublicKeyFile = null; + @CommandLine.Option( + names = {"--rpc-ws-ssl-enabled"}, + description = "Enable SSL/TLS for the WebSocket RPC service") + private boolean isRpcWsSslEnabled = false; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-keystore-file"}, + paramLabel = "", + description = "Path to the keystore file for the WebSocket RPC service") + private String rpcWsKeyStoreFile = null; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-keystore-password"}, + paramLabel = "", + description = "Password for the WebSocket RPC keystore file") + private String rpcWsKeyStorePassword = null; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-keystore-type"}, + paramLabel = "", + description = "Type of the WebSocket RPC keystore (JKS, PKCS12, PEM)") + private String rpcWsKeyStoreType = null; + + // For client authentication (mTLS) + @CommandLine.Option( + names = {"--rpc-ws-ssl-client-auth-enabled"}, + description = "Enable client authentication for the WebSocket RPC service") + private boolean isRpcWsClientAuthEnabled = false; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-truststore-file"}, + paramLabel = "", + description = "Path to the truststore file for the WebSocket RPC service") + private String rpcWsTrustStoreFile = null; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-truststore-password"}, + paramLabel = "", + description = "Password for the WebSocket RPC truststore file") + private String rpcWsTrustStorePassword = null; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-truststore-type"}, + paramLabel = "", + description = "Type of the truststore (JKS, PKCS12, PEM)") + private String rpcWsTrustStoreType = null; + /** Default Constructor. */ public RpcWebsocketOptions() {} @@ -222,6 +269,15 @@ public WebSocketConfiguration webSocketConfiguration( webSocketConfiguration.setAuthenticationPublicKeyFile(rpcWsAuthenticationPublicKeyFile); webSocketConfiguration.setAuthenticationAlgorithm(rpcWebsocketsAuthenticationAlgorithm); webSocketConfiguration.setTimeoutSec(wsTimoutSec); + webSocketConfiguration.setSslEnabled(isRpcWsSslEnabled); + webSocketConfiguration.setKeyStorePath(rpcWsKeyStoreFile); + webSocketConfiguration.setKeyStorePassword(rpcWsKeyStorePassword); + webSocketConfiguration.setKeyStoreType(rpcWsKeyStoreType); + webSocketConfiguration.setClientAuthEnabled(isRpcWsClientAuthEnabled); + webSocketConfiguration.setTrustStorePath(rpcWsTrustStoreFile); + webSocketConfiguration.setTrustStorePassword(rpcWsTrustStorePassword); + webSocketConfiguration.setTrustStoreType(rpcWsTrustStoreType); + return webSocketConfiguration; } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java index e27f7f21cec..857e6fceb84 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import com.google.common.base.MoreObjects; @@ -49,6 +50,21 @@ public class WebSocketConfiguration { private int maxActiveConnections; private int maxFrameSize; + private boolean isSslEnabled = false; + private Optional keyStorePath = Optional.empty(); + private Optional keyStorePassword = Optional.empty(); + private Optional keyStoreType = Optional.of("JKS"); // Default to JKS + + private boolean clientAuthEnabled = false; + private Optional trustStorePath = Optional.empty(); + private Optional trustStorePassword = Optional.empty(); + private Optional trustStoreType = Optional.of("JKS"); // Default to JKS + + // For PEM format (if applicable) + private Optional keyPath = Optional.empty(); + private Optional certPath = Optional.empty(); + private Optional trustCertPath = Optional.empty(); + public static WebSocketConfiguration createDefault() { final WebSocketConfiguration config = new WebSocketConfiguration(); config.setEnabled(false); @@ -159,6 +175,102 @@ public void setTimeoutSec(final long timeoutSec) { this.timeoutSec = timeoutSec; } + public boolean isSslEnabled() { + return isSslEnabled; + } + + public void setSslEnabled(final boolean isSslEnabled) { + this.isSslEnabled = isSslEnabled; + } + + public Optional getKeyStorePath() { + return keyStorePath; + } + + public void setKeyStorePath(final String keyStorePath) { + this.keyStorePath = Optional.ofNullable(keyStorePath); + } + + public Optional getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(final String keyStorePassword) { + this.keyStorePassword = Optional.ofNullable(keyStorePassword); + } + + // Keystore Type + public Optional getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(final String keyStoreType) { + this.keyStoreType = Optional.ofNullable(keyStoreType); + } + + // Key Path (for PEM) + public Optional getKeyPath() { + return keyPath; + } + + public void setKeyPath(final String keyPath) { + this.keyPath = Optional.ofNullable(keyPath); + } + + // Cert Path (for PEM) + public Optional getCertPath() { + return certPath; + } + + public void setCertPath(final String certPath) { + this.certPath = Optional.ofNullable(certPath); + } + + // Client Authentication Enabled + public boolean isClientAuthEnabled() { + return clientAuthEnabled; + } + + public void setClientAuthEnabled(final boolean clientAuthEnabled) { + this.clientAuthEnabled = clientAuthEnabled; + } + + // Truststore Path + public Optional getTrustStorePath() { + return trustStorePath; + } + + public void setTrustStorePath(final String trustStorePath) { + this.trustStorePath = Optional.ofNullable(trustStorePath); + } + + // Truststore Password + public Optional getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(final String trustStorePassword) { + this.trustStorePassword = Optional.ofNullable(trustStorePassword); + } + + // Truststore Type + public Optional getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(final String trustStoreType) { + this.trustStoreType = Optional.ofNullable(trustStoreType); + } + + // Trust Cert Path (for PEM) + public Optional getTrustCertPath() { + return trustCertPath; + } + + public void setTrustCertPath(final String trustCertPath) { + this.trustCertPath = Optional.ofNullable(trustCertPath); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java index 59742836ba8..9778173e8ed 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import java.net.InetSocketAddress; +import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; @@ -34,6 +35,7 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.ClientAuth; import io.vertx.core.http.HttpConnection; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; @@ -41,6 +43,9 @@ import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.ServerWebSocket; import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; import io.vertx.core.net.SocketAddress; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -103,18 +108,77 @@ public CompletableFuture start() { "Starting Websocket service on {}:{}", configuration.getHost(), configuration.getPort()); final CompletableFuture resultFuture = new CompletableFuture<>(); + HttpServerOptions serverOptions = + new HttpServerOptions() + .setHost(configuration.getHost()) + .setPort(configuration.getPort()) + .setHandle100ContinueAutomatically(true) + .setCompressionSupported(true) + .addWebSocketSubProtocol("undefined") + .setMaxWebSocketFrameSize(configuration.getMaxFrameSize()) + .setMaxWebSocketMessageSize(configuration.getMaxFrameSize() * 4) + .setRegisterWebSocketWriteHandlers(true); + + // Check if SSL/TLS is enabled in the configuration + if (configuration.isSslEnabled()) { + if (configuration.getKeyStorePath().isPresent() + && configuration.getKeyStorePassword().isPresent()) { + + String keystorePath = configuration.getKeyStorePath().get(); + String keystorePassword = configuration.getKeyStorePassword().get(); + String keystoreType = configuration.getKeyStoreType().orElse("JKS"); + + serverOptions.setSsl(true); + + if (keystorePath != null) { + switch (keystoreType.toUpperCase(Locale.getDefault())) { + case "PEM": + serverOptions.setPemKeyCertOptions( + new PemKeyCertOptions() + .setKeyPath(configuration.getKeyPath().get()) + .setCertPath(configuration.getCertPath().get())); + break; + case "JKS": + default: + serverOptions.setKeyStoreOptions( + new JksOptions().setPath(keystorePath).setPassword(keystorePassword)); + break; + } + } else { + throw new IllegalArgumentException("SSL is enabled but keystore path is not provided."); + } + } + } + + // Set up truststore for client authentication (mTLS) + if (configuration.isClientAuthEnabled()) { + serverOptions.setClientAuth(ClientAuth.REQUIRED); + + String truststorePath = configuration.getTrustStorePath().orElse(null); + String truststorePassword = configuration.getTrustStorePassword().orElse(""); + String truststoreType = configuration.getTrustStoreType().orElse("JKS"); + + if (truststorePath != null) { + switch (truststoreType.toUpperCase(Locale.getDefault())) { + case "PEM": + serverOptions.setPemTrustOptions( + new PemTrustOptions().addCertPath(configuration.getTrustCertPath().get())); + break; + case "JKS": + default: + serverOptions.setTrustStoreOptions( + new JksOptions().setPath(truststorePath).setPassword(truststorePassword)); + break; + } + } else { + throw new IllegalArgumentException( + "Client authentication is enabled but truststore path is not provided."); + } + } + httpServer = vertx - .createHttpServer( - new HttpServerOptions() - .setHost(configuration.getHost()) - .setPort(configuration.getPort()) - .setHandle100ContinueAutomatically(true) - .setCompressionSupported(true) - .addWebSocketSubProtocol("undefined") - .setMaxWebSocketFrameSize(configuration.getMaxFrameSize()) - .setMaxWebSocketMessageSize(configuration.getMaxFrameSize() * 4) - .setRegisterWebSocketWriteHandlers(true)) + .createHttpServer(serverOptions) .webSocketHandler(websocketHandler()) .connectionHandler(connectionHandler()) .requestHandler(httpHandler()) diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java new file mode 100644 index 00000000000..9972073b744 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java @@ -0,0 +1,363 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions; +import org.hyperledger.besu.ethereum.api.jsonrpc.execution.BaseJsonRpcProcessor; +import org.hyperledger.besu.ethereum.api.jsonrpc.execution.JsonRpcExecutor; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.WebSocketMethodsFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager; +import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocketClient; +import io.vertx.core.http.WebSocketClientOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +public class WebSocketServiceTLSTest { + + private Vertx vertx; + private WebSocketConfiguration config; + private WebSocketMessageHandler webSocketMessageHandlerSpy; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + config = WebSocketConfiguration.createDefault(); + Map websocketMethods; + config.setPort(0); // Use ephemeral port + config.setHost("localhost"); + websocketMethods = + new WebSocketMethodsFactory( + new SubscriptionManager(new NoOpMetricsSystem()), new HashMap<>()) + .methods(); + webSocketMessageHandlerSpy = + spy( + new WebSocketMessageHandler( + vertx, + new JsonRpcExecutor(new BaseJsonRpcProcessor(), websocketMethods), + mock(EthScheduler.class), + TimeoutOptions.defaultOptions().getTimeoutSeconds())); + } + + @Test + public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testContext) + throws Throwable { + // Generate a self-signed certificate + SelfSignedCertificate ssc = new SelfSignedCertificate(); + + // Create a temporary keystore file + File keystoreFile = File.createTempFile("keystore", ".p12"); + keystoreFile.deleteOnExit(); + + // Create a PKCS12 keystore and load the self-signed certificate + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry( + "alias", + ssc.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {ssc.cert()}); + + // Save the keystore to the temporary file + try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { + keyStore.store(fos, "password".toCharArray()); + } + + // Configure WebSocket with SSL enabled + config.setSslEnabled(true); + config.setKeyStorePath(keystoreFile.getAbsolutePath()); + config.setKeyStorePassword("password"); + config.setKeyStoreType("PKCS12"); + + // Create and start WebSocketService + WebSocketService webSocketService = + new WebSocketService(vertx, config, webSocketMessageHandlerSpy, new NoOpMetricsSystem()); + webSocketService.start().join(); + + // Get the actual port + int port = webSocketService.socketAddress().getPort(); + + // Create a temporary truststore file + File truststoreFile = File.createTempFile("truststore", ".p12"); + truststoreFile.deleteOnExit(); + + // Create a PKCS12 truststore and load the server's certificate + KeyStore trustStore = KeyStore.getInstance("PKCS12"); + trustStore.load(null, null); + trustStore.setCertificateEntry("alias", ssc.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos = new FileOutputStream(truststoreFile)) { + trustStore.store(fos, "password".toCharArray()); + } + + // Configure the HTTP client with the truststore + WebSocketClientOptions clientOptions = + new WebSocketClientOptions() + .setSsl(true) + .setTrustStoreOptions( + new JksOptions().setPath(truststoreFile.getAbsolutePath()).setPassword("password")) + .setVerifyHost(true); + + WebSocketClient webSocketClient = vertx.createWebSocketClient(clientOptions); + webSocketClient + .connect(port, "localhost", "/") + .onSuccess( + ws -> { + assertThat(ws.isSsl()).isTrue(); + ws.close(); + testContext.completeNow(); + }) + .onFailure(testContext::failNow); + + assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + + // Stop the WebSocketService after the test + webSocketService.stop().join(); + } + + @Test + public void shouldFailConnectionWithWrongCertificateInTrustStore( + final VertxTestContext testContext) throws Throwable { + // Generate a self-signed certificate for the server + SelfSignedCertificate serverCert = new SelfSignedCertificate(); + + // Create a temporary keystore file for the server + File keystoreFile = File.createTempFile("keystore", ".p12"); + keystoreFile.deleteOnExit(); + + // Create a PKCS12 keystore and load the server's self-signed certificate + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry( + "alias", + serverCert.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {serverCert.cert()}); + + // Save the keystore to the temporary file + try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { + keyStore.store(fos, "password".toCharArray()); + } + + // Configure WebSocket with SSL enabled + config.setSslEnabled(true); + config.setKeyStorePath(keystoreFile.getAbsolutePath()); + config.setKeyStorePassword("password"); + config.setKeyStoreType("PKCS12"); + + // Create and start WebSocketService + WebSocketService webSocketService = + new WebSocketService(vertx, config, webSocketMessageHandlerSpy, new NoOpMetricsSystem()); + webSocketService.start().join(); + + // Get the actual port + int port = webSocketService.socketAddress().getPort(); + + // Generate a different self-signed certificate for the trust store + SelfSignedCertificate wrongCert = new SelfSignedCertificate(); + + // Create a temporary truststore file + File truststoreFile = File.createTempFile("truststore", ".p12"); + truststoreFile.deleteOnExit(); + + // Create a PKCS12 truststore and load the wrong certificate + KeyStore trustStore = KeyStore.getInstance("PKCS12"); + trustStore.load(null, null); + trustStore.setCertificateEntry("alias", wrongCert.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos = new FileOutputStream(truststoreFile)) { + trustStore.store(fos, "password".toCharArray()); + } + + // Configure the HTTP client with the truststore containing the wrong certificate + WebSocketClientOptions clientOptions = + new WebSocketClientOptions() + .setSsl(true) + .setTrustStoreOptions( + new JksOptions().setPath(truststoreFile.getAbsolutePath()).setPassword("password")) + .setVerifyHost(true); + + WebSocketClient webSocketClient = vertx.createWebSocketClient(clientOptions); + webSocketClient + .connect(port, "localhost", "/") + .onSuccess( + ws -> { + testContext.failNow(new AssertionError("Connection should have been rejected")); + }) + .onFailure( + throwable -> { + assertThat(throwable).isInstanceOf(Exception.class); + testContext.completeNow(); + }); + + assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + + // Stop the WebSocketService after the test + webSocketService.stop().join(); + } + + @Test + public void shouldAuthenticateClient(final VertxTestContext testContext) throws Throwable { + // Generate a self-signed certificate for the server + SelfSignedCertificate serverCert = new SelfSignedCertificate(); + + // Generate a self-signed certificate for the client + SelfSignedCertificate clientCert = new SelfSignedCertificate(); + + // Create a temporary keystore file for the server + File serverKeystoreFile = File.createTempFile("keystore", ".p12"); + serverKeystoreFile.deleteOnExit(); + + // Create a temporary truststore file for the server + File serverTruststoreFile = File.createTempFile("truststore", ".p12"); + serverTruststoreFile.deleteOnExit(); + + // Create a temporary keystore file for the client + File clientKeystoreFile = File.createTempFile("client-keystore", ".p12"); + clientKeystoreFile.deleteOnExit(); + + // Create a temporary truststore file for the client + File clientTruststoreFile = File.createTempFile("truststore", ".p12"); + clientTruststoreFile.deleteOnExit(); + + // Create a PKCS12 keystore and load the server's self-signed certificate + KeyStore serverKeyStore = KeyStore.getInstance("PKCS12"); + serverKeyStore.load(null, null); + serverKeyStore.setKeyEntry( + "alias", + serverCert.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {serverCert.cert()}); + + // Save the keystore to the temporary file + try (FileOutputStream fos = new FileOutputStream(serverKeystoreFile)) { + serverKeyStore.store(fos, "password".toCharArray()); + } + + // Create a PKCS12 truststore and load the client's self-signed certificate + KeyStore serverTrustStore = KeyStore.getInstance("PKCS12"); + serverTrustStore.load(null, null); + serverTrustStore.setCertificateEntry("alias", clientCert.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos = new FileOutputStream(serverTruststoreFile)) { + serverTrustStore.store(fos, "password".toCharArray()); + } + + // Create a PKCS12 keystore and load the client's self-signed certificate + KeyStore clientKeyStore = KeyStore.getInstance("PKCS12"); + clientKeyStore.load(null, null); + clientKeyStore.setKeyEntry( + "alias", + clientCert.key(), + "password".toCharArray(), + new java.security.cert.Certificate[] {clientCert.cert()}); + + // Save the client keystore to the temporary file + try (FileOutputStream fos = new FileOutputStream(clientKeystoreFile)) { + clientKeyStore.store(fos, "password".toCharArray()); + } + + // Create a PKCS12 truststore and load the server's self-signed certificate + KeyStore clientTrustStore = KeyStore.getInstance("PKCS12"); + clientTrustStore.load(null, null); + clientTrustStore.setCertificateEntry("alias", serverCert.cert()); + + // Save the truststore to the temporary file + try (FileOutputStream fos = new FileOutputStream(clientTruststoreFile)) { + clientTrustStore.store(fos, "password".toCharArray()); + } + + // Configure WebSocket with SSL and client authentication enabled + config.setSslEnabled(true); + config.setKeyStorePath(serverKeystoreFile.getAbsolutePath()); + config.setKeyStorePassword("password"); + config.setKeyStoreType("PKCS12"); + config.setClientAuthEnabled(true); + config.setTrustStorePath(serverTruststoreFile.getAbsolutePath()); + config.setTrustStorePassword("password"); + config.setTrustStoreType("PKCS12"); + + // Create and start WebSocketService + WebSocketService webSocketService = + new WebSocketService(vertx, config, webSocketMessageHandlerSpy, new NoOpMetricsSystem()); + webSocketService.start().join(); + + // Get the actual port + int port = webSocketService.socketAddress().getPort(); + + // Configure the HTTP client with the client certificate + WebSocketClientOptions clientOptions = + new WebSocketClientOptions() + .setSsl(true) + .setKeyStoreOptions( + new JksOptions() + .setPath(clientKeystoreFile.getAbsolutePath()) + .setPassword("password")) + .setTrustStoreOptions( + new JksOptions() + .setPath(clientTruststoreFile.getAbsolutePath()) + .setPassword("password")) + .setVerifyHost(true); + + WebSocketClient webSocketClient = vertx.createWebSocketClient(clientOptions); + webSocketClient + .connect(port, "localhost", "/") + .onSuccess( + ws -> { + assertThat(ws.isSsl()).isTrue(); + ws.close(); + testContext.completeNow(); + }) + .onFailure(testContext::failNow); + + assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + + // Stop the WebSocketService after the test + webSocketService.stop().join(); + } +} From 88772a966b0b0d792df06e8cfc99576d4d91f277 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Mon, 4 Nov 2024 13:52:33 -0500 Subject: [PATCH 3/6] Revert an irrelevant change Signed-off-by: Bhanu Pulluri --- .../besu/tests/acceptance/bftsoak/BftMiningSoakTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java index 9098fcbb7de..878e503ba39 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java @@ -41,7 +41,7 @@ public class BftMiningSoakTest extends ParameterizedBftTestBase { private static final long ONE_MINUTE = Duration.of(1, ChronoUnit.MINUTES).toMillis(); - private static final long THREE_MINUTES = Duration.of(3, ChronoUnit.MINUTES).toMillis(); + private static final long THREE_MINUTES = Duration.of(1, ChronoUnit.MINUTES).toMillis(); private static final long TEN_SECONDS = Duration.of(10, ChronoUnit.SECONDS).toMillis(); From e477e377a5b7c5e7446738eaa1601402ee28c83c Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Wed, 6 Nov 2024 23:42:48 -0500 Subject: [PATCH 4/6] Update tests, options and option dependencies Signed-off-by: Bhanu Pulluri --- CHANGELOG.md | 1 + .../options/stable/RpcWebsocketOptions.java | 85 +++++++++++++++- .../src/test/resources/everything_config.toml | 13 +++ .../websocket/WebSocketConfiguration.java | 2 +- .../jsonrpc/websocket/WebSocketService.java | 72 ++++++-------- .../websocket/WebSocketServiceTLSTest.java | 97 +++++++++++++++++-- 6 files changed, 214 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5dcccd7d34..aa64fb603ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Breaking Changes - Besu will now fail to start if any plugins encounter errors during initialization. To allow Besu to continue running despite plugin errors, use the `--plugin-continue-on-error` option. [#7662](https://github.com/hyperledger/besu/pull/7662) +- Support for enabling and configuring TLS/mTLS in WebSocket service. [#7854](https://github.com/hyperledger/besu/pull/7854) ### Upcoming Breaking Changes - k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java index ed1c86faf62..4a0bf72226b 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java @@ -123,11 +123,11 @@ public class RpcWebsocketOptions { @CommandLine.Option( names = {"--rpc-ws-ssl-enabled"}, description = "Enable SSL/TLS for the WebSocket RPC service") - private boolean isRpcWsSslEnabled = false; + private final Boolean isRpcWsSslEnabled = false; @CommandLine.Option( names = {"--rpc-ws-ssl-keystore-file"}, - paramLabel = "", + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, description = "Path to the keystore file for the WebSocket RPC service") private String rpcWsKeyStoreFile = null; @@ -137,6 +137,18 @@ public class RpcWebsocketOptions { description = "Password for the WebSocket RPC keystore file") private String rpcWsKeyStorePassword = null; + @CommandLine.Option( + names = {"--rpc-ws-ssl-key-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "Path to the PEM key file for the WebSocket RPC service") + private String rpcWsKeyFile = null; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-cert-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "Path to the PEM cert file for the WebSocket RPC service") + private String rpcWsCertFile = null; + @CommandLine.Option( names = {"--rpc-ws-ssl-keystore-type"}, paramLabel = "", @@ -147,11 +159,11 @@ public class RpcWebsocketOptions { @CommandLine.Option( names = {"--rpc-ws-ssl-client-auth-enabled"}, description = "Enable client authentication for the WebSocket RPC service") - private boolean isRpcWsClientAuthEnabled = false; + private final Boolean isRpcWsClientAuthEnabled = false; @CommandLine.Option( names = {"--rpc-ws-ssl-truststore-file"}, - paramLabel = "", + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, description = "Path to the truststore file for the WebSocket RPC service") private String rpcWsTrustStoreFile = null; @@ -161,6 +173,12 @@ public class RpcWebsocketOptions { description = "Password for the WebSocket RPC truststore file") private String rpcWsTrustStorePassword = null; + @CommandLine.Option( + names = {"--rpc-ws-ssl-trustcert-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "Path to the PEM trustcert file for the WebSocket RPC service") + private String rpcWsTrustCertFile = null; + @CommandLine.Option( names = {"--rpc-ws-ssl-truststore-type"}, paramLabel = "", @@ -231,7 +249,61 @@ private void checkOptionDependencies(final Logger logger, final CommandLine comm "--rpc-ws-authentication-enabled", "--rpc-ws-authentication-credentials-file", "--rpc-ws-authentication-public-key-file", - "--rpc-ws-authentication-jwt-algorithm")); + "--rpc-ws-authentication-jwt-algorithm", + "--rpc-ws-ssl-enabled")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-enabled", + !isRpcWsSslEnabled, + List.of( + "--rpc-ws-ssl-keystore-file", + "--rpc-ws-ssl-keystore-type", + "--rpc-ws-ssl-client-auth-enabled")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-client-auth-enabled", + !isRpcWsClientAuthEnabled, + List.of( + "--rpc-ws-ssl-truststore-file", + "--rpc-ws-ssl-truststore-type", + "--rpc-ws-ssl-trustcert-file")); + + if (isRpcWsSslEnabled) { + if ("PEM".equalsIgnoreCase(rpcWsKeyStoreType)) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-key-file", + rpcWsKeyFile == null, + List.of("--rpc-ws-ssl-cert-file")); + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-cert-file", + rpcWsCertFile == null, + List.of("--rpc-ws-ssl-key-file")); + } else { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-keystore-file", + rpcWsKeyStoreFile == null, + List.of("--rpc-ws-ssl-keystore-password")); + } + } + + if (isRpcWsClientAuthEnabled && !"PEM".equalsIgnoreCase(rpcWsTrustStoreType)) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-ssl-truststore-file", + rpcWsTrustStoreFile == null, + List.of("--rpc-ws-ssl-truststore-password")); + } if (isRpcWsAuthenticationEnabled) { CommandLineUtils.checkOptionDependencies( @@ -277,6 +349,9 @@ public WebSocketConfiguration webSocketConfiguration( webSocketConfiguration.setTrustStorePath(rpcWsTrustStoreFile); webSocketConfiguration.setTrustStorePassword(rpcWsTrustStorePassword); webSocketConfiguration.setTrustStoreType(rpcWsTrustStoreType); + webSocketConfiguration.setKeyPath(rpcWsKeyFile); + webSocketConfiguration.setCertPath(rpcWsCertFile); + webSocketConfiguration.setTrustCertPath(rpcWsTrustCertFile); return webSocketConfiguration; } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index e89442f7f78..960b2b5772c 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -120,6 +120,19 @@ rpc-ws-max-frame-size=65535 rpc-ws-authentication-enabled=false rpc-ws-authentication-credentials-file="none" rpc-ws-authentication-jwt-public-key-file="none" +rpc-ws-ssl-enabled=false +rpc-ws-ssl-keystore-file="none.pfx" +rpc-ws-ssl-keystore-password="none.passwd" +rpc-ws-ssl-keystore-type="none" +rpc-ws-ssl-client-auth-enabled=false +rpc-ws-ssl-truststore-file="none.pfx" +rpc-ws-ssl-truststore-password="none.passwd" +rpc-ws-ssl-truststore-type="none" +rpc-ws-ssl-key-file="none.pfx" +rpc-ws-ssl-cert-file="none.pfx" +rpc-ws-ssl-trustcert-file="none.pfx" + + # API api-gas-price-blocks=100 diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java index 857e6fceb84..42905b96a70 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java @@ -60,7 +60,7 @@ public class WebSocketConfiguration { private Optional trustStorePassword = Optional.empty(); private Optional trustStoreType = Optional.of("JKS"); // Default to JKS - // For PEM format (if applicable) + // For PEM format private Optional keyPath = Optional.empty(); private Optional certPath = Optional.empty(); private Optional trustCertPath = Optional.empty(); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java index 9778173e8ed..31c86b18d21 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java @@ -121,32 +121,25 @@ public CompletableFuture start() { // Check if SSL/TLS is enabled in the configuration if (configuration.isSslEnabled()) { - if (configuration.getKeyStorePath().isPresent() - && configuration.getKeyStorePassword().isPresent()) { - - String keystorePath = configuration.getKeyStorePath().get(); - String keystorePassword = configuration.getKeyStorePassword().get(); - String keystoreType = configuration.getKeyStoreType().orElse("JKS"); - - serverOptions.setSsl(true); - - if (keystorePath != null) { - switch (keystoreType.toUpperCase(Locale.getDefault())) { - case "PEM": - serverOptions.setPemKeyCertOptions( - new PemKeyCertOptions() - .setKeyPath(configuration.getKeyPath().get()) - .setCertPath(configuration.getCertPath().get())); - break; - case "JKS": - default: - serverOptions.setKeyStoreOptions( - new JksOptions().setPath(keystorePath).setPassword(keystorePassword)); - break; - } - } else { - throw new IllegalArgumentException("SSL is enabled but keystore path is not provided."); - } + serverOptions.setSsl(true); + + String keystorePath = configuration.getKeyStorePath().orElse(null); + String keystorePassword = configuration.getKeyStorePassword().orElse(null); + String keyPath = configuration.getKeyPath().orElse(null); + String certPath = configuration.getCertPath().orElse(null); + + String keystoreType = configuration.getKeyStoreType().orElse("JKS"); + + switch (keystoreType.toUpperCase(Locale.getDefault())) { + case "PEM": + serverOptions.setKeyCertOptions( + new PemKeyCertOptions().setKeyPath(keyPath).setCertPath(certPath)); + break; + case "JKS": + default: + serverOptions.setKeyCertOptions( + new JksOptions().setPath(keystorePath).setPassword(keystorePassword)); + break; } } @@ -157,22 +150,17 @@ public CompletableFuture start() { String truststorePath = configuration.getTrustStorePath().orElse(null); String truststorePassword = configuration.getTrustStorePassword().orElse(""); String truststoreType = configuration.getTrustStoreType().orElse("JKS"); - - if (truststorePath != null) { - switch (truststoreType.toUpperCase(Locale.getDefault())) { - case "PEM": - serverOptions.setPemTrustOptions( - new PemTrustOptions().addCertPath(configuration.getTrustCertPath().get())); - break; - case "JKS": - default: - serverOptions.setTrustStoreOptions( - new JksOptions().setPath(truststorePath).setPassword(truststorePassword)); - break; - } - } else { - throw new IllegalArgumentException( - "Client authentication is enabled but truststore path is not provided."); + String trustCertPath = configuration.getTrustCertPath().orElse(null); + + switch (truststoreType.toUpperCase(Locale.getDefault())) { + case "PEM": + serverOptions.setTrustOptions(new PemTrustOptions().addCertPath(trustCertPath)); + break; + case "JKS": + default: + serverOptions.setTrustOptions( + new JksOptions().setPath(truststorePath).setPassword(truststorePassword)); + break; } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java index 9972073b744..9a227dc4326 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceTLSTest.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -30,6 +31,7 @@ import java.io.File; import java.io.FileOutputStream; import java.security.KeyStore; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -39,6 +41,7 @@ import io.vertx.core.http.WebSocketClient; import io.vertx.core.http.WebSocketClientOptions; import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemTrustOptions; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import org.junit.jupiter.api.BeforeEach; @@ -79,11 +82,11 @@ public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testCon SelfSignedCertificate ssc = new SelfSignedCertificate(); // Create a temporary keystore file - File keystoreFile = File.createTempFile("keystore", ".p12"); + File keystoreFile = File.createTempFile("keystore", ".jks"); keystoreFile.deleteOnExit(); // Create a PKCS12 keystore and load the self-signed certificate - KeyStore keyStore = KeyStore.getInstance("PKCS12"); + KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(null, null); keyStore.setKeyEntry( "alias", @@ -100,7 +103,7 @@ public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testCon config.setSslEnabled(true); config.setKeyStorePath(keystoreFile.getAbsolutePath()); config.setKeyStorePassword("password"); - config.setKeyStoreType("PKCS12"); + config.setKeyStoreType("JKS"); // Create and start WebSocketService WebSocketService webSocketService = @@ -111,11 +114,11 @@ public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testCon int port = webSocketService.socketAddress().getPort(); // Create a temporary truststore file - File truststoreFile = File.createTempFile("truststore", ".p12"); + File truststoreFile = File.createTempFile("truststore", ".jks"); truststoreFile.deleteOnExit(); // Create a PKCS12 truststore and load the server's certificate - KeyStore trustStore = KeyStore.getInstance("PKCS12"); + KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load(null, null); trustStore.setCertificateEntry("alias", ssc.cert()); @@ -128,7 +131,7 @@ public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testCon WebSocketClientOptions clientOptions = new WebSocketClientOptions() .setSsl(true) - .setTrustStoreOptions( + .setTrustOptions( new JksOptions().setPath(truststoreFile.getAbsolutePath()).setPassword("password")) .setVerifyHost(true); @@ -152,6 +155,84 @@ public void shouldAcceptSecureWebSocketConnection(final VertxTestContext testCon webSocketService.stop().join(); } + @Test + public void shouldAcceptSecureWebSocketConnectionPEM(final VertxTestContext testContext) + throws Throwable { + // Generate a self-signed certificate + SelfSignedCertificate ssc = new SelfSignedCertificate(); + + // Create temporary PEM files for the certificate and key + File certFile = File.createTempFile("cert", ".pem"); + certFile.deleteOnExit(); + File keyFile = File.createTempFile("key", ".pem"); + keyFile.deleteOnExit(); + + // Write the certificate and key to the PEM files + try (FileOutputStream certOut = new FileOutputStream(certFile); + FileOutputStream keyOut = new FileOutputStream(keyFile)) { + certOut.write("-----BEGIN CERTIFICATE-----\n".getBytes(UTF_8)); + certOut.write( + Base64.getMimeEncoder(64, "\n".getBytes(UTF_8)).encode(ssc.cert().getEncoded())); + certOut.write("\n-----END CERTIFICATE-----\n".getBytes(UTF_8)); + + keyOut.write("-----BEGIN PRIVATE KEY-----\n".getBytes(UTF_8)); + keyOut.write(Base64.getMimeEncoder(64, "\n".getBytes(UTF_8)).encode(ssc.key().getEncoded())); + keyOut.write("\n-----END PRIVATE KEY-----\n".getBytes(UTF_8)); + } + + // Configure WebSocket with SSL enabled using PEM files + config.setSslEnabled(true); + config.setKeyPath(keyFile.getAbsolutePath()); + config.setCertPath(certFile.getAbsolutePath()); + config.setKeyStoreType("PEM"); + + // Create and start WebSocketService + WebSocketService webSocketService = + new WebSocketService(vertx, config, webSocketMessageHandlerSpy, new NoOpMetricsSystem()); + webSocketService.start().join(); + + // Get the actual port + int port = webSocketService.socketAddress().getPort(); + + // Create a temporary PEM file for the trust store + File trustCertFile = File.createTempFile("trust-cert", ".pem"); + trustCertFile.deleteOnExit(); + + // Write the server's certificate to the PEM file + try (FileOutputStream trustCertOut = new FileOutputStream(trustCertFile)) { + trustCertOut.write("-----BEGIN CERTIFICATE-----\n".getBytes(UTF_8)); + trustCertOut.write( + Base64.getMimeEncoder(64, "\n".getBytes(UTF_8)).encode(ssc.cert().getEncoded())); + trustCertOut.write("\n-----END CERTIFICATE-----\n".getBytes(UTF_8)); + } + + // Configure the HTTP client with the trust store using PEM files + WebSocketClientOptions clientOptions = + new WebSocketClientOptions() + .setSsl(true) + .setTrustOptions(new PemTrustOptions().addCertPath(trustCertFile.getAbsolutePath())) + .setVerifyHost(true); + + WebSocketClient webSocketClient = vertx.createWebSocketClient(clientOptions); + webSocketClient + .connect(port, "localhost", "/") + .onSuccess( + ws -> { + assertThat(ws.isSsl()).isTrue(); + ws.close(); + testContext.completeNow(); + }) + .onFailure(testContext::failNow); + + assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + + // Stop the WebSocketService after the test + webSocketService.stop().join(); + } + @Test public void shouldFailConnectionWithWrongCertificateInTrustStore( final VertxTestContext testContext) throws Throwable { @@ -211,7 +292,7 @@ public void shouldFailConnectionWithWrongCertificateInTrustStore( WebSocketClientOptions clientOptions = new WebSocketClientOptions() .setSsl(true) - .setTrustStoreOptions( + .setTrustOptions( new JksOptions().setPath(truststoreFile.getAbsolutePath()).setPassword("password")) .setVerifyHost(true); @@ -335,7 +416,7 @@ public void shouldAuthenticateClient(final VertxTestContext testContext) throws new JksOptions() .setPath(clientKeystoreFile.getAbsolutePath()) .setPassword("password")) - .setTrustStoreOptions( + .setTrustOptions( new JksOptions() .setPath(clientTruststoreFile.getAbsolutePath()) .setPassword("password")) From 7ffb1048998df25afd055cd1d690794b47173b9c Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Thu, 7 Nov 2024 10:28:58 -0500 Subject: [PATCH 5/6] Fix CHANGELOG entry Signed-off-by: Bhanu Pulluri --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa64fb603ac..4d4e152b854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ ### Breaking Changes - Besu will now fail to start if any plugins encounter errors during initialization. To allow Besu to continue running despite plugin errors, use the `--plugin-continue-on-error` option. [#7662](https://github.com/hyperledger/besu/pull/7662) -- Support for enabling and configuring TLS/mTLS in WebSocket service. [#7854](https://github.com/hyperledger/besu/pull/7854) ### Upcoming Breaking Changes - k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release @@ -33,6 +32,7 @@ - Add support for `chainId` in `CallParameters` [#7720](https://github.com/hyperledger/besu/pull/7720) - Add `--ephemery` network support for Ephemery Testnet [#7563](https://github.com/hyperledger/besu/pull/7563) thanks to [@gconnect](https://github.com/gconnect) - Add configuration of Consolidation Request Contract Address via genesis configuration [#7647](https://github.com/hyperledger/besu/pull/7647) +- Support for enabling and configuring TLS/mTLS in WebSocket service. [#7854](https://github.com/hyperledger/besu/pull/7854) ### Bug fixes - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) From 4831d62450b4f248880499f95e3cf18bd718e6ee Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Thu, 7 Nov 2024 11:27:11 -0500 Subject: [PATCH 6/6] Fix CHANGELOG entry Signed-off-by: Bhanu Pulluri --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4e152b854..fcc54e288e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Additions and Improvements - Fine tune already seen txs tracker when a tx is removed from the pool [#7755](https://github.com/hyperledger/besu/pull/7755) +- Support for enabling and configuring TLS/mTLS in WebSocket service. [#7854](https://github.com/hyperledger/besu/pull/7854) ### Bug fixes @@ -32,7 +33,6 @@ - Add support for `chainId` in `CallParameters` [#7720](https://github.com/hyperledger/besu/pull/7720) - Add `--ephemery` network support for Ephemery Testnet [#7563](https://github.com/hyperledger/besu/pull/7563) thanks to [@gconnect](https://github.com/gconnect) - Add configuration of Consolidation Request Contract Address via genesis configuration [#7647](https://github.com/hyperledger/besu/pull/7647) -- Support for enabling and configuring TLS/mTLS in WebSocket service. [#7854](https://github.com/hyperledger/besu/pull/7854) ### Bug fixes - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575)