Skip to content

Commit

Permalink
[BESU-185] - CLI option to enable TLS client auth for JSON-RPC HTTP (#…
Browse files Browse the repository at this point in the history
…340)

Following cli options are added to enable TLS client authentication and trusting client certificates.

--rpc-http-tls-client-auth-enabled - Enable TLS client authentication for the JSON-RPC HTTP service (default: false)
--rpc-http-tls-known-clients-file - Path to file containing client's certificate common name and fingerprint for client authentication.
--rpc-http-tls-ca-clients-enabled - Enable to accept clients certificate signed by a valid CA for client authentication (default: false)

If client-auth is enabled, then user must either enable CA signed clients OR provide a known-clients file. An error is reported if both CA signed clients is disabled and known-clients file is not specified.

Signed-off-by: Usman Saleem <usman@usmans.info>
  • Loading branch information
usmansaleem authored Jan 30, 2020
1 parent 570299c commit eca91a9
Show file tree
Hide file tree
Showing 10 changed files with 862 additions and 159 deletions.
114 changes: 86 additions & 28 deletions besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
import org.hyperledger.besu.ethereum.api.tls.TlsClientAuthConfiguration;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash;
Expand Down Expand Up @@ -468,13 +469,25 @@ void setBannedNodeIds(final List<String> values) {
"File containing password to unlock keystore for the JSON-RPC HTTP service. Required if TLS is enabled.")
private final Path rpcHttpTlsKeyStorePasswordFile = null;

@Option(
names = {"--rpc-http-tls-client-auth-enabled"},
description =
"Enable TLS client authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcHttpTlsClientAuthEnabled = false;

@Option(
names = {"--rpc-http-tls-known-clients-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"Require clients to present known or CA-signed certificates. File must contain common name and fingerprint if certificate is not CA-signed")
"Path to file containing clients certificate common name and fingerprint for client authentication")
private final Path rpcHttpTlsKnownClientsFile = null;

@Option(
names = {"--rpc-http-tls-ca-clients-enabled"},
description =
"Enable to accept clients certificate signed by a valid CA for client authentication (default: ${DEFAULT-VALUE})")
private final Boolean isRpcHttpTlsCAClientsEnabled = false;

@Option(
names = {"--rpc-ws-enabled"},
description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
Expand Down Expand Up @@ -1183,15 +1196,8 @@ private GraphQLConfiguration graphQLConfiguration() {
}

private JsonRpcConfiguration jsonRpcConfiguration() {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-http-tls-enabled",
!isRpcHttpTlsEnabled,
asList(
"--rpc-http-tls-keystore-file",
"--rpc-http-tls-keystore-password-file",
"--rpc-http-tls-known-clients-file"));
checkRpcTlsClientAuthOptionsDependencies();
checkRpcTlsOptionsDependencies();

CommandLineUtils.checkOptionDependencies(
logger,
Expand All @@ -1210,7 +1216,9 @@ private JsonRpcConfiguration jsonRpcConfiguration() {
"--rpc-http-tls-enabled",
"--rpc-http-tls-keystore-file",
"--rpc-http-tls-keystore-password-file",
"--rpc-http-tls-known-clients-file"));
"--rpc-http-tls-client-auth-enabled",
"--rpc-http-tls-known-clients-file",
"--rpc-http-tls-ca-clients-enabled"));

if (isRpcHttpAuthenticationEnabled
&& rpcHttpAuthenticationCredentialsFile() == null
Expand All @@ -1234,27 +1242,77 @@ && rpcHttpAuthenticationPublicKeyFile() == null) {
return jsonRpcConfiguration;
}

private TlsConfiguration rpcHttpTlsConfiguration() {
if (isRpcHttpEnabled && isRpcHttpTlsEnabled) {
return new TlsConfiguration(
Optional.ofNullable(rpcHttpTlsKeyStoreFile)
.orElseThrow(
() ->
new ParameterException(
commandLine,
"Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint")),
new FileBasedPasswordProvider(
Optional.ofNullable(rpcHttpTlsKeyStorePasswordFile)
.orElseThrow(
() ->
new ParameterException(
commandLine,
"File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"))),
rpcHttpTlsKnownClientsFile);
private void checkRpcTlsOptionsDependencies() {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-http-tls-enabled",
!isRpcHttpTlsEnabled,
asList(
"--rpc-http-tls-keystore-file",
"--rpc-http-tls-keystore-password-file",
"--rpc-http-tls-client-auth-enabled",
"--rpc-http-tls-known-clients-file",
"--rpc-http-tls-ca-clients-enabled"));
}

private void checkRpcTlsClientAuthOptionsDependencies() {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-http-tls-client-auth-enabled",
!isRpcHttpTlsClientAuthEnabled,
asList("--rpc-http-tls-known-clients-file", "--rpc-http-tls-ca-clients-enabled"));
}

private Optional<TlsConfiguration> rpcHttpTlsConfiguration() {
if (!isRpcTlsConfigurationRequired()) {
return Optional.empty();
}

if (rpcHttpTlsKeyStoreFile == null) {
throw new ParameterException(
commandLine, "Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint");
}

if (rpcHttpTlsKeyStorePasswordFile == null) {
throw new ParameterException(
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 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");
}

return Optional.of(
TlsConfiguration.Builder.aTlsConfiguration()
.withKeyStorePath(rpcHttpTlsKeyStoreFile)
.withKeyStorePasswordSupplier(
new FileBasedPasswordProvider(rpcHttpTlsKeyStorePasswordFile))
.withClientAuthConfiguration(rpcHttpTlsClientAuthConfiguration())
.build());
}

private TlsClientAuthConfiguration rpcHttpTlsClientAuthConfiguration() {
if (isRpcHttpTlsClientAuthEnabled) {
return TlsClientAuthConfiguration.Builder.aTlsClientAuthConfiguration()
.withKnownClientsFile(rpcHttpTlsKnownClientsFile)
.withCaClientsEnabled(isRpcHttpTlsCAClientsEnabled)
.build();
}

return null;
}

private boolean isRpcTlsConfigurationRequired() {
return isRpcHttpEnabled && isRpcHttpTlsEnabled;
}

private WebSocketConfiguration webSocketConfiguration() {

CommandLineUtils.checkOptionDependencies(
Expand Down
222 changes: 222 additions & 0 deletions besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.MiningParameters;
Expand Down Expand Up @@ -1601,6 +1602,227 @@ public void rpcHttpHostMayBeIPv6() {
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void rpcHttpTlsRequiresRpcHttpEnabled() {
parseCommand("--rpc-http-tls-enabled");

verifyOptionsConstraintLoggerCall("--rpc-http-enabled", "--rpc-http-tls-enabled");

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void rpcHttpTlsWithoutKeystoreReportsError() {
parseCommand("--rpc-http-enabled", "--rpc-http-tls-enabled");

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint");
}

@Test
public void rpcHttpTlsWithoutPasswordfileReportsError() {
parseCommand(
"--rpc-http-enabled",
"--rpc-http-tls-enabled",
"--rpc-http-tls-keystore-file",
"/tmp/test.p12");

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains(
"File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint");
}

@Test
public void rpcHttpTlsKeystoreAndPasswordMustBeUsed() {
final String host = "1.2.3.4";
final int port = 1234;
final String keystoreFile = "/tmp/test.p12";
final String keystorePasswordFile = "/tmp/test.txt";
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);

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().isEmpty()).isTrue();

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void rpcHttpTlsClientAuthWithoutKnownFileReportsError() {
final String host = "1.2.3.4";
final int port = 1234;
final String keystoreFile = "/tmp/test.p12";
final String keystorePasswordFile = "/tmp/test.txt";
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");

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains(
"Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint");
}

@Test
public void rpcHttpTlsClientAuthWithKnownClientFile() {
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 knownClientFile = "/tmp/knownClientFile";
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-known-clients-file",
knownClientFile);

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().getKnownClientsFile().get())
.isEqualTo(Path.of(knownClientFile));
assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled())
.isFalse();

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void rpcHttpTlsClientAuthWithCAClient() {
final String host = "1.2.3.4";
final int port = 1234;
final String keystoreFile = "/tmp/test.p12";
final String keystorePasswordFile = "/tmp/test.txt";
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-ca-clients-enabled");

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()
.getKnownClientsFile()
.isEmpty())
.isTrue();
assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled())
.isTrue();

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void rpcHttpTlsClientAuthWithCAClientAndKnownClientFile() {
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 knownClientFile = "/tmp/knownClientFile";
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-ca-clients-enabled",
"--rpc-http-tls-known-clients-file",
knownClientFile);

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().getKnownClientsFile().get())
.isEqualTo(Path.of(knownClientFile));
assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled())
.isTrue();

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void graphQLHttpHostAndPortOptionsMustBeUsed() {

Expand Down
Loading

0 comments on commit eca91a9

Please sign in to comment.