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

Add RPC WS options to specify password file for keystore and truststore #7970

Merged
merged 4 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<PASSWORD>",
description = "Password for the WebSocket RPC keystore file")
private String rpcWsKeyStorePassword;

@CommandLine.Option(
names = {"--rpc-ws-ssl-keystore-password-file"},
paramLabel = "<FILE>",
description = "File containing the password for WebSocket keystore.")
private String rpcWsKeystorePasswordFile;
}

static class TruststorePasswordOptions {
@CommandLine.Option(
names = {"--rpc-ws-ssl-truststore-password"},
paramLabel = "<PASSWORD>",
description = "Password for the WebSocket RPC truststore file")
private String rpcWsTrustStorePassword;

@CommandLine.Option(
names = {"--rpc-ws-ssl-truststore-password-file"},
paramLabel = "<FILE>",
description = "File containing the password for WebSocket truststore.")
private String rpcWsTruststorePasswordFile;
}

@CommandLine.Option(
names = {"--rpc-ws-authentication-jwt-algorithm"},
description =
Expand Down Expand Up @@ -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 = "<PASSWORD>",
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,
Expand Down Expand Up @@ -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 = "<PASSWORD>",
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,
Expand All @@ -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() {}

Expand Down Expand Up @@ -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"));
}
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions besu/src/test/resources/everything_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to specify both password and password-file or should we only allow one or the other?

Copy link
Contributor

@fab-10 fab-10 Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is handled by @CommandLine.ArgGroup(exclusive = true, multiplicity = "1")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's the intent using ArgGroup to make them exclusive but it doesn't seem to work when I tested. Right now , password takes precedence over password file if both are specified. I'll track this in a new issue

rpc-ws-ssl-truststore-type="none"
rpc-ws-ssl-key-file="none.pfx"
rpc-ws-ssl-cert-file="none.pfx"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,11 +58,13 @@ public class WebSocketConfiguration {
private Optional<String> keyStorePath = Optional.empty();
private Optional<String> keyStorePassword = Optional.empty();
private Optional<String> keyStoreType = Optional.of("JKS"); // Default to JKS
private Optional<String> keyStorePasswordFile = Optional.empty();

private boolean clientAuthEnabled = false;
private Optional<String> trustStorePath = Optional.empty();
private Optional<String> trustStorePassword = Optional.empty();
private Optional<String> trustStoreType = Optional.of("JKS"); // Default to JKS
private Optional<String> trustStorePasswordFile = Optional.empty();

// For PEM format
private Optional<String> keyPath = Optional.empty();
Expand Down Expand Up @@ -191,8 +197,11 @@ public void setKeyStorePath(final String keyStorePath) {
this.keyStorePath = Optional.ofNullable(keyStorePath);
}

public Optional<String> getKeyStorePassword() {
return keyStorePassword;
public Optional<String> getKeyStorePassword() throws IOException {
if (keyStorePassword.isPresent()) {
return keyStorePassword;
}
return Optional.ofNullable(getKeystorePasswordFromFile());
}

public void setKeyStorePassword(final String keyStorePassword) {
Expand Down Expand Up @@ -245,8 +254,11 @@ public void setTrustStorePath(final String trustStorePath) {
}

// Truststore Password
public Optional<String> getTrustStorePassword() {
return trustStorePassword;
public Optional<String> getTrustStorePassword() throws IOException {
if (trustStorePassword.isPresent()) {
return trustStorePassword;
}
return Optional.ofNullable(getTruststorePasswordFromFile());
}

public void setTrustStorePassword(final String trustStorePassword) {
Expand All @@ -258,6 +270,38 @@ public Optional<String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading