mirror of https://github.com/hyperledger/besu
[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
parent
a7cde4dd8e
commit
cb56b3c745
@ -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); |
||||
} |
||||
} |
@ -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"); |
||||
} |
||||
} |
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@ |
||||
changeit |
Binary file not shown.
@ -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 |
Loading…
Reference in new issue