Add RPC HTTP options to specify custom truststore and password (#7978)

* Add RPC HTTP options to specify custom truststore and it's password

* Update error logs to indicate options to use

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

---------

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>
Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com>
Co-authored-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>
Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
This commit is contained in:
Bhanu Pulluri
2024-12-17 21:45:01 -05:00
committed by GitHub
parent 49ed3ce48b
commit 43c8a6a89b
7 changed files with 252 additions and 17 deletions

View File

@@ -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)

View File

@@ -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 class JsonRpcHttpOptions {
jsonRpcConfiguration.setHost(
Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost);
jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist);
;
jsonRpcConfiguration.setHttpTimeoutSec(timoutSec);
return jsonRpcConfiguration;
}
@@ -330,7 +343,18 @@ public class JsonRpcHttpOptions {
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 @@ public class JsonRpcHttpOptions {
"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 @@ public class JsonRpcHttpOptions {
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;

View File

@@ -332,7 +332,10 @@ public class JsonRpcHttpOptionsTest extends CommandTestAbstract {
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 class JsonRpcHttpOptionsTest extends CommandTestAbstract {
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 class JsonRpcHttpOptionsTest extends CommandTestAbstract {
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> 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";

View File

@@ -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"]

View File

@@ -429,7 +429,7 @@ public class JsonRpcHttpService {
try {
httpServerOptions
.setSsl(true)
.setPfxKeyCertOptions(
.setKeyCertOptions(
new PfxOptions()
.setPath(tlsConfiguration.getKeyStorePath().toString())
.setPassword(tlsConfiguration.getKeyStorePassword()))
@@ -472,6 +472,14 @@ public class JsonRpcHttpService {
httpServerOptions.setTrustOptions(
allowlistClients(
knownClientsFile, clientAuthConfiguration.isCaClientsEnabled())));
clientAuthConfiguration
.getTruststorePath()
.ifPresent(
truststorePath ->
httpServerOptions.setTrustOptions(
new PfxOptions()
.setPath(truststorePath.toString())
.setPassword(clientAuthConfiguration.getTrustStorePassword())));
}
private String tlsLogMessage() {

View File

@@ -17,15 +17,23 @@ package org.hyperledger.besu.ethereum.api.tls;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
public class TlsClientAuthConfiguration {
private final Optional<Path> knownClientsFile;
private final boolean caClientsEnabled;
private final Optional<Path> truststorePath;
private final Supplier<String> trustStorePasswordSupplier;
private TlsClientAuthConfiguration(
final Optional<Path> knownClientsFile, final boolean caClientsEnabled) {
final Optional<Path> knownClientsFile,
final boolean caClientsEnabled,
final Optional<Path> truststorePath,
final Supplier<String> trustStorePasswordSupplier) {
this.knownClientsFile = knownClientsFile;
this.caClientsEnabled = caClientsEnabled;
this.truststorePath = truststorePath;
this.trustStorePasswordSupplier = trustStorePasswordSupplier;
}
public Optional<Path> getKnownClientsFile() {
@@ -36,9 +44,19 @@ public class TlsClientAuthConfiguration {
return caClientsEnabled;
}
public Optional<Path> 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<String> trustStorePasswordSupplier;
private Builder() {}
@@ -56,12 +74,29 @@ public class TlsClientAuthConfiguration {
return this;
}
public Builder withTruststorePath(final Path truststorePath) {
this.truststorePath = truststorePath;
return this;
}
public Builder withTruststorePasswordSupplier(final Supplier<String> 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);
}
}
}

View File

@@ -53,11 +53,13 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
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 @@ public class JsonRpcHttpServiceTlsClientAuthTest {
return config;
}
private Optional<TlsConfiguration> 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<TlsConfiguration> getRpcHttpTlsConfiguration() {
final Path knownClientsFile = createTempFile();
writeToKnownClientsFile(
@@ -260,6 +293,23 @@ public class JsonRpcHttpServiceTlsClientAuthTest {
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);