[BESU-77] - Enable TLS for JSON-RPC HTTP Service (#271)

Expose following new command line parameters to enable TLS on Ethereum JSON-RPC HTTP interface to allow clients like Ethsigner to connect via TLS

--rpc-http-tls-enabled=true
(Optional - Only required if --rpc-http-enabled is set to true) Set to ‘true’ to enable TLS. false by default.

--rpc-http-tls-keystore-file="/path/to/cert.pfx"
(Must be specified if TLS is enabled) Path to PKCS12 format key store which contains server's certificate and it's private key

--rpc-http-tls-keystore-password-file="/path/to/cert.passwd"
(Must be specified if TLS is enabled) Path to the text file containing password for unlocking key store.

--rpc-http-tls-known-clients-file="/path/to/rpc_tls_clients.txt"
(Optional) Path to a plain text file containing space separated client’s certificate’s common name and its sha-256 fingerprints when they are not signed by a known CA. The presence of this file (even empty) will enable TLS client authentication i.e. the client will present its certificate to server on TLS handshake and server will establish that the client’s certificate is either signed by a proper/known CA otherwise server trusts client's certificate by reading it's sha-256 fingerprint from known clients file specified above. The format of the file is (as an example):

localhost DF:65:B8:02:08:5E:91:82:0F:91:F5:1C:96:56:92:C4:1A:F6:C6:27:FD:6C:FC:31:F2:BB:90:17:22:59:5B:50

Signed-off-by: Usman Saleem <usman@usmans.info>
pull/302/head
Usman Saleem 5 years ago committed by GitHub
parent a7cde4dd8e
commit cb56b3c745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  2. 4
      besu/src/test/resources/everything_config.toml
  3. 1
      ethereum/api/build.gradle
  4. 12
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
  5. 147
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java
  6. 48
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/FileBasedPasswordProvider.java
  7. 51
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsConfiguration.java
  8. 26
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/tls/TlsConfigurationException.java
  9. 8
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfigurationTest.java
  10. 260
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsMisconfigurationTest.java
  11. 274
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTlsTest.java
  12. 144
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/TlsHttpClient.java
  13. 64
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/tls/FileBasedPasswordProviderTest.java
  14. BIN
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_client_2.pfx
  15. BIN
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_client_keystore.pfx
  16. 1
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_keystore.password
  17. BIN
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_keystore.pfx
  18. 16
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_keystore.readme
  19. 3
      ethereum/api/src/test/resources/JsonRpcHttpService/rpc_known_clients.txt
  20. 1
      gradle/versions.gradle

@ -71,6 +71,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash; import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.MiningParameters; import org.hyperledger.besu.ethereum.core.MiningParameters;
@ -447,6 +449,32 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
"Require authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") "Require authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcHttpAuthenticationEnabled = false; private final Boolean isRpcHttpAuthenticationEnabled = false;
@Option(
names = {"--rpc-http-tls-enabled"},
description = "Enable TLS for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcHttpTlsEnabled = false;
@Option(
names = {"--rpc-http-tls-keystore-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"Keystore (PKCS#12) containing key/certificate for the JSON-RPC HTTP service. Required if TLS is enabled.")
private final Path rpcHttpTlsKeyStoreFile = null;
@Option(
names = {"--rpc-http-tls-keystore-password-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"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-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")
private final Path rpcHttpTlsKnownClientsFile = null;
@Option( @Option(
names = {"--rpc-ws-enabled"}, names = {"--rpc-ws-enabled"},
description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})") description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
@ -1153,6 +1181,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
} }
private JsonRpcConfiguration jsonRpcConfiguration() { 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"));
CommandLineUtils.checkOptionDependencies( CommandLineUtils.checkOptionDependencies(
logger, logger,
@ -1167,7 +1204,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
"--rpc-http-port", "--rpc-http-port",
"--rpc-http-authentication-enabled", "--rpc-http-authentication-enabled",
"--rpc-http-authentication-credentials-file", "--rpc-http-authentication-credentials-file",
"--rpc-http-authentication-public-key-file")); "--rpc-http-authentication-public-key-file",
"--rpc-http-tls-enabled",
"--rpc-http-tls-keystore-file",
"--rpc-http-tls-keystore-password-file",
"--rpc-http-tls-known-clients-file"));
if (isRpcHttpAuthenticationEnabled if (isRpcHttpAuthenticationEnabled
&& rpcHttpAuthenticationCredentialsFile() == null && rpcHttpAuthenticationCredentialsFile() == null
@ -1187,9 +1228,31 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled); jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled);
jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile()); jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile());
jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile()); jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile());
jsonRpcConfiguration.setTlsConfiguration(rpcHttpTlsConfiguration());
return jsonRpcConfiguration; 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);
}
return null;
}
private WebSocketConfiguration webSocketConfiguration() { private WebSocketConfiguration webSocketConfiguration() {
CommandLineUtils.checkOptionDependencies( CommandLineUtils.checkOptionDependencies(

@ -51,6 +51,10 @@ rpc-http-cors-origins=["none"]
rpc-http-authentication-enabled=false rpc-http-authentication-enabled=false
rpc-http-authentication-credentials-file="none" rpc-http-authentication-credentials-file="none"
rpc-http-authentication-jwt-public-key-file="none" rpc-http-authentication-jwt-public-key-file="none"
rpc-http-tls-enabled=false
rpc-http-tls-keystore-file="none.pfx"
rpc-http-tls-keystore-password-file="none.passwd"
rpc-http-tls-known-clients-file="rpc_tls_clients.txt"
# GRAPHQL HTTP # GRAPHQL HTTP
graphql-http-enabled=false graphql-http-enabled=false

@ -49,6 +49,7 @@ dependencies {
implementation 'io.vertx:vertx-unit' implementation 'io.vertx:vertx-unit'
implementation 'io.vertx:vertx-web' implementation 'io.vertx:vertx-web'
implementation 'org.apache.tuweni:tuweni-bytes' implementation 'org.apache.tuweni:tuweni-bytes'
implementation 'org.apache.tuweni:tuweni-net'
implementation 'org.apache.tuweni:tuweni-toml' implementation 'org.apache.tuweni:tuweni-toml'
implementation 'org.apache.tuweni:tuweni-units' implementation 'org.apache.tuweni:tuweni-units'
implementation 'org.bouncycastle:bcprov-jdk15on' implementation 'org.bouncycastle:bcprov-jdk15on'

@ -14,6 +14,8 @@
*/ */
package org.hyperledger.besu.ethereum.api.jsonrpc; package org.hyperledger.besu.ethereum.api.jsonrpc;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -21,6 +23,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
@ -37,6 +40,7 @@ public class JsonRpcConfiguration {
private boolean authenticationEnabled = false; private boolean authenticationEnabled = false;
private String authenticationCredentialsFile; private String authenticationCredentialsFile;
private File authenticationPublicKeyFile; private File authenticationPublicKeyFile;
private TlsConfiguration tlsConfiguration;
public static JsonRpcConfiguration createDefault() { public static JsonRpcConfiguration createDefault() {
final JsonRpcConfiguration config = new JsonRpcConfiguration(); final JsonRpcConfiguration config = new JsonRpcConfiguration();
@ -128,6 +132,14 @@ public class JsonRpcConfiguration {
this.authenticationPublicKeyFile = authenticationPublicKeyFile; this.authenticationPublicKeyFile = authenticationPublicKeyFile;
} }
public Optional<TlsConfiguration> getTlsConfiguration() {
return Optional.ofNullable(tlsConfiguration);
}
public void setTlsConfiguration(final TlsConfiguration tlsConfiguration) {
this.tlsConfiguration = tlsConfiguration;
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)

@ -41,6 +41,7 @@ import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer; import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import org.hyperledger.besu.util.ExceptionUtils;
import org.hyperledger.besu.util.NetworkUtility; import org.hyperledger.besu.util.NetworkUtility;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -60,6 +61,8 @@ import io.vertx.core.CompositeFuture;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Handler; import io.vertx.core.Handler;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerOptions;
@ -68,6 +71,7 @@ import io.vertx.core.json.DecodeException;
import io.vertx.core.json.Json; import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PfxOptions;
import io.vertx.ext.auth.User; import io.vertx.ext.auth.User;
import io.vertx.ext.web.Router; import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
@ -75,6 +79,7 @@ import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler; import io.vertx.ext.web.handler.CorsHandler;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.tuweni.net.tls.VertxTrustOptions;
public class JsonRpcHttpService { public class JsonRpcHttpService {
@ -168,14 +173,53 @@ public class JsonRpcHttpService {
public CompletableFuture<?> start() { public CompletableFuture<?> start() {
LOG.info("Starting JsonRPC service on {}:{}", config.getHost(), config.getPort()); LOG.info("Starting JsonRPC service on {}:{}", config.getHost(), config.getPort());
// Create the HTTP server and a router object. final CompletableFuture<?> resultFuture = new CompletableFuture<>();
httpServer = try {
vertx.createHttpServer( // Create the HTTP server and a router object.
new HttpServerOptions() httpServer = vertx.createHttpServer(getHttpServerOptions());
.setHost(config.getHost()) httpServer
.setPort(config.getPort()) .requestHandler(buildRouter())
.setHandle100ContinueAutomatically(true)); .listen(
res -> {
if (!res.failed()) {
resultFuture.complete(null);
config.setPort(httpServer.actualPort());
LOG.info(
"JsonRPC service started and listening on {}:{}{}",
config.getHost(),
config.getPort(),
tlsLogMessage());
natService.ifNatEnvironment(
NatMethod.UPNP,
natManager -> {
((UpnpNatManager) natManager)
.requestPortForward(
config.getPort(), NetworkProtocol.TCP, NatServiceType.JSON_RPC);
});
return;
}
httpServer = null;
resultFuture.completeExceptionally(getFailureException(res.cause()));
});
} catch (final JsonRpcServiceException tlsException) {
httpServer = null;
resultFuture.completeExceptionally(tlsException);
} catch (final VertxException listenException) {
httpServer = null;
resultFuture.completeExceptionally(
new JsonRpcServiceException(
String.format(
"Ethereum JSON RPC listener failed to start: %s",
ExceptionUtils.rootCause(listenException).getMessage())));
}
return resultFuture;
}
private Router buildRouter() {
// Handle json rpc requests // Handle json rpc requests
final Router router = Router.router(vertx); final Router router = Router.router(vertx);
@ -222,43 +266,62 @@ public class JsonRpcHttpService {
.produces(APPLICATION_JSON) .produces(APPLICATION_JSON)
.handler(AuthenticationService::handleDisabledLogin); .handler(AuthenticationService::handleDisabledLogin);
} }
return router;
}
final CompletableFuture<?> resultFuture = new CompletableFuture<>(); private HttpServerOptions getHttpServerOptions() {
httpServer final HttpServerOptions httpServerOptions =
.requestHandler(router) new HttpServerOptions()
.listen( .setHost(config.getHost())
res -> { .setPort(config.getPort())
if (!res.failed()) { .setHandle100ContinueAutomatically(true);
resultFuture.complete(null);
final int actualPort = httpServer.actualPort();
LOG.info(
"JsonRPC service started and listening on {}:{}", config.getHost(), actualPort);
config.setPort(actualPort);
natService.ifNatEnvironment(
NatMethod.UPNP,
natManager -> {
((UpnpNatManager) natManager)
.requestPortForward(
config.getPort(), NetworkProtocol.TCP, NatServiceType.JSON_RPC);
});
return; return applyTlsConfig(httpServerOptions);
} }
httpServer = null;
final Throwable cause = res.cause(); private HttpServerOptions applyTlsConfig(final HttpServerOptions httpServerOptions) {
if (cause instanceof SocketException) { config
resultFuture.completeExceptionally( .getTlsConfiguration()
new JsonRpcServiceException( .ifPresent(
String.format( tlsConfiguration -> {
"Failed to bind Ethereum JSON RPC listener to %s:%s: %s", try {
config.getHost(), config.getPort(), cause.getMessage()))); httpServerOptions
return; .setSsl(true)
.setPfxKeyCertOptions(
new PfxOptions()
.setPath(tlsConfiguration.getKeyStorePath().toString())
.setPassword(tlsConfiguration.getKeyStorePassword()));
tlsConfiguration
.getKnownClientsFile()
.ifPresent(
knownClientsFile ->
httpServerOptions
.setClientAuth(ClientAuth.REQUIRED)
.setTrustOptions(
VertxTrustOptions.whitelistClients(knownClientsFile)));
} catch (final RuntimeException re) {
throw new JsonRpcServiceException(
String.format(
"TLS options failed to initialise for Ethereum JSON RPC listener: %s",
re.getMessage()));
} }
resultFuture.completeExceptionally(cause);
}); });
return httpServerOptions;
}
return resultFuture; private String tlsLogMessage() {
return config.getTlsConfiguration().isPresent() ? " with TLS enabled." : "";
}
private Throwable getFailureException(final Throwable listenFailure) {
if (listenFailure instanceof SocketException) {
return new JsonRpcServiceException(
String.format(
"Failed to bind Ethereum JSON RPC listener to %s:%s: %s",
config.getHost(), config.getPort(), listenFailure.getMessage()));
}
return listenFailure;
} }
private Handler<RoutingContext> checkWhitelistHostHeader() { private Handler<RoutingContext> checkWhitelistHostHeader() {
@ -329,7 +392,11 @@ public class JsonRpcHttpService {
if (httpServer == null) { if (httpServer == null) {
return ""; return "";
} }
return NetworkUtility.urlForSocketAddress("http", socketAddress()); return NetworkUtility.urlForSocketAddress(getScheme(), socketAddress());
}
private String getScheme() {
return config.getTlsConfiguration().isPresent() ? "https" : "http";
} }
private void handleJsonRPCRequest(final RoutingContext routingContext) { private void handleJsonRPCRequest(final RoutingContext routingContext) {

@ -0,0 +1,48 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.tls;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class FileBasedPasswordProvider implements Supplier<String> {
private final Path passwordFile;
public FileBasedPasswordProvider(final Path passwordFile) {
requireNonNull(passwordFile, "Password file path cannot be null");
this.passwordFile = passwordFile;
}
@Override
public String get() {
try (final Stream<String> fileStream = Files.lines(passwordFile)) {
return fileStream
.findFirst()
.orElseThrow(
() ->
new TlsConfigurationException(
String.format(
"Unable to read keystore password from %s", passwordFile.toString())));
} catch (final IOException e) {
throw new TlsConfigurationException(
String.format("Unable to read keystore password file %s", passwordFile.toString()), e);
}
}
}

@ -0,0 +1,51 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.tls;
import static java.util.Objects.requireNonNull;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
public class TlsConfiguration {
private final Path keyStorePath;
private final Supplier<String> keyStorePasswordSupplier;
private final Path knownClientsFile;
public TlsConfiguration(
final Path keyStorePath,
final Supplier<String> keyStorePasswordSupplier,
final Path knownClientsFile) {
requireNonNull(keyStorePath, "Key Store Path must not be null");
requireNonNull(keyStorePasswordSupplier, "Key Store password supplier must not be null");
this.keyStorePath = keyStorePath;
this.keyStorePasswordSupplier = keyStorePasswordSupplier;
this.knownClientsFile = knownClientsFile;
}
public Path getKeyStorePath() {
return keyStorePath;
}
public String getKeyStorePassword() {
return keyStorePasswordSupplier.get();
}
public Optional<Path> getKnownClientsFile() {
return Optional.ofNullable(knownClientsFile);
}
}

@ -0,0 +1,26 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.tls;
public class TlsConfigurationException extends RuntimeException {
public TlsConfigurationException(final String message) {
super(message);
}
public TlsConfigurationException(final String message, final Throwable cause) {
super(message, cause);
}
}

@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.api.jsonrpc;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.junit.Test; import org.junit.Test;
@ -66,4 +68,10 @@ public class JsonRpcConfigurationTest {
configuration.setRpcApis(Lists.newArrayList(RpcApis.DEBUG)); configuration.setRpcApis(Lists.newArrayList(RpcApis.DEBUG));
assertThat(configuration.getRpcApis()).containsExactly(RpcApis.DEBUG); assertThat(configuration.getRpcApis()).containsExactly(RpcApis.DEBUG);
} }
@Test
public void tlsConfigurationDefaultShouldBeEmpty() {
final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault();
assertThat(configuration.getTlsConfiguration()).isEqualTo(Optional.empty());
}
} }

@ -0,0 +1,260 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;
import static com.google.common.io.Resources.getResource;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import org.hyperledger.besu.config.StubGenesisConfigOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManager;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethodsFactory;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.blockcreation.EthHashMiningCoordinator;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioningController;
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionException;
import io.vertx.core.Vertx;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class JsonRpcHttpServiceTlsMisconfigurationTest {
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder();
protected static final Vertx vertx = Vertx.vertx();
private static final String CLIENT_VERSION = "TestClientVersion/0.1.0";
private static final BigInteger CHAIN_ID = BigInteger.valueOf(123);
private static final Collection<RpcApi> JSON_RPC_APIS = List.of(ETH, NET, WEB3);
private static final String KEYSTORE_RESOURCE = "JsonRpcHttpService/rpc_keystore.pfx";
private static final String KNOWN_CLIENTS_RESOURCE = "JsonRpcHttpService/rpc_known_clients.txt";
private static final NatService natService = new NatService(Optional.empty());
private Map<String, JsonRpcMethod> rpcMethods;
private JsonRpcHttpService service;
@Before
public void beforeEach() {
final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class);
final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class);
final Synchronizer synchronizer = mock(Synchronizer.class);
final Set<Capability> supportedCapabilities = new HashSet<>();
supportedCapabilities.add(EthProtocol.ETH62);
supportedCapabilities.add(EthProtocol.ETH63);
rpcMethods =
spy(
new JsonRpcMethodsFactory()
.methods(
CLIENT_VERSION,
CHAIN_ID,
new StubGenesisConfigOptions(),
peerDiscoveryMock,
blockchainQueries,
synchronizer,
MainnetProtocolSchedule.fromConfig(
new StubGenesisConfigOptions().constantinopleBlock(0).chainId(CHAIN_ID)),
mock(FilterManager.class),
mock(TransactionPool.class),
mock(EthHashMiningCoordinator.class),
new NoOpMetricsSystem(),
supportedCapabilities,
Optional.of(mock(AccountLocalConfigPermissioningController.class)),
Optional.of(mock(NodeLocalConfigPermissioningController.class)),
JSON_RPC_APIS,
mock(PrivacyParameters.class),
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
Collections.emptyMap()));
}
@After
public void shutdownServer() {
Optional.ofNullable(service).ifPresent(s -> service.stop().join());
}
@Test
public void exceptionRaisedWhenNonExistentKeystoreFileIsSpecified() throws IOException {
service =
createJsonRpcHttpService(
rpcMethods, createJsonRpcConfig(invalidKeystorePathTlsConfiguration()));
assertThatExceptionOfType(CompletionException.class)
.isThrownBy(
() -> {
service.start().join();
Assertions.fail("service.start should have failed");
})
.withCauseInstanceOf(JsonRpcServiceException.class);
}
@Test
public void exceptionRaisedWhenIncorrectKeystorePasswordIsSpecified() throws IOException {
service =
createJsonRpcHttpService(
rpcMethods, createJsonRpcConfig(invalidPasswordTlsConfiguration()));
assertThatExceptionOfType(CompletionException.class)
.isThrownBy(
() -> {
service.start().join();
Assertions.fail("service.start should have failed");
})
.withCauseInstanceOf(JsonRpcServiceException.class)
.withMessageContaining("failed to decrypt safe contents entry");
}
@Test
public void exceptionRaisedWhenIncorrectKeystorePasswordFileIsSpecified() throws IOException {
service =
createJsonRpcHttpService(
rpcMethods, createJsonRpcConfig(invalidPasswordFileTlsConfiguration()));
assertThatExceptionOfType(CompletionException.class)
.isThrownBy(
() -> {
service.start().join();
Assertions.fail("service.start should have failed");
})
.withCauseInstanceOf(JsonRpcServiceException.class)
.withMessageContaining("Unable to read keystore password file");
}
@Test
public void exceptionRaisedWhenInvalidKeystoreFileIsSpecified() throws IOException {
service =
createJsonRpcHttpService(
rpcMethods, createJsonRpcConfig(invalidKeystoreFileTlsConfiguration()));
assertThatExceptionOfType(CompletionException.class)
.isThrownBy(
() -> {
service.start().join();
Assertions.fail("service.start should have failed");
})
.withCauseInstanceOf(JsonRpcServiceException.class)
.withMessageContaining("Short read of DER length");
}
@Test
public void exceptionRaisedWhenInvalidKnownClientsFileIsSpecified() throws IOException {
service =
createJsonRpcHttpService(
rpcMethods, createJsonRpcConfig(invalidKnownClientsTlsConfiguration()));
assertThatExceptionOfType(CompletionException.class)
.isThrownBy(
() -> {
service.start().join();
Assertions.fail("service.start should have failed");
})
.withCauseInstanceOf(JsonRpcServiceException.class)
.withMessageContaining("Invalid fingerprint in");
}
private TlsConfiguration invalidKeystoreFileTlsConfiguration() throws IOException {
final File tempFile = folder.newFile();
return new TlsConfiguration(tempFile.toPath(), () -> "invalid_password", getKnownClientsFile());
}
private TlsConfiguration invalidKeystorePathTlsConfiguration() {
return new TlsConfiguration(
Path.of("/tmp/invalidkeystore.pfx"), () -> "invalid_password", getKnownClientsFile());
}
private TlsConfiguration invalidPasswordTlsConfiguration() {
return new TlsConfiguration(getKeyStorePath(), () -> "invalid_password", getKnownClientsFile());
}
private TlsConfiguration invalidPasswordFileTlsConfiguration() {
return new TlsConfiguration(
getKeyStorePath(),
new FileBasedPasswordProvider(Path.of("/tmp/invalid_password_file.txt")),
getKnownClientsFile());
}
private TlsConfiguration invalidKnownClientsTlsConfiguration() throws IOException {
final Path tempKnownClientsFile = folder.newFile().toPath();
Files.write(tempKnownClientsFile, List.of("cn invalid_sha256"));
return new TlsConfiguration(getKeyStorePath(), () -> "changeit", tempKnownClientsFile);
}
private JsonRpcHttpService createJsonRpcHttpService(
final Map<String, JsonRpcMethod> rpcMethods, final JsonRpcConfiguration jsonRpcConfig)
throws IOException {
return new JsonRpcHttpService(
vertx,
folder.newFolder().toPath(),
jsonRpcConfig,
new NoOpMetricsSystem(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);
}
private JsonRpcConfiguration createJsonRpcConfig(
final TlsConfiguration tlsConfigurationSupplier) {
final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault();
config.setPort(0);
config.setHostsWhitelist(Collections.singletonList("*"));
config.setTlsConfiguration(tlsConfigurationSupplier);
return config;
}
private static Path getKeyStorePath() {
return Paths.get(getResource(KEYSTORE_RESOURCE).getPath());
}
private static Path getKnownClientsFile() {
return Paths.get(getResource(KNOWN_CLIENTS_RESOURCE).getPath());
}
}

@ -0,0 +1,274 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;
import static com.google.common.io.Resources.getResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import org.hyperledger.besu.config.StubGenesisConfigOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManager;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethodsFactory;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import org.hyperledger.besu.ethereum.blockcreation.EthHashMiningCoordinator;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioningController;
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.math.BigInteger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import io.vertx.core.Vertx;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class JsonRpcHttpServiceTlsTest {
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder();
protected static final Vertx vertx = Vertx.vertx();
private static final String JSON_HEADER = "application/json; charset=utf-8";
private static final String CLIENT_VERSION = "TestClientVersion/0.1.0";
private static final BigInteger CHAIN_ID = BigInteger.valueOf(123);
private static final Collection<RpcApi> JSON_RPC_APIS = List.of(ETH, NET, WEB3);
private static final NatService natService = new NatService(Optional.empty());
private static final String ROOT_RESOURCE = "JsonRpcHttpService/";
private static final Path KEYSTORE_PATH =
Paths.get(getResource(ROOT_RESOURCE + "rpc_keystore.pfx").getPath());
private static final Path KEYSTORE_PASSWORD_FILE =
Paths.get(getResource(ROOT_RESOURCE + "rpc_keystore.password").getPath());
private static final Path KNOWN_CLIENTS_FILE =
Paths.get(getResource(ROOT_RESOURCE + "rpc_known_clients.txt").getPath());
private static final Path CLIENT_CERT_KEYSTORE_PATH =
Paths.get(getResource(ROOT_RESOURCE + "rpc_client_keystore.pfx").getPath());
private static final Path X_CLIENT_CERT_KEYSTORE_PATH =
Paths.get(getResource(ROOT_RESOURCE + "rpc_client_2.pfx").getPath());
private JsonRpcHttpService service;
private String baseUrl;
private Map<String, JsonRpcMethod> rpcMethods;
private final JsonRpcTestHelper testHelper = new JsonRpcTestHelper();
@Before
public void initServer() throws Exception {
final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class);
final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class);
final Synchronizer synchronizer = mock(Synchronizer.class);
final Set<Capability> supportedCapabilities = new HashSet<>();
supportedCapabilities.add(EthProtocol.ETH62);
supportedCapabilities.add(EthProtocol.ETH63);
rpcMethods =
spy(
new JsonRpcMethodsFactory()
.methods(
CLIENT_VERSION,
CHAIN_ID,
new StubGenesisConfigOptions(),
peerDiscoveryMock,
blockchainQueries,
synchronizer,
MainnetProtocolSchedule.fromConfig(
new StubGenesisConfigOptions().constantinopleBlock(0).chainId(CHAIN_ID)),
mock(FilterManager.class),
mock(TransactionPool.class),
mock(EthHashMiningCoordinator.class),
new NoOpMetricsSystem(),
supportedCapabilities,
Optional.of(mock(AccountLocalConfigPermissioningController.class)),
Optional.of(mock(NodeLocalConfigPermissioningController.class)),
JSON_RPC_APIS,
mock(PrivacyParameters.class),
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
Collections.emptyMap()));
service = createJsonRpcHttpService(createJsonRpcConfig());
service.start().join();
baseUrl = service.url();
}
private JsonRpcHttpService createJsonRpcHttpService(final JsonRpcConfiguration jsonRpcConfig)
throws Exception {
return new JsonRpcHttpService(
vertx,
folder.newFolder().toPath(),
jsonRpcConfig,
new NoOpMetricsSystem(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);
}
private JsonRpcConfiguration createJsonRpcConfig() {
final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault();
config.setPort(0);
config.setHostsWhitelist(Collections.singletonList("*"));
config.setTlsConfiguration(getRpcHttpTlsConfiguration());
return config;
}
private TlsConfiguration getRpcHttpTlsConfiguration() {
return new TlsConfiguration(
KEYSTORE_PATH, new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE), KNOWN_CLIENTS_FILE);
}
@After
public void shutdownServer() {
service.stop().join();
}
@Test
public void connectionFailsWhenTlsClientAuthIsNotProvided() {
final String id = "123";
final String json =
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}";
final OkHttpClient httpClient = getTlsHttpClientWithoutClientAuthentication();
assertThatIOException()
.isThrownBy(
() -> {
try (final Response response = httpClient.newCall(buildPostRequest(json)).execute()) {
Assertions.fail("Call should have failed. Got: " + response);
} catch (final Exception e) {
e.printStackTrace();
throw e;
}
});
}
@Test
public void connectionFailsWhenClientIsNotWhitelisted() {
final String id = "123";
final String json =
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}";
final OkHttpClient httpClient = getTlsHttpClientNotTrustedWithServer();
assertThatIOException()
.isThrownBy(
() -> {
try (final Response response = httpClient.newCall(buildPostRequest(json)).execute()) {
Assertions.fail("Call should have failed. Got: " + response);
} catch (final Exception e) {
e.printStackTrace();
throw e;
}
});
}
@Test
public void netVersionSuccessfulOnTls() throws Exception {
final String id = "123";
final String json =
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}";
final OkHttpClient httpClient = getTlsHttpClient();
try (final Response response = httpClient.newCall(buildPostRequest(json)).execute()) {
assertThat(response.code()).isEqualTo(200);
// Check general format of result
final ResponseBody body = response.body();
assertThat(body).isNotNull();
final JsonObject jsonObject = new JsonObject(body.string());
testHelper.assertValidJsonRpcResult(jsonObject, id);
// Check result
final String result = jsonObject.getString("result");
assertThat(result).isEqualTo(String.valueOf(CHAIN_ID));
} catch (final Exception e) {
e.printStackTrace();
throw e;
}
}
private OkHttpClient getTlsHttpClient() {
final TlsConfiguration serverTrustConfiguration =
new TlsConfiguration(
KEYSTORE_PATH, new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE), null);
final TlsConfiguration clientCertConfiguration =
new TlsConfiguration(
CLIENT_CERT_KEYSTORE_PATH, new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE), null);
return TlsHttpClient.fromServerTrustAndClientCertConfiguration(
serverTrustConfiguration, clientCertConfiguration)
.getHttpClient();
}
private OkHttpClient getTlsHttpClientNotTrustedWithServer() {
final TlsConfiguration serverTrustConfiguration =
new TlsConfiguration(
KEYSTORE_PATH, new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE), null);
final TlsConfiguration clientCertConfiguration =
new TlsConfiguration(
X_CLIENT_CERT_KEYSTORE_PATH,
new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE),
null);
return TlsHttpClient.fromServerTrustAndClientCertConfiguration(
serverTrustConfiguration, clientCertConfiguration)
.getHttpClient();
}
private OkHttpClient getTlsHttpClientWithoutClientAuthentication() {
final TlsConfiguration serverTrustConfiguration =
new TlsConfiguration(
KEYSTORE_PATH, new FileBasedPasswordProvider(KEYSTORE_PASSWORD_FILE), null);
return TlsHttpClient.fromServerTrustConfiguration(serverTrustConfiguration).getHttpClient();
}
private Request buildPostRequest(final String json) {
final RequestBody body = RequestBody.create(json, MediaType.parse(JSON_HEADER));
return new Request.Builder().post(body).url(baseUrl).build();
}
}

@ -0,0 +1,144 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;
import static org.hyperledger.besu.crypto.SecureRandomProvider.createSecureRandom;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
public class TlsHttpClient {
private final TlsConfiguration serverTrustConfiguration;
private final TlsConfiguration clientCertConfiguration;
private TrustManagerFactory trustManagerFactory;
private KeyManagerFactory keyManagerFactory;
private OkHttpClient client;
private TlsHttpClient(
final TlsConfiguration serverTrustConfiguration,
final TlsConfiguration clientCertConfiguration) {
this.serverTrustConfiguration = serverTrustConfiguration;
this.clientCertConfiguration = clientCertConfiguration;
}
public static TlsHttpClient fromServerTrustConfiguration(
final TlsConfiguration serverTrustConfiguration) {
final TlsHttpClient tlsHttpClient = new TlsHttpClient(serverTrustConfiguration, null);
tlsHttpClient.initHttpClient();
return tlsHttpClient;
}
public static TlsHttpClient fromServerTrustAndClientCertConfiguration(
final TlsConfiguration serverTrustConfiguration,
final TlsConfiguration clientCertConfiguration) {
final TlsHttpClient tlsHttpClient =
new TlsHttpClient(serverTrustConfiguration, clientCertConfiguration);
tlsHttpClient.initHttpClient();
return tlsHttpClient;
}
public OkHttpClient getHttpClient() {
return client;
}
private void initHttpClient() {
initTrustManagerFactory();
initKeyManagerFactory();
try {
client =
new OkHttpClient.Builder()
.sslSocketFactory(
getCustomSslContext().getSocketFactory(),
(X509TrustManager)
trustManagerFactory.getTrustManagers()[0]) // we only have one trust manager
.build();
} catch (final GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private void initTrustManagerFactory() {
try {
final KeyStore trustStore =
loadP12KeyStore(
serverTrustConfiguration.getKeyStorePath(),
serverTrustConfiguration.getKeyStorePassword().toCharArray());
final TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
this.trustManagerFactory = trustManagerFactory;
} catch (final GeneralSecurityException gse) {
throw new RuntimeException("Unable to load trust manager factory", gse);
}
}
private void initKeyManagerFactory() {
if (!isClientAuthRequired()) {
return;
}
try {
final char[] keyStorePassword = clientCertConfiguration.getKeyStorePassword().toCharArray();
final KeyStore keyStore =
loadP12KeyStore(clientCertConfiguration.getKeyStorePath(), keyStorePassword);
final KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
this.keyManagerFactory = keyManagerFactory;
} catch (final GeneralSecurityException gse) {
throw new RuntimeException("Unable to load key manager factory", gse);
}
}
private boolean isClientAuthRequired() {
return clientCertConfiguration != null;
}
private KeyStore loadP12KeyStore(final Path keyStore, final char[] password)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException {
final KeyStore store = KeyStore.getInstance("pkcs12");
try (final InputStream keystoreStream = Files.newInputStream(keyStore)) {
store.load(keystoreStream, password);
} catch (final IOException e) {
throw new RuntimeException("Unable to load keystore.", e);
}
return store;
}
private SSLContext getCustomSslContext() throws GeneralSecurityException {
final KeyManager[] km = isClientAuthRequired() ? keyManagerFactory.getKeyManagers() : null;
final TrustManager[] tm = trustManagerFactory.getTrustManagers();
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(km, tm, createSecureRandom());
return sslContext;
}
}

@ -0,0 +1,64 @@
/*
*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
package org.hyperledger.besu.ethereum.api.tls;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class FileBasedPasswordProviderTest {
@Rule public final TemporaryFolder folder = new TemporaryFolder();
@Test
public void passwordCanBeReadFromFile() throws IOException {
final Path passwordFile = folder.newFile().toPath();
Files.write(passwordFile, List.of("line1", "line2"));
final String password = new FileBasedPasswordProvider(passwordFile).get();
Assertions.assertThat(password).isEqualTo("line1");
}
@Test
public void exceptionRaisedFromReadingEmptyFile() throws IOException {
final Path passwordFile = folder.newFile().toPath();
Files.write(passwordFile, new byte[0]);
Assertions.assertThatExceptionOfType(TlsConfigurationException.class)
.isThrownBy(
() -> {
new FileBasedPasswordProvider(passwordFile).get();
})
.withMessageContaining("Unable to read keystore password from");
}
@Test
public void exceptionRaisedFromReadingNonExistingFile() throws IOException {
Assertions.assertThatExceptionOfType(TlsConfigurationException.class)
.isThrownBy(
() -> {
new FileBasedPasswordProvider(Path.of("/tmp/invalid_file.txt")).get();
})
.withMessageContaining("Unable to read keystore password file");
}
}

@ -0,0 +1,16 @@
The test key stores are created using following commands:
keytool -genkeypair -keystore rpc_keystore.pfx -storetype PKCS12 -storepass changeit -alias testrpcserver \
-keyalg RSA -keysize 2048 -validity 109500 -dname "CN=localhost, OU=PegaSys, O=ConsenSys, L=Brisbane, ST=QLD, C=AU" \
-ext san=dns:localhost,ip:127.0.0.1
keytool -genkeypair -keystore rpc_client_keystore.pfx -storetype PKCS12 -storepass changeit -alias testrpcclient \
-keyalg RSA -keysize 2048 -validity 109500 -dname "CN=localhost, OU=PegaSys, O=ConsenSys, L=Brisbane, ST=QLD, C=AU" \
-ext san=dns:localhost,ip:127.0.0.1
keytool -genkeypair -keystore rpc_client_2.pfx -storetype PKCS12 -storepass changeit -alias testrpcclient2 \
-keyalg RSA -keysize 2048 -validity 109500 -dname "CN=localhost, OU=PegaSys, O=ConsenSys, L=Brisbane, ST=QLD, C=AU" \
-ext san=dns:localhost,ip:127.0.0.1
The fingerprint is obtained using following command:
keytool -list -keystore rpc_client_keystore.pfx

@ -0,0 +1,3 @@
#client certificates fingerprint obtained from rpc_client_keystore.pfx
localhost DF:65:B8:02:08:5E:91:82:0F:91:F5:1C:96:56:92:C4:1A:F6:C6:27:FD:6C:FC:31:F2:BB:90:17:22:59:5B:50
127.0.0.1 DF:65:B8:02:08:5E:91:82:0F:91:F5:1C:96:56:92:C4:1A:F6:C6:27:FD:6C:FC:31:F2:BB:90:17:22:59:5B:50

@ -70,6 +70,7 @@ dependencyManagement {
dependency 'org.apache.tuweni:tuweni-io:0.10.0' dependency 'org.apache.tuweni:tuweni-io:0.10.0'
dependency 'org.apache.tuweni:tuweni-toml:0.10.0' dependency 'org.apache.tuweni:tuweni-toml:0.10.0'
dependency 'org.apache.tuweni:tuweni-units:0.10.0' dependency 'org.apache.tuweni:tuweni-units:0.10.0'
dependency 'org.apache.tuweni:tuweni-net:0.10.0'
dependency 'org.assertj:assertj-core:3.14.0' dependency 'org.assertj:assertj-core:3.14.0'

Loading…
Cancel
Save