diff --git a/CHANGELOG.md b/CHANGELOG.md index 172d884a4d9..9f0f3d40763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,10 @@ - Fast Sync ### Additions and Improvements +- Add RPC HTTP options to specify custom truststore and its password [#7978](https://github.com/hyperledger/besu/pull/7978) - Retrieve all transaction receipts for a block in one request [#6646](https://github.com/hyperledger/besu/pull/6646) + ### Bug fixes - Fix serialization of state overrides when `movePrecompileToAddress` is present [#8204](https://github.com/hyperledger/besu/pull/8024) - Revise the approach for setting level_compaction_dynamic_level_bytes RocksDB configuration option [#8037](https://github.com/hyperledger/besu/pull/8037) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java index c8c2c733440..371f855b7a0 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java @@ -176,6 +176,20 @@ public class JsonRpcHttpOptions { "Enable to accept clients certificate signed by a valid CA for client authentication (default: ${DEFAULT-VALUE})") private final Boolean isRpcHttpTlsCAClientsEnabled = false; + @CommandLine.Option( + names = {"--rpc-http-tls-truststore-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "Path to the truststore file for the JSON-RPC HTTP service.", + arity = "1") + private final Path rpcHttpTlsTruststoreFile = null; + + @CommandLine.Option( + names = {"--rpc-http-tls-truststore-password-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "Path to the file containing the password for the truststore.", + arity = "1") + private final Path rpcHttpTlsTruststorePasswordFile = null; + @CommandLine.Option( names = {"--rpc-http-tls-protocol", "--rpc-http-tls-protocols"}, description = "Comma separated list of TLS protocols to support (default: ${DEFAULT-VALUE})", @@ -306,7 +320,6 @@ public JsonRpcConfiguration jsonRpcConfiguration( jsonRpcConfiguration.setHost( Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost); jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist); - ; jsonRpcConfiguration.setHttpTimeoutSec(timoutSec); return jsonRpcConfiguration; } @@ -330,7 +343,18 @@ private void checkRpcTlsClientAuthOptionsDependencies( commandLine, "--rpc-http-tls-client-auth-enabled", !isRpcHttpTlsClientAuthEnabled, - asList("--rpc-http-tls-known-clients-file", "--rpc-http-tls-ca-clients-enabled")); + asList( + "--rpc-http-tls-known-clients-file", + "--rpc-http-tls-ca-clients-enabled", + "--rpc-http-tls-truststore-file", + "--rpc-http-tls-truststore-password-file")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-http-tls-truststore-file", + rpcHttpTlsTruststoreFile == null, + asList("--rpc-http-tls-truststore-password-file")); } private void checkRpcTlsOptionsDependencies(final Logger logger, final CommandLine commandLine) { @@ -392,12 +416,31 @@ private void validateTls(final CommandLine commandLine) { "File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"); } - if (isRpcHttpTlsClientAuthEnabled - && !isRpcHttpTlsCAClientsEnabled - && rpcHttpTlsKnownClientsFile == null) { - throw new CommandLine.ParameterException( - commandLine, - "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); + if (isRpcHttpTlsClientAuthEnabled) { + if (!isRpcHttpTlsCAClientsEnabled + && rpcHttpTlsKnownClientsFile == null + && rpcHttpTlsTruststoreFile == null) { + throw new CommandLine.ParameterException( + commandLine, + "Configuration error: TLS client authentication is enabled, but none of the following options are provided: " + + "1. Specify a known-clients file (--rpc-http-tls-known-clients-file) and/or Enable CA clients (--rpc-http-tls-ca-clients-enabled). " + + "2. Specify a truststore file and its password file (--rpc-http-tls-truststore-file and --rpc-http-tls-truststore-password-file). " + + "Only one of these options must be configured"); + } + + if (rpcHttpTlsTruststoreFile != null && rpcHttpTlsTruststorePasswordFile == null) { + throw new CommandLine.ParameterException( + commandLine, + "Configuration error: A truststore file is specified for JSON RPC HTTP endpoint, but the corresponding truststore password file (--rpc-http-tls-truststore-password-file) is missing"); + } + + if ((isRpcHttpTlsCAClientsEnabled || rpcHttpTlsKnownClientsFile != null) + && rpcHttpTlsTruststoreFile != null) { + throw new CommandLine.ParameterException( + commandLine, + "Configuration error: Truststore file (--rpc-http-tls-truststore-file) cannot be used together with CA clients (--rpc-http-tls-ca-clients-enabled) or a known-clients (--rpc-http-tls-known-clients-file) option. " + + "These options are mutually exclusive. Choose either truststore-based authentication or known-clients/CA clients configuration."); + } } rpcHttpTlsProtocols.retainAll(getJDKEnabledProtocols()); @@ -441,10 +484,17 @@ private boolean isRpcTlsConfigurationRequired() { private TlsClientAuthConfiguration rpcHttpTlsClientAuthConfiguration() { if (isRpcHttpTlsClientAuthEnabled) { - return TlsClientAuthConfiguration.Builder.aTlsClientAuthConfiguration() - .withKnownClientsFile(rpcHttpTlsKnownClientsFile) - .withCaClientsEnabled(isRpcHttpTlsCAClientsEnabled) - .build(); + TlsClientAuthConfiguration.Builder tlsClientAuthConfigurationBuilder = + TlsClientAuthConfiguration.Builder.aTlsClientAuthConfiguration() + .withKnownClientsFile(rpcHttpTlsKnownClientsFile) + .withCaClientsEnabled(isRpcHttpTlsCAClientsEnabled) + .withTruststorePath(rpcHttpTlsTruststoreFile); + + if (rpcHttpTlsTruststorePasswordFile != null) { + tlsClientAuthConfigurationBuilder.withTruststorePasswordSupplier( + new FileBasedPasswordProvider(rpcHttpTlsTruststorePasswordFile)); + } + return tlsClientAuthConfigurationBuilder.build(); } return null; diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java index afb53bac934..e29b16c573e 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java @@ -332,7 +332,10 @@ public void rpcHttpTlsClientAuthWithoutKnownFileReportsError() { assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)) .contains( - "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); + "Configuration error: TLS client authentication is enabled, but none of the following options are provided: " + + "1. Specify a known-clients file (--rpc-http-tls-known-clients-file) and/or Enable CA clients (--rpc-http-tls-ca-clients-enabled). " + + "2. Specify a truststore file and its password file (--rpc-http-tls-truststore-file and --rpc-http-tls-truststore-password-file). " + + "Only one of these options must be configured"); } @Test @@ -342,6 +345,7 @@ public void rpcHttpTlsClientAuthWithKnownClientFile() { final String keystoreFile = "/tmp/test.p12"; final String keystorePasswordFile = "/tmp/test.txt"; final String knownClientFile = "/tmp/knownClientFile"; + parseCommand( "--rpc-http-enabled", "--rpc-http-host", @@ -422,6 +426,90 @@ public void rpcHttpTlsClientAuthWithCAClient() { assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } + @Test + public void rpcHttpTlsClientAuthWithTrustStore() throws IOException { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String truststoreFile = "/tmp/truststore.p12"; + final String truststorePasswordFile = "/tmp/truststore.txt"; + + Files.writeString(Path.of(truststorePasswordFile), "password"); + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-truststore-file", + truststoreFile, + "--rpc-http-tls-truststore-password-file", + truststorePasswordFile); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration.isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().getTruststorePath()) + .isEqualTo(Optional.of(Path.of(truststoreFile))); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().getTrustStorePassword()) + .isEqualTo(Files.readString(Path.of(truststorePasswordFile))); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsClientAuthWithTrustStoreAndKnownClientsFileReportsError() + throws IOException { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String truststoreFile = "/tmp/truststore.p12"; + final String truststorePasswordFile = "/tmp/truststore.txt"; + final String knownClientFile = "/tmp/knownClientFile"; + + Files.writeString(Path.of(truststorePasswordFile), "password"); + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-truststore-file", + truststoreFile, + "--rpc-http-tls-truststore-password-file", + truststorePasswordFile, + "--rpc-http-tls-known-clients-file", + knownClientFile); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "Configuration error: Truststore file (--rpc-http-tls-truststore-file) cannot be used together with CA clients (--rpc-http-tls-ca-clients-enabled) or a known-clients (--rpc-http-tls-known-clients-file) option. " + + "These options are mutually exclusive. Choose either truststore-based authentication or known-clients/CA clients configuration."); + } + @Test public void rpcHttpTlsClientAuthWithCAClientAndKnownClientFile() { final String host = "1.2.3.4"; diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 402d5962765..0e944e7f0cf 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -84,6 +84,8 @@ rpc-http-tls-keystore-password-file="none.passwd" rpc-http-tls-client-auth-enabled=false rpc-http-tls-known-clients-file="rpc_tls_clients.txt" rpc-http-tls-ca-clients-enabled=false +rpc-http-tls-truststore-file="none.pfx" +rpc-http-tls-truststore-password-file="none.passwd" rpc-http-authentication-jwt-algorithm="RS256" rpc-ws-authentication-jwt-algorithm="RS256" rpc-http-tls-protocols=["TLSv1.2,TlSv1.1"] diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java index af50b53bb1c..5f746304c97 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java @@ -429,7 +429,7 @@ private void applyTlsConfig(final HttpServerOptions httpServerOptions) { try { httpServerOptions .setSsl(true) - .setPfxKeyCertOptions( + .setKeyCertOptions( new PfxOptions() .setPath(tlsConfiguration.getKeyStorePath().toString()) .setPassword(tlsConfiguration.getKeyStorePassword())) @@ -472,6 +472,14 @@ private void applyTlsClientAuth( httpServerOptions.setTrustOptions( allowlistClients( knownClientsFile, clientAuthConfiguration.isCaClientsEnabled()))); + clientAuthConfiguration + .getTruststorePath() + .ifPresent( + truststorePath -> + httpServerOptions.setTrustOptions( + new PfxOptions() + .setPath(truststorePath.toString()) + .setPassword(clientAuthConfiguration.getTrustStorePassword()))); } private String tlsLogMessage() { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsClientAuthConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsClientAuthConfiguration.java index f765405bcaf..885bc22e3da 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsClientAuthConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsClientAuthConfiguration.java @@ -17,15 +17,23 @@ import java.nio.file.Path; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; public class TlsClientAuthConfiguration { private final Optional knownClientsFile; private final boolean caClientsEnabled; + private final Optional truststorePath; + private final Supplier trustStorePasswordSupplier; private TlsClientAuthConfiguration( - final Optional knownClientsFile, final boolean caClientsEnabled) { + final Optional knownClientsFile, + final boolean caClientsEnabled, + final Optional truststorePath, + final Supplier trustStorePasswordSupplier) { this.knownClientsFile = knownClientsFile; this.caClientsEnabled = caClientsEnabled; + this.truststorePath = truststorePath; + this.trustStorePasswordSupplier = trustStorePasswordSupplier; } public Optional getKnownClientsFile() { @@ -36,9 +44,19 @@ public boolean isCaClientsEnabled() { return caClientsEnabled; } + public Optional getTruststorePath() { + return truststorePath; + } + + public String getTrustStorePassword() { + return trustStorePasswordSupplier.get(); + } + public static final class Builder { private Path knownClientsFile; private boolean caClientsEnabled; + private Path truststorePath; + private Supplier trustStorePasswordSupplier; private Builder() {} @@ -56,12 +74,29 @@ public Builder withCaClientsEnabled(final boolean caClientsEnabled) { return this; } + public Builder withTruststorePath(final Path truststorePath) { + this.truststorePath = truststorePath; + return this; + } + + public Builder withTruststorePasswordSupplier(final Supplier keyStorePasswordSupplier) { + this.trustStorePasswordSupplier = keyStorePasswordSupplier; + return this; + } + public TlsClientAuthConfiguration build() { - if (!caClientsEnabled) { + if (!caClientsEnabled && truststorePath == null) { Objects.requireNonNull(knownClientsFile, "Known Clients File is required"); } + if (!caClientsEnabled && knownClientsFile == null) { + Objects.requireNonNull(truststorePath, "Truststore File is required"); + } + return new TlsClientAuthConfiguration( - Optional.ofNullable(knownClientsFile), caClientsEnabled); + Optional.ofNullable(knownClientsFile), + caClientsEnabled, + Optional.ofNullable(truststorePath), + trustStorePasswordSupplier); } } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsClientAuthTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsClientAuthTest.java index d1348e5327e..c2b1f4e3285 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsClientAuthTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsClientAuthTest.java @@ -53,11 +53,13 @@ import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.nat.NatService; +import java.io.FileOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; +import java.security.KeyStore; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -187,6 +189,37 @@ private JsonRpcConfiguration createJsonRpcConfig( return config; } + private Optional getRpcHttpTlsConfigurationOnlyWithTruststore() { + final Path truststorePath = createTempFile(); + + // Create a new truststore and add the okHttpClientCertificate to it + try (FileOutputStream truststoreOutputStream = new FileOutputStream(truststorePath.toFile())) { + KeyStore truststore = KeyStore.getInstance("PKCS12"); + truststore.load(null, null); + truststore.setCertificateEntry( + "okHttpClientCertificate", okHttpClientCertificate.getCertificate()); + truststore.store(truststoreOutputStream, okHttpClientCertificate.getPassword()); + } catch (Exception e) { + throw new RuntimeException("Failed to create truststore", e); + } + + final FileBasedPasswordProvider trustStorePasswordProvider = + new FileBasedPasswordProvider(createPasswordFile(okHttpClientCertificate)); + + final TlsConfiguration tlsConfiguration = + aTlsConfiguration() + .withKeyStorePath(besuCertificate.getKeyStoreFile()) + .withKeyStorePasswordSupplier(fileBasedPasswordProvider) + .withClientAuthConfiguration( + aTlsClientAuthConfiguration() + .withTruststorePath(truststorePath) + .withTruststorePasswordSupplier(trustStorePasswordProvider) + .build()) + .build(); + + return Optional.of(tlsConfiguration); + } + private Optional getRpcHttpTlsConfiguration() { final Path knownClientsFile = createTempFile(); writeToKnownClientsFile( @@ -260,6 +293,23 @@ public void netVersionSuccessfulOnTlsWithClientCertInKnownClientsFile() throws E netVersionSuccessful(this::getTlsHttpClient, baseUrl); } + @Test + public void netVersionSuccessfulOnTlsWithClientCertInTruststore() throws Exception { + + JsonRpcHttpService jsonRpcHttpService = null; + try { + jsonRpcHttpService = + createJsonRpcHttpService( + createJsonRpcConfig(this::getRpcHttpTlsConfigurationOnlyWithTruststore)); + jsonRpcHttpService.start().join(); + netVersionSuccessful(this::getTlsHttpClient, jsonRpcHttpService.url()); + } finally { + if (jsonRpcHttpService != null) { + jsonRpcHttpService.stop().join(); + } + } + } + @Test public void netVersionSuccessfulOnTlsWithClientCertAddedAsCA() throws Exception { netVersionSuccessful(this::getTlsHttpClientAddedAsCA, baseUrl);