Skip to content

Commit

Permalink
[BESU-77] - Enable TLS for JSON-RPC HTTP Service (hyperledger#271)
Browse files Browse the repository at this point in the history
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>
Signed-off-by: edwardmack <ed@edwardmack.com>
  • Loading branch information
usmansaleem authored and edwardmack committed Feb 4, 2020
1 parent 428956a commit adf7a38
Show file tree
Hide file tree
Showing 20 changed files with 1,084 additions and 41 deletions.
65 changes: 64 additions & 1 deletion besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
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.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 @@ -447,6 +449,32 @@ void setBannedNodeIds(final List<String> values) {
"Require authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})")
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(
names = {"--rpc-ws-enabled"},
description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
Expand Down Expand Up @@ -1153,6 +1181,15 @@ 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"));

CommandLineUtils.checkOptionDependencies(
logger,
Expand All @@ -1167,7 +1204,11 @@ private JsonRpcConfiguration jsonRpcConfiguration() {
"--rpc-http-port",
"--rpc-http-authentication-enabled",
"--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
&& rpcHttpAuthenticationCredentialsFile() == null
Expand All @@ -1187,9 +1228,31 @@ && rpcHttpAuthenticationPublicKeyFile() == null) {
jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled);
jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile());
jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile());
jsonRpcConfiguration.setTlsConfiguration(rpcHttpTlsConfiguration());
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() {

CommandLineUtils.checkOptionDependencies(
Expand Down
4 changes: 4 additions & 0 deletions besu/src/test/resources/everything_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ rpc-http-cors-origins=["none"]
rpc-http-authentication-enabled=false
rpc-http-authentication-credentials-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-enabled=false
Expand Down
1 change: 1 addition & 0 deletions ethereum/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
implementation 'io.vertx:vertx-unit'
implementation 'io.vertx:vertx-web'
implementation 'org.apache.tuweni:tuweni-bytes'
implementation 'org.apache.tuweni:tuweni-net'
implementation 'org.apache.tuweni:tuweni-toml'
implementation 'org.apache.tuweni:tuweni-units'
implementation 'org.bouncycastle:bcprov-jdk15on'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;

import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import com.google.common.base.MoreObjects;

Expand All @@ -37,6 +40,7 @@ public class JsonRpcConfiguration {
private boolean authenticationEnabled = false;
private String authenticationCredentialsFile;
private File authenticationPublicKeyFile;
private TlsConfiguration tlsConfiguration;

public static JsonRpcConfiguration createDefault() {
final JsonRpcConfiguration config = new JsonRpcConfiguration();
Expand Down Expand Up @@ -128,6 +132,14 @@ public void setAuthenticationPublicKeyFile(final File authenticationPublicKeyFil
this.authenticationPublicKeyFile = authenticationPublicKeyFile;
}

public Optional<TlsConfiguration> getTlsConfiguration() {
return Optional.ofNullable(tlsConfiguration);
}

public void setTlsConfiguration(final TlsConfiguration tlsConfiguration) {
this.tlsConfiguration = tlsConfiguration;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import org.hyperledger.besu.util.ExceptionUtils;
import org.hyperledger.besu.util.NetworkUtility;

import java.net.InetSocketAddress;
Expand All @@ -60,6 +61,8 @@
import io.vertx.core.Future;
import io.vertx.core.Handler;
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.HttpServer;
import io.vertx.core.http.HttpServerOptions;
Expand All @@ -68,13 +71,15 @@
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PfxOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.net.tls.VertxTrustOptions;

public class JsonRpcHttpService {

Expand Down Expand Up @@ -168,14 +173,53 @@ private void validateConfig(final JsonRpcConfiguration config) {
public CompletableFuture<?> start() {
LOG.info("Starting JsonRPC service on {}:{}", config.getHost(), config.getPort());

// Create the HTTP server and a router object.
httpServer =
vertx.createHttpServer(
new HttpServerOptions()
.setHost(config.getHost())
.setPort(config.getPort())
.setHandle100ContinueAutomatically(true));
final CompletableFuture<?> resultFuture = new CompletableFuture<>();
try {
// Create the HTTP server and a router object.
httpServer = vertx.createHttpServer(getHttpServerOptions());
httpServer
.requestHandler(buildRouter())
.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
final Router router = Router.router(vertx);

Expand Down Expand Up @@ -222,43 +266,62 @@ public CompletableFuture<?> start() {
.produces(APPLICATION_JSON)
.handler(AuthenticationService::handleDisabledLogin);
}
return router;
}

final CompletableFuture<?> resultFuture = new CompletableFuture<>();
httpServer
.requestHandler(router)
.listen(
res -> {
if (!res.failed()) {
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);
});
private HttpServerOptions getHttpServerOptions() {
final HttpServerOptions httpServerOptions =
new HttpServerOptions()
.setHost(config.getHost())
.setPort(config.getPort())
.setHandle100ContinueAutomatically(true);

return;
}
httpServer = null;
final Throwable cause = res.cause();
if (cause instanceof SocketException) {
resultFuture.completeExceptionally(
new JsonRpcServiceException(
String.format(
"Failed to bind Ethereum JSON RPC listener to %s:%s: %s",
config.getHost(), config.getPort(), cause.getMessage())));
return;
return applyTlsConfig(httpServerOptions);
}

private HttpServerOptions applyTlsConfig(final HttpServerOptions httpServerOptions) {
config
.getTlsConfiguration()
.ifPresent(
tlsConfiguration -> {
try {
httpServerOptions
.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() {
Expand Down Expand Up @@ -329,7 +392,11 @@ public String url() {
if (httpServer == null) {
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) {
Expand Down

0 comments on commit adf7a38

Please sign in to comment.