diff --git a/CHANGELOG.md b/CHANGELOG.md index 0444a9deb61..4ee0947a31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,9 +43,11 @@ - Add a method to check if a metric category is enabled to the plugin API [#7832](https://github.com/hyperledger/besu/pull/7832) - Add a new metric collector for counters which get their value from suppliers [#7894](https://github.com/hyperledger/besu/pull/7894) - Add account and state overrides to `eth_call` [#7801](https://github.com/hyperledger/besu/pull/7801) and `eth_estimateGas` [#7890](https://github.com/hyperledger/besu/pull/7890) +- Add RPC WS options to specify password file for keystore and truststore [#7970](https://github.com/hyperledger/besu/pull/7970) - Prometheus Java Metrics library upgraded to version 1.3.3 [#7880](https://github.com/hyperledger/besu/pull/7880) - Add histogram to Prometheus metrics system [#7944](https://github.com/hyperledger/besu/pull/7944) + ### Bug fixes - Fix registering new metric categories from plugins [#7825](https://github.com/hyperledger/besu/pull/7825) - Fix CVE-2024-47535 [7878](https://github.com/hyperledger/besu/pull/7878) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/RpcWebsocketOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/RpcWebsocketOptions.java index 2dde517ccc7..fb7c73e7eae 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/RpcWebsocketOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/RpcWebsocketOptions.java @@ -37,6 +37,35 @@ /** This class represents the WebSocket options for the RPC. */ public class RpcWebsocketOptions { + + static class KeystorePasswordOptions { + @CommandLine.Option( + names = {"--rpc-ws-ssl-keystore-password"}, + paramLabel = "", + description = "Password for the WebSocket RPC keystore file") + private String rpcWsKeyStorePassword; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-keystore-password-file"}, + paramLabel = "", + description = "File containing the password for WebSocket keystore.") + private String rpcWsKeystorePasswordFile; + } + + static class TruststorePasswordOptions { + @CommandLine.Option( + names = {"--rpc-ws-ssl-truststore-password"}, + paramLabel = "", + description = "Password for the WebSocket RPC truststore file") + private String rpcWsTrustStorePassword; + + @CommandLine.Option( + names = {"--rpc-ws-ssl-truststore-password-file"}, + paramLabel = "", + description = "File containing the password for WebSocket truststore.") + private String rpcWsTruststorePasswordFile; + } + @CommandLine.Option( names = {"--rpc-ws-authentication-jwt-algorithm"}, description = @@ -131,12 +160,6 @@ public class RpcWebsocketOptions { 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-key-file"}, paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, @@ -167,12 +190,6 @@ public class RpcWebsocketOptions { 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-trustcert-file"}, paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, @@ -185,6 +202,12 @@ public class RpcWebsocketOptions { description = "Type of the truststore (JKS, PKCS12, PEM)") private String rpcWsTrustStoreType = null; + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") + private KeystorePasswordOptions keystorePasswordOptions; + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") + private TruststorePasswordOptions truststorePasswordOptions; + /** Default Constructor. */ public RpcWebsocketOptions() {} @@ -292,7 +315,7 @@ private void checkOptionDependencies(final Logger logger, final CommandLine comm commandLine, "--rpc-ws-ssl-keystore-file", rpcWsKeyStoreFile == null, - List.of("--rpc-ws-ssl-keystore-password")); + List.of("--rpc-ws-ssl-keystore-password", "--rpc-ws-ssl-keystore-password-file")); } } @@ -302,7 +325,7 @@ private void checkOptionDependencies(final Logger logger, final CommandLine comm commandLine, "--rpc-ws-ssl-truststore-file", rpcWsTrustStoreFile == null, - List.of("--rpc-ws-ssl-truststore-password")); + List.of("--rpc-ws-ssl-truststore-password", "--rpc-ws-ssl-truststore-password-file")); } if (isRpcWsAuthenticationEnabled) { @@ -343,16 +366,27 @@ public WebSocketConfiguration webSocketConfiguration( 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); webSocketConfiguration.setKeyPath(rpcWsKeyFile); webSocketConfiguration.setCertPath(rpcWsCertFile); webSocketConfiguration.setTrustCertPath(rpcWsTrustCertFile); + if (keystorePasswordOptions != null) { + webSocketConfiguration.setKeyStorePassword(keystorePasswordOptions.rpcWsKeyStorePassword); + webSocketConfiguration.setKeyStorePasswordFile( + keystorePasswordOptions.rpcWsKeystorePasswordFile); + } + + if (truststorePasswordOptions != null) { + webSocketConfiguration.setTrustStorePassword( + truststorePasswordOptions.rpcWsTrustStorePassword); + webSocketConfiguration.setTrustStorePasswordFile( + truststorePasswordOptions.rpcWsTruststorePasswordFile); + } + return webSocketConfiguration; } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index e210d9fa92e..3904fe90d87 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -123,10 +123,12 @@ 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-password-file="none.txt" 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-password-file="none.txt" rpc-ws-ssl-truststore-type="none" rpc-ws-ssl-key-file="none.pfx" rpc-ws-ssl-cert-file="none.pfx" 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 42905b96a70..278e06bd7dd 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 @@ -20,6 +20,10 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -54,11 +58,13 @@ public class WebSocketConfiguration { private Optional keyStorePath = Optional.empty(); private Optional keyStorePassword = Optional.empty(); private Optional keyStoreType = Optional.of("JKS"); // Default to JKS + private Optional keyStorePasswordFile = Optional.empty(); private boolean clientAuthEnabled = false; private Optional trustStorePath = Optional.empty(); private Optional trustStorePassword = Optional.empty(); private Optional trustStoreType = Optional.of("JKS"); // Default to JKS + private Optional trustStorePasswordFile = Optional.empty(); // For PEM format private Optional keyPath = Optional.empty(); @@ -191,8 +197,11 @@ public void setKeyStorePath(final String keyStorePath) { this.keyStorePath = Optional.ofNullable(keyStorePath); } - public Optional getKeyStorePassword() { - return keyStorePassword; + public Optional getKeyStorePassword() throws IOException { + if (keyStorePassword.isPresent()) { + return keyStorePassword; + } + return Optional.ofNullable(getKeystorePasswordFromFile()); } public void setKeyStorePassword(final String keyStorePassword) { @@ -245,8 +254,11 @@ public void setTrustStorePath(final String trustStorePath) { } // Truststore Password - public Optional getTrustStorePassword() { - return trustStorePassword; + public Optional getTrustStorePassword() throws IOException { + if (trustStorePassword.isPresent()) { + return trustStorePassword; + } + return Optional.ofNullable(getTruststorePasswordFromFile()); } public void setTrustStorePassword(final String trustStorePassword) { @@ -258,6 +270,38 @@ public Optional getTrustStoreType() { return trustStoreType; } + public void setKeyStorePasswordFile(final String keyStorePasswordFile) { + this.keyStorePasswordFile = Optional.ofNullable(keyStorePasswordFile); + } + + public void setTrustStorePasswordFile(final String trustStorePasswordFile) { + this.trustStorePasswordFile = Optional.ofNullable(trustStorePasswordFile); + } + + private String loadPasswordFromFile(final String passwordFile) throws IOException { + if (passwordFile != null) { + Path path = Path.of(passwordFile); + if (Files.exists(path)) { + return Files.readString(path, StandardCharsets.UTF_8).trim(); + } + } + return null; + } + + public String getKeystorePasswordFromFile() throws IOException { + if (keyStorePasswordFile.isPresent()) { + return loadPasswordFromFile(keyStorePasswordFile.get()); + } + return null; + } + + public String getTruststorePasswordFromFile() throws IOException { + if (trustStorePasswordFile.isPresent()) { + return loadPasswordFromFile(trustStorePasswordFile.get()); + } + return null; + } + public void setTrustStoreType(final String trustStoreType) { this.trustStoreType = Optional.ofNullable(trustStoreType); } 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 31c86b18d21..d72a659cd74 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 @@ -122,9 +122,16 @@ public CompletableFuture start() { // Check if SSL/TLS is enabled in the configuration if (configuration.isSslEnabled()) { serverOptions.setSsl(true); + String keystorePassword = null; String keystorePath = configuration.getKeyStorePath().orElse(null); - String keystorePassword = configuration.getKeyStorePassword().orElse(null); + try { + keystorePassword = configuration.getKeyStorePassword().orElse(null); + } catch (Exception e) { + LOG.error("Error reading keystore password", e); + resultFuture.completeExceptionally(e); + return resultFuture; + } String keyPath = configuration.getKeyPath().orElse(null); String certPath = configuration.getCertPath().orElse(null); @@ -146,9 +153,16 @@ public CompletableFuture start() { // Set up truststore for client authentication (mTLS) if (configuration.isClientAuthEnabled()) { serverOptions.setClientAuth(ClientAuth.REQUIRED); + String truststorePassword; String truststorePath = configuration.getTrustStorePath().orElse(null); - String truststorePassword = configuration.getTrustStorePassword().orElse(""); + try { + truststorePassword = configuration.getTrustStorePassword().orElse(null); + } catch (Exception e) { + LOG.error("Error reading truststore password", e); + resultFuture.completeExceptionally(e); + return resultFuture; + } String truststoreType = configuration.getTrustStoreType().orElse("JKS"); String trustCertPath = configuration.getTrustCertPath().orElse(null); 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 9a227dc4326..da4fd9458d0 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 @@ -30,6 +30,9 @@ import java.io.File; import java.io.FileOutputStream; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.Files; import java.security.KeyStore; import java.util.Base64; import java.util.HashMap; @@ -390,14 +393,20 @@ public void shouldAuthenticateClient(final VertxTestContext testContext) throws clientTrustStore.store(fos, "password".toCharArray()); } + File tempFile = File.createTempFile("pwdfile", ".txt"); + tempFile.deleteOnExit(); + try (Writer writer = Files.newBufferedWriter(tempFile.toPath(), Charset.defaultCharset())) { + writer.write("password"); + } + // Configure WebSocket with SSL and client authentication enabled config.setSslEnabled(true); config.setKeyStorePath(serverKeystoreFile.getAbsolutePath()); - config.setKeyStorePassword("password"); + config.setKeyStorePasswordFile(tempFile.getAbsolutePath()); config.setKeyStoreType("PKCS12"); config.setClientAuthEnabled(true); config.setTrustStorePath(serverTruststoreFile.getAbsolutePath()); - config.setTrustStorePassword("password"); + config.setTrustStorePasswordFile(tempFile.getAbsolutePath()); config.setTrustStoreType("PKCS12"); // Create and start WebSocketService