diff --git a/README.md b/README.md index 775e503f84..b8cec251a0 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,42 @@ projectRoot $ docker build -f ./docker/monitoring-client.Dockerfile -t mantis-mo projectRoot $ docker run --network=host mantis-monitoring-client ``` +### TLS setup + +Both the JSON RPC (on the node and faucet) can be additionally protected using TLS. +On the development environment it's already properly configured with a development certificate. + +#### Generating a new certificate + +If a new certificate is required, create a new keystore with a certificate by running `./tls/gen-cert.sh` + +#### Configuring the node + +1. Configure the certificate and password file to be used at `mantis.network.rpc.http.certificate` key on the `application.conf` file: + + keystore-path: path to the keystore storing the certificates (if generated through our script they are by default located in "./tls/mantisCA.p12") + keystore-type: type of certificate keystore being used (if generated through our script use "pkcs12") + password-file: path to the file with the password used for accessing the certificate keystore (if generated through our script they are by default located in "./tls/password") +2. Enable TLS in specific config: + - For JSON RPC: `mantis.network.rpc.http.mode=https` + +#### Configuring the faucet + +1. Configure the certificate and password file to be used at `mantis.network.rpc.http.certificate` key on the `faucet.conf` file: + + keystore-path: path to the keystore storing the certificates (if generated through our script they are by default located in "./tls/mantisCA.p12") + keystore-type: type of certificate keystore being used (if generated through our script use "pkcs12") + password-file: path to the file with the password used for accessing the certificate keystore (if generated through our script they are by default located in "./tls/password") +2. Enable TLS in specific config: + - For JSON RPC: `mantis.network.rpc.http.mode=https` +3. Configure the certificate used from RpcClient to connect with the node. Necessary if the node uses http secure. + This certificate and password file to be used at `faucet.rpc-client.certificate` key on the `faucet.conf` file: + + keystore-path: path to the keystore storing the certificates + keystore-type: type of certificate keystore being used (if generated through our script use "pkcs12") + password-file: path to the file with the password used for accessing the certificate keystore + + ### Feedback Feedback gratefully received through the Ethereum Classic Forum (http://forum.ethereumclassic.org/) diff --git a/src/evmTest/scala/io/iohk/ethereum/vm/PrecompiledContractsSpecEvm.scala b/src/evmTest/scala/io/iohk/ethereum/vm/PrecompiledContractsSpecEvm.scala index 2cf08a893c..888124c107 100644 --- a/src/evmTest/scala/io/iohk/ethereum/vm/PrecompiledContractsSpecEvm.scala +++ b/src/evmTest/scala/io/iohk/ethereum/vm/PrecompiledContractsSpecEvm.scala @@ -4,7 +4,7 @@ import akka.util.ByteString import io.iohk.ethereum.crypto import io.iohk.ethereum.crypto._ import io.iohk.ethereum.domain.SignedTransaction.{FirstByteOfAddress, LastByteOfAddress} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.vm.utils.EvmTestEnv import org.bouncycastle.crypto.params.ECPublicKeyParameters import org.scalatest.funsuite.AnyFunSuite diff --git a/src/it/scala/io/iohk/ethereum/sync/util/CommonFakePeer.scala b/src/it/scala/io/iohk/ethereum/sync/util/CommonFakePeer.scala index 6f9cff6850..c13a635b93 100644 --- a/src/it/scala/io/iohk/ethereum/sync/util/CommonFakePeer.scala +++ b/src/it/scala/io/iohk/ethereum/sync/util/CommonFakePeer.scala @@ -15,6 +15,7 @@ import io.iohk.ethereum.db.dataSource.{RocksDbConfig, RocksDbDataSource} import io.iohk.ethereum.db.storage.pruning.{ArchivePruning, PruningMode} import io.iohk.ethereum.db.storage.{AppStateStorage, Namespaces} import io.iohk.ethereum.domain.{Block, Blockchain, BlockchainImpl, ChainWeight} +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.ledger.InMemoryWorldStateProxy import io.iohk.ethereum.mpt.MerklePatriciaTrie import io.iohk.ethereum.network.EtcPeerManagerActor.PeerInfo @@ -33,7 +34,7 @@ import io.iohk.ethereum.network.{ PeerManagerActor, ServerActor } -import io.iohk.ethereum.nodebuilder.{PruningConfigBuilder, SecureRandomBuilder} +import io.iohk.ethereum.nodebuilder.PruningConfigBuilder import io.iohk.ethereum.sync.util.SyncCommonItSpec._ import io.iohk.ethereum.sync.util.SyncCommonItSpecUtils._ import io.iohk.ethereum.utils.ServerStatus.Listening diff --git a/src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala b/src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala index da16612342..68576a635f 100644 --- a/src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala +++ b/src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala @@ -23,7 +23,8 @@ import io.iohk.ethereum.network.handshaker.{EtcHandshaker, EtcHandshakerConfigur import io.iohk.ethereum.network.p2p.EthereumMessageDecoder import io.iohk.ethereum.network.rlpx.RLPxConnectionHandler.RLPxConfiguration import io.iohk.ethereum.network.{ForkResolver, PeerEventBusActor, PeerManagerActor} -import io.iohk.ethereum.nodebuilder.{AuthHandshakerBuilder, NodeKeyBuilder, SecureRandomBuilder} +import io.iohk.ethereum.nodebuilder.{AuthHandshakerBuilder, NodeKeyBuilder} +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.{Config, NodeStatus, ServerStatus} import monix.reactive.Observable import org.bouncycastle.util.encoders.Hex diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 59095c70b7..95b28a873f 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -183,17 +183,20 @@ mantis { # Listening port of JSON-RPC HTTP(S) endpoint port = 8546 + certificate = null + #certificate { # Path to the keystore storing the certificates (used only for https) # null value indicates HTTPS is not being used - certificate-keystore-path = null + # keystore-path = "tls/mantisCA.p12" # Type of certificate keystore being used # null value indicates HTTPS is not being used - certificate-keystore-type = null + # keystore-type = "pkcs12" # File with the password used for accessing the certificate keystore (used only for https) # null value indicates HTTPS is not being used - certificate-password-file = null + # password-file = "tls/password" + #} # Domains allowed to query RPC endpoint. Use "*" to enable requests from # any domain. diff --git a/src/main/scala/io/iohk/ethereum/cli/CliCommands.scala b/src/main/scala/io/iohk/ethereum/cli/CliCommands.scala index 3f56254bfc..76df5057d1 100644 --- a/src/main/scala/io/iohk/ethereum/cli/CliCommands.scala +++ b/src/main/scala/io/iohk/ethereum/cli/CliCommands.scala @@ -7,7 +7,7 @@ import io.iohk.ethereum.crypto._ import io.iohk.ethereum.domain.Address import io.iohk.ethereum.utils.ByteStringUtils import java.security.SecureRandom -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.bouncycastle.util.encoders.Hex object CliCommands extends SecureRandomBuilder { diff --git a/src/main/scala/io/iohk/ethereum/consensus/TestConsensusBuilder.scala b/src/main/scala/io/iohk/ethereum/consensus/TestConsensusBuilder.scala index 095842814c..d1b8e27b4b 100644 --- a/src/main/scala/io/iohk/ethereum/consensus/TestConsensusBuilder.scala +++ b/src/main/scala/io/iohk/ethereum/consensus/TestConsensusBuilder.scala @@ -1,6 +1,7 @@ package io.iohk.ethereum.consensus import io.iohk.ethereum.nodebuilder._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.Logger /** diff --git a/src/main/scala/io/iohk/ethereum/crypto/EcKeyGen.scala b/src/main/scala/io/iohk/ethereum/crypto/EcKeyGen.scala index d3b4dd0323..cbd77c288c 100644 --- a/src/main/scala/io/iohk/ethereum/crypto/EcKeyGen.scala +++ b/src/main/scala/io/iohk/ethereum/crypto/EcKeyGen.scala @@ -1,6 +1,6 @@ package io.iohk.ethereum.crypto -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder /** * A simple tool to generate ECDSA key pairs. Takes an optional positional argument [n] - number of key pairs diff --git a/src/main/scala/io/iohk/ethereum/faucet/FaucetConfig.scala b/src/main/scala/io/iohk/ethereum/faucet/FaucetConfig.scala index a66c91cf26..ad0030d639 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/FaucetConfig.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/FaucetConfig.scala @@ -36,7 +36,7 @@ object FaucetConfig { txGasPrice = faucetConfig.getLong("tx-gas-price"), txGasLimit = faucetConfig.getLong("tx-gas-limit"), txValue = faucetConfig.getLong("tx-value"), - rpcAddress = faucetConfig.getString("rpc-address"), + rpcAddress = faucetConfig.getString("rpc-client.rpc-address"), keyStoreDir = faucetConfig.getString("keystore-dir"), minRequestInterval = faucetConfig.getDuration("min-request-interval").toMillis.millis, handlerTimeout = faucetConfig.getDuration("handler-timeout").toMillis.millis, diff --git a/src/main/scala/io/iohk/ethereum/faucet/FaucetSupervisor.scala b/src/main/scala/io/iohk/ethereum/faucet/FaucetSupervisor.scala index 4fc37eab7e..fde93f763c 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/FaucetSupervisor.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/FaucetSupervisor.scala @@ -12,11 +12,11 @@ object FaucetSupervisor { val name = "FaucetSupervisor" } -class FaucetSupervisor(walletRpcClient: WalletService, config: FaucetConfig, shutdown: () => Unit)(implicit +class FaucetSupervisor(walletService: WalletService, config: FaucetConfig, shutdown: () => Unit)(implicit system: ActorSystem ) extends Logger { - val childProps = FaucetHandler.props(walletRpcClient, config) + val childProps = FaucetHandler.props(walletService, config) val minBackoff: FiniteDuration = config.supervisor.minBackoff val maxBackoff: FiniteDuration = config.supervisor.maxBackoff diff --git a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetBuilder.scala b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetBuilder.scala index 588d6819e8..10857f77d8 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetBuilder.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetBuilder.scala @@ -1,18 +1,15 @@ package io.iohk.ethereum.faucet.jsonrpc -import java.security.SecureRandom - import akka.actor.ActorSystem import io.iohk.ethereum.faucet.{FaucetConfigBuilder, FaucetSupervisor} import io.iohk.ethereum.jsonrpc.server.controllers.ApisBase import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController.JsonRpcConfig import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer import io.iohk.ethereum.keystore.KeyStoreImpl -import io.iohk.ethereum.mallet.service.RpcClient -import io.iohk.ethereum.utils.{ConfigUtils, KeyStoreConfig, Logger} +import io.iohk.ethereum.security.{SSLContextBuilder, SecureRandomBuilder} +import io.iohk.ethereum.utils.{KeyStoreConfig, Logger} import scala.concurrent.Await -import scala.util.Try trait ActorSystemBuilder { def systemName: String @@ -26,13 +23,24 @@ trait FaucetControllerBuilder { } trait FaucetRpcServiceBuilder { - self: FaucetConfigBuilder with FaucetControllerBuilder with ActorSystemBuilder with ShutdownHookBuilder => + self: FaucetConfigBuilder + with FaucetControllerBuilder + with ActorSystemBuilder + with SecureRandomBuilder + with ShutdownHookBuilder + with SSLContextBuilder => + + val keyStore = + new KeyStoreImpl( + KeyStoreConfig.customKeyStoreConfig(faucetConfig.keyStoreDir), + secureRandom + ) - val keyStore = new KeyStoreImpl(KeyStoreConfig.customKeyStoreConfig(faucetConfig.keyStoreDir), new SecureRandom()) - val rpcClient = new RpcClient(faucetConfig.rpcAddress) - val walletService = new WalletService(rpcClient, keyStore, faucetConfig) + val walletRpcClient: WalletRpcClient = + new WalletRpcClient(faucetConfig.rpcAddress, () => sslContext("faucet.rpc-client")) + val walletService = new WalletService(walletRpcClient, keyStore, faucetConfig) val faucetSupervisor: FaucetSupervisor = new FaucetSupervisor(walletService, faucetConfig, shutdown)(system) - val faucetRpcService = new FaucetRpcService(faucetConfig)(system) + val faucetRpcService = new FaucetRpcService(faucetConfig) } trait FaucetJsonRpcHealthCheckBuilder { @@ -63,27 +71,20 @@ trait FaucetJsonRpcControllerBuilder { val faucetJsonRpcController = new FaucetJsonRpcController(faucetRpcService, jsonRpcConfig) } -trait SecureRandomBuilder { - self: FaucetConfigBuilder => - lazy val secureRandom: SecureRandom = - ConfigUtils - .getOptionalValue(rawMantisConfig, _.getString, "secure-random-algo") - .flatMap(name => Try { SecureRandom.getInstance(name) }.toOption) - .getOrElse(new SecureRandom()) -} - trait FaucetJsonRpcHttpServerBuilder { self: ActorSystemBuilder with JsonRpcConfigBuilder with SecureRandomBuilder with FaucetJsonRpcHealthCheckBuilder - with FaucetJsonRpcControllerBuilder => + with FaucetJsonRpcControllerBuilder + with SSLContextBuilder => val faucetJsonRpcHttpServer = JsonRpcHttpServer( faucetJsonRpcController, faucetJsonRpcHealthCheck, jsonRpcConfig.httpServerConfig, - secureRandom + secureRandom, + () => sslContext("mantis.network.rpc.http") ) } @@ -107,6 +108,7 @@ class FaucetServer with ApisBuilder with JsonRpcConfigBuilder with SecureRandomBuilder + with SSLContextBuilder with FaucetControllerBuilder with FaucetRpcServiceBuilder with FaucetJsonRpcHealthCheckBuilder @@ -127,5 +129,4 @@ class FaucetServer case Right(jsonRpcServer) => jsonRpcServer.run() case Left(error) => throw new RuntimeException(s"$error") } - } diff --git a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerBuilder.scala b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerSelector.scala similarity index 87% rename from src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerBuilder.scala rename to src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerSelector.scala index d95c26855a..b9fd9259b8 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerBuilder.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetHandlerSelector.scala @@ -6,7 +6,7 @@ import akka.util.Timeout import io.iohk.ethereum.faucet.{FaucetConfigBuilder, FaucetHandler, FaucetSupervisor} import monix.eval.Task -trait FaucetHandlerBuilder { +trait FaucetHandlerSelector { self: FaucetConfigBuilder with RetrySupport => val handlerPath = s"user/${FaucetSupervisor.name}/${FaucetHandler.name}" @@ -15,7 +15,7 @@ trait FaucetHandlerBuilder { lazy val handlerTimeout: Timeout = Timeout(faucetConfig.handlerTimeout) - def faucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { + def selectFaucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { Task.deferFuture( retry(() => system.actorSelection(handlerPath).resolveOne(handlerTimeout.duration), attempts, delay)( system.dispatcher, diff --git a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcService.scala b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcService.scala index 52af9aa6bd..d46750c86e 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcService.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcService.scala @@ -13,13 +13,13 @@ import io.iohk.ethereum.utils.Logger class FaucetRpcService(config: FaucetConfig)(implicit system: ActorSystem) extends FaucetConfigBuilder with RetrySupport - with FaucetHandlerBuilder + with FaucetHandlerSelector with Logger { implicit lazy val actorTimeout: Timeout = Timeout(config.responseTimeout) def sendFunds(sendFundsRequest: SendFundsRequest): ServiceResponse[SendFundsResponse] = - faucetHandler() + selectFaucetHandler() .flatMap(handler => handler .askFor[Any](FaucetHandlerMsg.SendFunds(sendFundsRequest.address)) @@ -28,7 +28,7 @@ class FaucetRpcService(config: FaucetConfig)(implicit system: ActorSystem) .onErrorRecover(handleErrors) def status(statusRequest: StatusRequest): ServiceResponse[StatusResponse] = - faucetHandler() + selectFaucetHandler() .flatMap(handler => handler.askFor[Any](FaucetHandlerMsg.Status)) .map(handleStatusResponse orElse handleErrors) .onErrorRecover(handleErrors) diff --git a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletRpcClient.scala b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletRpcClient.scala new file mode 100644 index 0000000000..88bc075c2c --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletRpcClient.scala @@ -0,0 +1,29 @@ +package io.iohk.ethereum.faucet.jsonrpc + +import akka.actor.ActorSystem +import akka.http.scaladsl.model.Uri +import io.circe.syntax._ +import akka.util.ByteString +import io.iohk.ethereum.domain.Address +import io.iohk.ethereum.jsonrpc.client.RpcClient +import io.iohk.ethereum.jsonrpc.client.RpcClient.RpcError +import io.iohk.ethereum.security.SSLError +import io.iohk.ethereum.utils.Logger +import javax.net.ssl.SSLContext +import monix.eval.Task + +import scala.concurrent.ExecutionContext + +class WalletRpcClient(node: Uri, getSSLContext: () => Either[SSLError, SSLContext])(implicit + system: ActorSystem, + ec: ExecutionContext +) extends RpcClient(node, getSSLContext) + with Logger { + import io.iohk.ethereum.jsonrpc.client.CommonJsonCodecs._ + + def getNonce(address: Address): Task[Either[RpcError, BigInt]] = + doRequest[BigInt]("eth_getTransactionCount", List(address.asJson, "latest".asJson)) + + def sendTransaction(rawTx: ByteString): Task[Either[RpcError, ByteString]] = + doRequest[ByteString]("eth_sendRawTransaction", List(rawTx.asJson)) +} diff --git a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletService.scala b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletService.scala index c9818e5d8b..d6e7de85c3 100644 --- a/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletService.scala +++ b/src/main/scala/io/iohk/ethereum/faucet/jsonrpc/WalletService.scala @@ -1,33 +1,31 @@ package io.iohk.ethereum.faucet.jsonrpc import akka.util.ByteString +import cats.data.EitherT import io.iohk.ethereum.domain.{Address, Transaction} import io.iohk.ethereum.faucet.FaucetConfig +import io.iohk.ethereum.jsonrpc.client.RpcClient.RpcError import io.iohk.ethereum.keystore.KeyStore.KeyStoreError import io.iohk.ethereum.keystore.{KeyStore, Wallet} -import io.iohk.ethereum.mallet.common.Err -import io.iohk.ethereum.mallet.service.RpcClient import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions.SignedTransactionEnc import io.iohk.ethereum.rlp import io.iohk.ethereum.utils.{ByteStringUtils, Logger} import monix.eval.Task -class WalletService(rpcClient: RpcClient, keyStore: KeyStore, config: FaucetConfig) extends Logger { +class WalletService(walletRpcClient: WalletRpcClient, keyStore: KeyStore, config: FaucetConfig) extends Logger { - def sendFunds(wallet: Wallet, addressTo: Address): Task[Either[Err, ByteString]] = { - Task { - (for { - nonce <- rpcClient.getNonce(wallet.address) - txId <- rpcClient.sendTransaction(prepareTx(wallet, addressTo, nonce)) - } yield txId) match { - case Right(txId) => - val txIdHex = s"0x${ByteStringUtils.hash2string(txId)}" - log.info(s"Sending ${config.txValue} ETH to $addressTo in tx: $txIdHex.") - Right(txId) - case Left(error) => - log.error(s"An error occurred while using faucet", error) - Left(error) - } + def sendFunds(wallet: Wallet, addressTo: Address): Task[Either[RpcError, ByteString]] = { + (for { + nonce <- EitherT(walletRpcClient.getNonce(wallet.address)) + txId <- EitherT(walletRpcClient.sendTransaction(prepareTx(wallet, addressTo, nonce))) + } yield txId).value map { + case Right(txId) => + val txIdHex = s"0x${ByteStringUtils.hash2string(txId)}" + log.info(s"Sending ${config.txValue} ETC to $addressTo in tx: $txIdHex.") + Right(txId) + case Left(error) => + log.error(s"An error occurred while using faucet", error) + Left(error) } } diff --git a/src/main/scala/io/iohk/ethereum/mallet/service/CommonJsonCodecs.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/client/CommonJsonCodecs.scala similarity index 96% rename from src/main/scala/io/iohk/ethereum/mallet/service/CommonJsonCodecs.scala rename to src/main/scala/io/iohk/ethereum/jsonrpc/client/CommonJsonCodecs.scala index 5c2336abee..0904aeeb9b 100644 --- a/src/main/scala/io/iohk/ethereum/mallet/service/CommonJsonCodecs.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/client/CommonJsonCodecs.scala @@ -1,4 +1,4 @@ -package io.iohk.ethereum.mallet.service +package io.iohk.ethereum.jsonrpc.client import akka.util.ByteString import io.circe._ diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/client/RpcClient.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/client/RpcClient.scala new file mode 100644 index 0000000000..d56c85cfd6 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/client/RpcClient.scala @@ -0,0 +1,101 @@ +package io.iohk.ethereum.jsonrpc.client + +import java.io.{PrintWriter, StringWriter} +import java.util.UUID + +import akka.actor.ActorSystem +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.http.scaladsl.{ConnectionContext, Http, HttpsConnectionContext} +import io.circe.generic.auto._ +import io.circe.parser.parse +import io.circe.syntax._ +import io.circe.{Decoder, Json} +import io.iohk.ethereum.jsonrpc.JsonRpcError +import io.iohk.ethereum.security.SSLError +import io.iohk.ethereum.utils.Logger +import javax.net.ssl.SSLContext +import monix.eval.Task + +import scala.concurrent.ExecutionContext + +abstract class RpcClient(node: Uri, getSSLContext: () => Either[SSLError, SSLContext])(implicit + system: ActorSystem, + ec: ExecutionContext +) extends Logger { + + import RpcClient._ + + lazy val connectionContext: HttpsConnectionContext = if (node.scheme.startsWith("https")) { + getSSLContext().toOption.fold(Http().defaultClientHttpsContext)(ConnectionContext.httpsClient) + } else { + Http().defaultClientHttpsContext + } + + protected def doRequest[T: Decoder](method: String, args: Seq[Json]): RpcResponse[T] = { + doJsonRequest(method, args).map(_.flatMap(getResult[T])) + } + + protected def doJsonRequest( + method: String, + args: Seq[Json] + ): RpcResponse[Json] = { + val request = prepareJsonRequest(method, args) + log.info(s"Making RPC call with request: $request") + makeRpcCall(request.asJson) + } + + private def getResult[T: Decoder](jsonResponse: Json): Either[RpcError, T] = { + jsonResponse.hcursor.downField("error").as[JsonRpcError] match { + case Right(error) => + Left(RpcClientError(s"Node returned an error: ${error.message} (${error.code})")) + case Left(_) => + jsonResponse.hcursor.downField("result").as[T].left.map(f => RpcClientError(f.message)) + } + } + + private def makeRpcCall(jsonRequest: Json): Task[Either[RpcError, Json]] = { + val entity = HttpEntity(ContentTypes.`application/json`, jsonRequest.noSpaces) + val request = HttpRequest(method = HttpMethods.POST, uri = node, entity = entity) + + Task + .deferFuture(for { + response <- Http().singleRequest(request, connectionContext) + data <- Unmarshal(response.entity).to[String] + } yield parse(data).left.map(e => RpcClientError(e.message))) + .onErrorHandle { ex: Throwable => + Left(RpcClientError(s"RPC request failed: ${exceptionToString(ex)}")) + } + } + + private def prepareJsonRequest(method: String, args: Seq[Json]): Json = { + Map( + "jsonrpc" -> "2.0".asJson, + "method" -> method.asJson, + "params" -> args.asJson, + "id" -> s"${UUID.randomUUID()}".asJson + ).asJson + } + + private def exceptionToString(ex: Throwable): String = { + val sw = new StringWriter() + sw.append(ex.getMessage + "\n") + ex.printStackTrace(new PrintWriter(sw)) + sw.toString + } + +} + +object RpcClient { + type RpcResponse[T] = Task[Either[RpcError, T]] + + type Secrets = Map[String, Json] + + sealed trait RpcError { + def msg: String + } + + case class ParserError(msg: String) extends RpcError + + case class RpcClientError(msg: String) extends RpcError +} diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala index 29b513bd03..020f077eb7 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala @@ -12,9 +12,11 @@ import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings import de.heikoseeberger.akkahttpjson4s.Json4sSupport import io.iohk.ethereum.jsonrpc._ +import io.iohk.ethereum.security.SSLError import io.iohk.ethereum.jsonrpc.serialization.JsonSerializers import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController import io.iohk.ethereum.utils.{ConfigUtils, Logger} +import javax.net.ssl.SSLContext import monix.eval.Task import monix.execution.Scheduler.Implicits.global import org.json4s.{DefaultFormats, JInt, native} @@ -98,12 +100,17 @@ object JsonRpcHttpServer extends Logger { jsonRpcController: JsonRpcBaseController, jsonRpcHealthchecker: JsonRpcHealthChecker, config: JsonRpcHttpServerConfig, - secureRandom: SecureRandom + secureRandom: SecureRandom, + fSslContext: () => Either[SSLError, SSLContext] )(implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] = config.mode match { case "http" => Right(new BasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthchecker, config)(actorSystem)) case "https" => - Right(new JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom)(actorSystem)) + Right( + new JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom, fSslContext)( + actorSystem + ) + ) case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected") } @@ -112,9 +119,6 @@ object JsonRpcHttpServer extends Logger { val enabled: Boolean val interface: String val port: Int - val certificateKeyStorePath: Option[String] - val certificateKeyStoreType: Option[String] - val certificatePasswordFile: Option[String] val corsAllowedOrigins: HttpOriginMatcher } @@ -131,13 +135,6 @@ object JsonRpcHttpServer extends Logger { override val port: Int = rpcHttpConfig.getInt("port") override val corsAllowedOrigins = ConfigUtils.parseCorsAllowedOrigins(rpcHttpConfig, "cors-allowed-origins") - - override val certificateKeyStorePath: Option[String] = - ConfigUtils.getOptionalValue(rpcHttpConfig, _.getString, "certificate-keystore-path") - override val certificateKeyStoreType: Option[String] = - ConfigUtils.getOptionalValue(rpcHttpConfig, _.getString, "certificate-keystore-type") - override val certificatePasswordFile: Option[String] = - ConfigUtils.getOptionalValue(rpcHttpConfig, _.getString, "certificate-password-file") } } } diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala index 1777c9389d..c837e4c184 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala @@ -1,47 +1,32 @@ package io.iohk.ethereum.jsonrpc.server.http +import java.security.SecureRandom + import akka.actor.ActorSystem import akka.http.scaladsl.{ConnectionContext, Http} import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher import io.iohk.ethereum.jsonrpc.JsonRpcHealthChecker +import io.iohk.ethereum.security.SSLError +import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig -import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpsServer.HttpsSetupResult import io.iohk.ethereum.utils.Logger -import java.io.{File, FileInputStream} -import java.security.{KeyStore, SecureRandom} - -import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController -import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} +import javax.net.ssl.SSLContext import scala.concurrent.ExecutionContext.Implicits.global -import scala.io.Source -import scala.util.{Failure, Success, Try} +import scala.util.{Failure, Success} class JsonRpcHttpsServer( val jsonRpcController: JsonRpcBaseController, val jsonRpcHealthChecker: JsonRpcHealthChecker, config: JsonRpcHttpServerConfig, - secureRandom: SecureRandom + secureRandom: SecureRandom, + getSSLContext: () => Either[SSLError, SSLContext] )(implicit val actorSystem: ActorSystem) extends JsonRpcHttpServer with Logger { def run(): Unit = { - val maybeSslContext = validateCertificateFiles( - config.certificateKeyStorePath, - config.certificateKeyStoreType, - config.certificatePasswordFile - ).flatMap { case (keystorePath, keystoreType, passwordFile) => - val passwordReader = Source.fromFile(passwordFile) - try { - val password = passwordReader.getLines().mkString - obtainSSLContext(keystorePath, keystoreType, password) - } finally { - passwordReader.close() - } - } - - val maybeHttpsContext = maybeSslContext.map(sslContext => ConnectionContext.httpsServer(sslContext)) + val maybeHttpsContext = getSSLContext().map(sslContext => ConnectionContext.httpsServer(sslContext)) maybeHttpsContext match { case Right(httpsContext) => @@ -51,84 +36,11 @@ class JsonRpcHttpsServer( case Success(serverBinding) => log.info(s"JSON RPC HTTPS server listening on ${serverBinding.localAddress}") case Failure(ex) => log.error("Cannot start JSON HTTPS RPC server", ex) } - case Left(error) => log.error(s"Cannot start JSON HTTPS RPC server due to: $error") - } - } - - /** - * Constructs the SSL context given a certificate - * - * @param certificateKeyStorePath, path to the keystore where the certificate is stored - * @param password for accessing the keystore with the certificate - * @return the SSL context with the obtained certificate or an error if any happened - */ - private def obtainSSLContext( - certificateKeyStorePath: String, - certificateKeyStoreType: String, - password: String - ): HttpsSetupResult[SSLContext] = { - val passwordCharArray: Array[Char] = password.toCharArray - - val maybeKeyStore: HttpsSetupResult[KeyStore] = Try(KeyStore.getInstance(certificateKeyStoreType)).toOption - .toRight(s"Certificate keystore invalid type set: $certificateKeyStoreType") - val keyStoreInitResult: HttpsSetupResult[KeyStore] = maybeKeyStore.flatMap { keyStore => - val keyStoreFileCreationResult = Option(new FileInputStream(certificateKeyStorePath)) - .toRight("Certificate keystore file creation failed") - keyStoreFileCreationResult.flatMap { keyStoreFile => - Try(keyStore.load(keyStoreFile, passwordCharArray)) match { - case Success(_) => Right(keyStore) - case Failure(err) => Left(err.getMessage) - } - } - } - - keyStoreInitResult.map { ks => - val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509") - keyManagerFactory.init(ks, passwordCharArray) - - val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509") - tmf.init(ks) - - val sslContext: SSLContext = SSLContext.getInstance("TLS") - sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, secureRandom) - sslContext + case Left(error) => + log.error(s"Cannot start JSON HTTPS RPC server due to: $error") + throw new IllegalStateException(error.reason) } - } - /** - * Validates that the keystore certificate file and password file were configured and that the files exists - * - * @param maybeKeystorePath, with the path to the certificate keystore if it was configured - * @param maybePasswordFile, with the path to the password file if it was configured - * @return the certificate path and password file or the error detected - */ - private def validateCertificateFiles( - maybeKeystorePath: Option[String], - maybeKeystoreType: Option[String], - maybePasswordFile: Option[String] - ): HttpsSetupResult[(String, String, String)] = - (maybeKeystorePath, maybeKeystoreType, maybePasswordFile) match { - case (Some(keystorePath), Some(keystoreType), Some(passwordFile)) => - val keystoreDirMissing = !new File(keystorePath).isFile - val passwordFileMissing = !new File(passwordFile).isFile - if (keystoreDirMissing && passwordFileMissing) - Left("Certificate keystore path and password file configured but files are missing") - else if (keystoreDirMissing) - Left("Certificate keystore path configured but file is missing") - else if (passwordFileMissing) - Left("Certificate password file configured but file is missing") - else - Right((keystorePath, keystoreType, passwordFile)) - case _ => - Left( - "HTTPS requires: certificate-keystore-path, certificate-keystore-type and certificate-password-file to be configured" - ) - } - override def corsAllowedOrigins: HttpOriginMatcher = config.corsAllowedOrigins } - -object JsonRpcHttpsServer { - type HttpsSetupResult[T] = Either[String, T] -} diff --git a/src/main/scala/io/iohk/ethereum/mallet/interpreter/Commands.scala b/src/main/scala/io/iohk/ethereum/mallet/interpreter/Commands.scala index 3dd09c5310..8bc091d5c6 100644 --- a/src/main/scala/io/iohk/ethereum/mallet/interpreter/Commands.scala +++ b/src/main/scala/io/iohk/ethereum/mallet/interpreter/Commands.scala @@ -6,7 +6,7 @@ import io.circe.syntax._ import io.iohk.ethereum.domain.{Address, Transaction} import io.iohk.ethereum.mallet.common.{StringUtil, Util} import io.iohk.ethereum.mallet.interpreter.Parameter._ -import io.iohk.ethereum.mallet.service.CommonJsonCodecs._ +import io.iohk.ethereum.jsonrpc.client.CommonJsonCodecs._ import io.iohk.ethereum.mallet.service.State import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions.SignedTransactionEnc import io.iohk.ethereum.rlp diff --git a/src/main/scala/io/iohk/ethereum/mallet/main/Mallet.scala b/src/main/scala/io/iohk/ethereum/mallet/main/Mallet.scala index 6c3d2d5782..135ae8aced 100644 --- a/src/main/scala/io/iohk/ethereum/mallet/main/Mallet.scala +++ b/src/main/scala/io/iohk/ethereum/mallet/main/Mallet.scala @@ -5,7 +5,7 @@ import java.time.Instant import io.iohk.ethereum.keystore.KeyStoreImpl import io.iohk.ethereum.mallet.interpreter.Interpreter -import io.iohk.ethereum.mallet.service.{RpcClient, State} +import io.iohk.ethereum.mallet.service.{RpcClientMallet, State} import io.iohk.ethereum.utils.KeyStoreConfig import scala.annotation.tailrec @@ -23,7 +23,7 @@ object Mallet extends App { private val initialState = { new State( shell, - RpcClient(clOptions.node), + RpcClientMallet(clOptions.node), new KeyStoreImpl(KeyStoreConfig.customKeyStoreConfig(clOptions.dataDir), new SecureRandom()), clOptions.account, None, diff --git a/src/main/scala/io/iohk/ethereum/mallet/service/RpcClient.scala b/src/main/scala/io/iohk/ethereum/mallet/service/RpcClientMallet.scala similarity index 88% rename from src/main/scala/io/iohk/ethereum/mallet/service/RpcClient.scala rename to src/main/scala/io/iohk/ethereum/mallet/service/RpcClientMallet.scala index fcc41035d2..10b4d5e4dd 100644 --- a/src/main/scala/io/iohk/ethereum/mallet/service/RpcClient.scala +++ b/src/main/scala/io/iohk/ethereum/mallet/service/RpcClientMallet.scala @@ -19,20 +19,21 @@ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} import scala.util.{Failure, Success, Try} -object RpcClient { +//TODO: change it class name. Because we have pending remove it. Task: https://jira.iohk.io/browse/ETCM-423 +object RpcClientMallet { /** * This factory method is defining an ActorSystem, ActorMaterializer and ExecutionContext for - * the [[RpcClient]]. To customize these dependencies use [[RpcClient]]'s constructor + * the [[RpcClientMallet]]. To customize these dependencies use [[RpcClientMallet]]'s constructor */ - def apply(node: Uri): RpcClient = { + def apply(node: Uri): RpcClientMallet = { // TODO: CL option to enable akka logging val akkaConfig = ConfigFactory.load("mallet") implicit val system = ActorSystem("mallet_rpc", akkaConfig) implicit val ec = scala.concurrent.ExecutionContext.Implicits.global - new RpcClient(node) + new RpcClientMallet(node) } } @@ -40,8 +41,8 @@ object RpcClient { * Talks to a node over HTTP(S) JSON-RPC * Note: the URI schema determines whether HTTP or HTTPS is used */ -class RpcClient(node: Uri)(implicit system: ActorSystem, ec: ExecutionContext) { - import CommonJsonCodecs._ +class RpcClientMallet(node: Uri)(implicit system: ActorSystem, ec: ExecutionContext) { + import io.iohk.ethereum.jsonrpc.client.CommonJsonCodecs._ //TODO: CL option private val httpTimeout = 5.seconds diff --git a/src/main/scala/io/iohk/ethereum/mallet/service/State.scala b/src/main/scala/io/iohk/ethereum/mallet/service/State.scala index f1843f17b1..d73bb0ec05 100644 --- a/src/main/scala/io/iohk/ethereum/mallet/service/State.scala +++ b/src/main/scala/io/iohk/ethereum/mallet/service/State.scala @@ -9,7 +9,7 @@ import io.iohk.ethereum.keystore.KeyStore /** Immutable representation of application state, which is changed and used by certain commands */ class State( val passwordReader: PasswordReader, - val rpcClient: RpcClient, + val rpcClient: RpcClientMallet, val keyStore: KeyStore, val selectedAccount: Option[Address], val unlockedKey: Option[ByteString], @@ -18,7 +18,7 @@ class State( def copy( passwordReader: PasswordReader = passwordReader, - rpcClient: RpcClient = rpcClient, + rpcClient: RpcClientMallet = rpcClient, keyStore: KeyStore = keyStore, selectedAccount: Option[Address] = selectedAccount, unlockedKey: Option[ByteString] = unlockedKey, diff --git a/src/main/scala/io/iohk/ethereum/network/discovery/Secp256k1SigAlg.scala b/src/main/scala/io/iohk/ethereum/network/discovery/Secp256k1SigAlg.scala index a7edcce53e..ff4a7e0073 100644 --- a/src/main/scala/io/iohk/ethereum/network/discovery/Secp256k1SigAlg.scala +++ b/src/main/scala/io/iohk/ethereum/network/discovery/Secp256k1SigAlg.scala @@ -3,13 +3,13 @@ package io.iohk.ethereum.network.discovery import akka.util.ByteString import io.iohk.ethereum.crypto import io.iohk.ethereum.crypto.ECDSASignature -import io.iohk.scalanet.discovery.crypto.{SigAlg, PublicKey, PrivateKey, Signature} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder +import io.iohk.scalanet.discovery.crypto.{PrivateKey, PublicKey, SigAlg, Signature} +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.params.ECPublicKeyParameters import scodec.bits.BitVector import scodec.{Attempt, Err} -import scodec.bits.BitVector -import org.bouncycastle.crypto.params.ECPublicKeyParameters -import org.bouncycastle.crypto.AsymmetricCipherKeyPair + import scala.collection.concurrent.TrieMap class Secp256k1SigAlg extends SigAlg with SecureRandomBuilder { diff --git a/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala b/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala index e2c6b1abce..90b5a99e28 100644 --- a/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala +++ b/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala @@ -1,13 +1,9 @@ package io.iohk.ethereum.nodebuilder -import java.security.SecureRandom -import java.util.concurrent.atomic.AtomicReference - import akka.actor.{ActorRef, ActorSystem} import io.iohk.ethereum.blockchain.data.GenesisDataLoader import io.iohk.ethereum.blockchain.sync.{BlockchainHostActor, SyncController} import io.iohk.ethereum.consensus._ -import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator import io.iohk.ethereum.db.components.Storages.PruningModeComponent import io.iohk.ethereum.db.components._ import io.iohk.ethereum.db.storage.AppStateStorage @@ -15,6 +11,7 @@ import io.iohk.ethereum.db.storage.pruning.PruningMode import io.iohk.ethereum.domain._ import io.iohk.ethereum.jsonrpc.NetService.NetServiceConfig import io.iohk.ethereum.jsonrpc._ +import io.iohk.ethereum.security.{SSLContextBuilder, SecureRandomBuilder} import io.iohk.ethereum.jsonrpc.server.controllers.ApisBase import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController.JsonRpcConfig import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer @@ -34,7 +31,6 @@ import io.iohk.ethereum.testmode.{TestLedgerBuilder, TestmodeConsensusBuilder} import io.iohk.ethereum.transactions.{PendingTransactionsManager, TransactionHistoryService} import io.iohk.ethereum.utils.Config.SyncConfig import io.iohk.ethereum.utils._ -import java.security.SecureRandom import java.util.concurrent.atomic.AtomicReference import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator import org.bouncycastle.crypto.AsymmetricCipherKeyPair @@ -487,10 +483,17 @@ trait JSONRpcHttpServerBuilder { with JSONRpcControllerBuilder with JSONRpcHealthcheckerBuilder with SecureRandomBuilder - with JSONRpcConfigBuilder => + with JSONRpcConfigBuilder + with SSLContextBuilder => lazy val maybeJsonRpcHttpServer = - JsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, jsonRpcConfig.httpServerConfig, secureRandom) + JsonRpcHttpServer( + jsonRpcController, + jsonRpcHealthChecker, + jsonRpcConfig.httpServerConfig, + secureRandom, + () => sslContext("mantis.network.rpc.http") + ) } trait JSONRpcIpcServerBuilder { @@ -613,21 +616,6 @@ trait GenesisDataLoaderBuilder { lazy val genesisDataLoader = new GenesisDataLoader(blockchain, blockchainConfig) } -trait SecureRandomBuilder extends Logger { - lazy val secureRandom: SecureRandom = - Config.secureRandomAlgo - .flatMap(name => - Try(SecureRandom.getInstance(name)) match { - case Failure(exception) => - log.warn(s"Couldn't create SecureRandom instance using algorithm ${name}. Falling-back to default one") - None - case Success(value) => - Some(value) - } - ) - .getOrElse(new SecureRandom()) -} - /** Provides the basic functionality of a Node, except the consensus algorithm. * The latter is loaded dynamically based on configuration. * @@ -635,7 +623,8 @@ trait SecureRandomBuilder extends Logger { * [[io.iohk.ethereum.consensus.ConsensusConfigBuilder ConsensusConfigBuilder]] */ trait Node - extends NodeKeyBuilder + extends SecureRandomBuilder + with NodeKeyBuilder with ActorSystemBuilder with StorageBuilder with BlockchainBuilder @@ -658,6 +647,7 @@ trait Node with JSONRpcConfigBuilder with JSONRpcHealthcheckerBuilder with JSONRpcControllerBuilder + with SSLContextBuilder with JSONRpcHttpServerBuilder with JSONRpcIpcServerBuilder with ShutdownHookBuilder @@ -673,7 +663,6 @@ trait Node with FilterManagerBuilder with FilterConfigBuilder with TxPoolConfigBuilder - with SecureRandomBuilder with AuthHandshakerBuilder with PruningConfigBuilder with PeerDiscoveryManagerBuilder diff --git a/src/main/scala/io/iohk/ethereum/security/FileUtils.scala b/src/main/scala/io/iohk/ethereum/security/FileUtils.scala new file mode 100644 index 0000000000..243f4e4c53 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/FileUtils.scala @@ -0,0 +1,28 @@ +package io.iohk.ethereum.security + +import java.io.{File, FileInputStream} + +import io.iohk.ethereum.utils.Logger + +import scala.io.{BufferedSource, Source} +import scala.util.Try + +trait FileUtils extends Logger { + + def exist(pathName: String): Boolean = new File(pathName).isFile + + def createFileInputStream(pathName: String): Either[Throwable, FileInputStream] = + Try(new FileInputStream(pathName)).toEither match { + case Right(fileInputStream) => + Option(fileInputStream).map(Right(_)).getOrElse { + log.error("empty fileInputStream") + Left(new IllegalStateException("empty fileInputStream")) + } + case Left(error) => + log.error("create file input stream failed", error) + Left(error) + } + + def getReader(file: String): BufferedSource = Source.fromFile(file) + +} diff --git a/src/main/scala/io/iohk/ethereum/security/KeyStoreUtils.scala b/src/main/scala/io/iohk/ethereum/security/KeyStoreUtils.scala new file mode 100644 index 0000000000..dd0e0297b6 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/KeyStoreUtils.scala @@ -0,0 +1,44 @@ +package io.iohk.ethereum.security + +import java.io.FileInputStream +import java.security.KeyStore + +import io.iohk.ethereum.utils.Logger +import javax.net.ssl.{KeyManager, KeyManagerFactory, TrustManager, TrustManagerFactory} + +import scala.util.Try + +trait KeyStoreUtils extends Logger { + + lazy val algorithm: String = "SunX509" + + def loadKeyStore( + keyStoreFile: FileInputStream, + passwordCharArray: Array[Char], + keyStore: KeyStore + ): Either[Throwable, Unit] = + Try(keyStore.load(keyStoreFile, passwordCharArray)).toEither + + def getKeyManager(keyStore: KeyStore, passwordCharArray: Array[Char]): Either[Throwable, Array[KeyManager]] = + Try { + val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(algorithm) + keyManagerFactory.init(keyStore, passwordCharArray) + keyManagerFactory.getKeyManagers + }.toEither match { + case Right(keyManager) => Right(keyManager) + case Left(error) => + log.error("getKeyManager failure", error) + Left(error) + } + + def getTrustManager(keyStore: KeyStore): Either[Throwable, Array[TrustManager]] = Try { + val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(algorithm) + tmf.init(keyStore) + tmf.getTrustManagers + }.toEither match { + case Right(trustManager) => Right(trustManager) + case Left(error) => + log.error("getTrustManager failure", error) + Left(error) + } +} diff --git a/src/main/scala/io/iohk/ethereum/security/SSLConfig.scala b/src/main/scala/io/iohk/ethereum/security/SSLConfig.scala new file mode 100644 index 0000000000..371d253e5d --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/SSLConfig.scala @@ -0,0 +1,30 @@ +package io.iohk.ethereum.security + +import com.typesafe.config.Config + +case class SSLConfig( + keyStorePath: String, + keyStoreType: String, + passwordFile: String +) + +object SSLConfig { + + val key = "certificate" + + def apply(config: Config): Option[SSLConfig] = { + if (config.getIsNull(key)) + None + else { + val certificateConfig = config.getConfig(key) + Some( + SSLConfig( + keyStorePath = certificateConfig.getString("keystore-path"), + keyStoreType = certificateConfig.getString("keystore-type"), + passwordFile = certificateConfig.getString("password-file") + ) + ) + } + } + +} diff --git a/src/main/scala/io/iohk/ethereum/security/SSLContextBuilder.scala b/src/main/scala/io/iohk/ethereum/security/SSLContextBuilder.scala new file mode 100644 index 0000000000..7054da67ea --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/SSLContextBuilder.scala @@ -0,0 +1,21 @@ +package io.iohk.ethereum.security +import com.typesafe.config.ConfigFactory +import javax.net.ssl.SSLContext + +case class SSLError(reason: String) + +trait SSLContextBuilder { self: SecureRandomBuilder => + + def sslContext(key: String): Either[SSLError, SSLContext] = + SSLConfig(ConfigFactory.load().getConfig(key)) + .toRight(SSLError("No SSL config present")) + .flatMap(SSLContextFactory().createSSLContext(_, secureRandom)) match { + case Right(sslConfig) => + log.debug("Loaded ssl config successful") + Right(sslConfig) + case Left(error) => + log.error(s"Loaded ssl config failure - $error") + Left(error) + } + +} diff --git a/src/main/scala/io/iohk/ethereum/security/SSLContextFactory.scala b/src/main/scala/io/iohk/ethereum/security/SSLContextFactory.scala new file mode 100644 index 0000000000..299b448ad9 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/SSLContextFactory.scala @@ -0,0 +1,103 @@ +package io.iohk.ethereum.security + +import java.io.FileInputStream +import java.security.{KeyStore, SecureRandom} + +import javax.net.ssl.SSLContext + +import scala.util.Try + +case class SSLContextFactory() extends FileUtils with KeyStoreUtils { + + def createSSLContext(sslConfig: SSLConfig, secureRandom: SecureRandom): Either[SSLError, SSLContext] = { + for { + _ <- validateCertificateFiles( + sslConfig.keyStorePath, + sslConfig.passwordFile + ) + sslContext <- getSSLContext(sslConfig, secureRandom) + } yield sslContext + } + + private def getSSLContext(sslConfig: SSLConfig, secureRandom: SecureRandom): Either[SSLError, SSLContext] = { + val passwordReader = getReader(sslConfig.passwordFile) + try { + val password = passwordReader.getLines().mkString + obtainSSLContext(secureRandom, sslConfig.keyStorePath, sslConfig.keyStoreType, password) + } finally { + passwordReader.close() + } + } + + /** + * Validates that the keystore certificate file and password file were configured and that the files exists + * + * @param keystorePath with the path to the certificate keystore if it was configured + * @param passwordFile with the path to the password file if it was configured + * @return the certificate path and password file or the error detected + */ + private def validateCertificateFiles( + keystorePath: String, + passwordFile: String + ): Either[SSLError, Unit] = { + val keystoreDirMissing = !exist(keystorePath) + val passwordFileMissing = !exist(passwordFile) + if (keystoreDirMissing && passwordFileMissing) + Left(SSLError("Certificate keystore path and password file configured but files are missing")) + else if (keystoreDirMissing) + Left(SSLError("Certificate keystore path configured but file is missing")) + else if (passwordFileMissing) + Left(SSLError("Certificate password file configured but file is missing")) + else + Right(()) + } + + /** + * Constructs the SSL context given a certificate + * + * @param secureRandom + * @param keyStorePath path to the keystore where the certificate is stored + * @param keyStoreType for accessing the keystore with the certificate + * @param password + * @return the SSL context with the obtained certificate or an error if any happened + */ + private def obtainSSLContext( + secureRandom: SecureRandom, + keyStorePath: String, + keyStoreType: String, + password: String + ): Either[SSLError, SSLContext] = { + val passwordCharArray: Array[Char] = password.toCharArray + + val maybeKeyStore: Either[SSLError, KeyStore] = Try(KeyStore.getInstance(keyStoreType)).toOption + .toRight(SSLError(s"Certificate keystore invalid type set: $keyStoreType")) + val keyStoreInitResult: Either[SSLError, KeyStore] = maybeKeyStore.flatMap { keyStore => + val keyStoreFileCreationResult: Either[SSLError, FileInputStream] = + createFileInputStream(keyStorePath).toOption.toRight(SSLError("Certificate keystore file creation failed")) + keyStoreFileCreationResult.flatMap { keyStoreFile => + loadKeyStore(keyStoreFile, passwordCharArray, keyStore) match { + case Right(_) => + Right(keyStore) + case Left(err) => + Left(SSLError(err.getMessage)) + } + } + } + + keyStoreInitResult.flatMap { ks => + (for { + km <- getKeyManager(ks, passwordCharArray) + tm <- getTrustManager(ks) + } yield (km, tm)) match { + case Right((km, tm)) => + val sslContext: SSLContext = SSLContext.getInstance("TLS") + sslContext.init(km, tm, secureRandom) + Right(sslContext) + case Left(error) => + log.error("Invalid Certificate keystore", error) + Left(SSLError("Invalid Certificate keystore")) + } + } + } + +} diff --git a/src/main/scala/io/iohk/ethereum/security/SecureRandomBuilder.scala b/src/main/scala/io/iohk/ethereum/security/SecureRandomBuilder.scala new file mode 100644 index 0000000000..0c02c1d19c --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/security/SecureRandomBuilder.scala @@ -0,0 +1,33 @@ +package io.iohk.ethereum.security + +import java.security.SecureRandom + +import com.typesafe.config.{Config, ConfigFactory} +import io.iohk.ethereum.utils.Logger + +import scala.util.{Failure, Success, Try} + +trait SecureRandomBuilder extends Logger { + + private lazy val rawMantisConfig: Config = ConfigFactory.load().getConfig("mantis") + + private val secureRandomAlgo: Option[String] = + if (rawMantisConfig.hasPath("secure-random-algo")) Some(rawMantisConfig.getString("secure-random-algo")) + else None + + lazy val secureRandom: SecureRandom = + secureRandomAlgo + .flatMap(name => + Try(SecureRandom.getInstance(name)) match { + case Failure(exception) => + log.error( + s"Couldn't create SecureRandom instance using algorithm $name. Falling-back to default one", + exception + ) + None + case Success(value) => + Some(value) + } + ) + .getOrElse(new SecureRandom()) +} diff --git a/src/rpcTest/resources/privateNetConfig/conf/rpc-test-private.conf b/src/rpcTest/resources/privateNetConfig/conf/rpc-test-private.conf index da984edd30..fc9f4b795f 100644 --- a/src/rpcTest/resources/privateNetConfig/conf/rpc-test-private.conf +++ b/src/rpcTest/resources/privateNetConfig/conf/rpc-test-private.conf @@ -48,9 +48,7 @@ mantis { interface = "localhost" port = 8546 - certificate-keystore-path = null - certificate-keystore-type = null - certificate-password-file = null + certificate = null cors-allowed-origins = "*" } diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index 3456d282e3..8f81ba93f0 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -151,7 +151,10 @@ faucet { tx-value = 1000000000000000000 - rpc-address = "http://127.0.0.1:8546/" + rpc-client { + rpc-address = "http://127.0.0.1:8546/" + certificate = null + } min-request-interval = 1.minute diff --git a/src/test/scala/io/iohk/ethereum/BlockHelpers.scala b/src/test/scala/io/iohk/ethereum/BlockHelpers.scala index 6eef1ac638..05667b39f1 100644 --- a/src/test/scala/io/iohk/ethereum/BlockHelpers.scala +++ b/src/test/scala/io/iohk/ethereum/BlockHelpers.scala @@ -3,7 +3,7 @@ package io.iohk.ethereum import akka.util.ByteString import io.iohk.ethereum.crypto.generateKeyPair import io.iohk.ethereum.domain.{Address, Block, BlockBody, SignedTransaction, Transaction} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.bouncycastle.crypto.AsymmetricCipherKeyPair import mouse.all._ diff --git a/src/test/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSyncFixtures.scala b/src/test/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSyncFixtures.scala index 5e0ca38ddd..646c056516 100644 --- a/src/test/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSyncFixtures.scala +++ b/src/test/scala/io/iohk/ethereum/blockchain/sync/regular/RegularSyncFixtures.scala @@ -15,6 +15,7 @@ import io.iohk.ethereum.blockchain.sync._ import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator import io.iohk.ethereum.domain.BlockHeaderImplicits._ import io.iohk.ethereum.domain._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.ledger._ import io.iohk.ethereum.network.EtcPeerManagerActor.{PeerInfo, RemoteStatus} import io.iohk.ethereum.network.PeerEventBusActor.PeerEvent.MessageFromPeer @@ -25,7 +26,6 @@ import io.iohk.ethereum.network.p2p.messages.PV63.{GetNodeData, NodeData} import io.iohk.ethereum.network.p2p.messages.PV64.NewBlock import io.iohk.ethereum.network.p2p.messages.ProtocolVersions import io.iohk.ethereum.network.{Peer, PeerId} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.utils.Config.SyncConfig import monix.eval.Task import monix.reactive.Observable diff --git a/src/test/scala/io/iohk/ethereum/consensus/ethash/RestrictedEthashSignerSpec.scala b/src/test/scala/io/iohk/ethereum/consensus/ethash/RestrictedEthashSignerSpec.scala index fd49311bec..0d9190bfb6 100644 --- a/src/test/scala/io/iohk/ethereum/consensus/ethash/RestrictedEthashSignerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/consensus/ethash/RestrictedEthashSignerSpec.scala @@ -1,7 +1,7 @@ package io.iohk.ethereum.consensus.ethash import io.iohk.ethereum.{ObjectGenerators, crypto} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks diff --git a/src/test/scala/io/iohk/ethereum/consensus/validators/BlockValidatorSpec.scala b/src/test/scala/io/iohk/ethereum/consensus/validators/BlockValidatorSpec.scala index e5e3e4fffd..f1b7926fa9 100644 --- a/src/test/scala/io/iohk/ethereum/consensus/validators/BlockValidatorSpec.scala +++ b/src/test/scala/io/iohk/ethereum/consensus/validators/BlockValidatorSpec.scala @@ -7,8 +7,8 @@ import io.iohk.ethereum.consensus.validators.std.StdBlockValidator import io.iohk.ethereum.consensus.validators.std.StdBlockValidator._ import io.iohk.ethereum.crypto import io.iohk.ethereum.domain._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.ledger.BloomFilter -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import org.bouncycastle.util.encoders.Hex import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidatorSpec.scala b/src/test/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidatorSpec.scala index 1ccb266d78..d968ef39e0 100644 --- a/src/test/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidatorSpec.scala +++ b/src/test/scala/io/iohk/ethereum/consensus/validators/BlockWithCheckpointHeaderValidatorSpec.scala @@ -9,8 +9,9 @@ import io.iohk.ethereum.crypto.ECDSASignature import io.iohk.ethereum.crypto.ECDSASignatureImplicits.ECDSASignatureOrdering import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097 import io.iohk.ethereum.domain._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.ledger.BloomFilter -import io.iohk.ethereum.nodebuilder.{BlockchainConfigBuilder, SecureRandomBuilder} +import io.iohk.ethereum.nodebuilder.BlockchainConfigBuilder import io.iohk.ethereum.utils.{BlockchainConfig, ByteStringUtils} import io.iohk.ethereum.{Fixtures, ObjectGenerators, crypto} import org.scalamock.scalatest.MockFactory diff --git a/src/test/scala/io/iohk/ethereum/consensus/validators/RestrictedEthashBlockHeaderValidatorSpec.scala b/src/test/scala/io/iohk/ethereum/consensus/validators/RestrictedEthashBlockHeaderValidatorSpec.scala index cf99bf89ac..204e12050f 100644 --- a/src/test/scala/io/iohk/ethereum/consensus/validators/RestrictedEthashBlockHeaderValidatorSpec.scala +++ b/src/test/scala/io/iohk/ethereum/consensus/validators/RestrictedEthashBlockHeaderValidatorSpec.scala @@ -7,7 +7,7 @@ import io.iohk.ethereum.consensus.validators.BlockHeaderError.{HeaderPoWError, R import io.iohk.ethereum.crypto import io.iohk.ethereum.crypto.ECDSASignature import io.iohk.ethereum.domain.{Address, BlockHeader, UInt256} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.{BlockchainConfig, ByteStringUtils} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/io/iohk/ethereum/crypto/ECDSASignatureSpec.scala b/src/test/scala/io/iohk/ethereum/crypto/ECDSASignatureSpec.scala index ed29ed72e0..f0207408da 100644 --- a/src/test/scala/io/iohk/ethereum/crypto/ECDSASignatureSpec.scala +++ b/src/test/scala/io/iohk/ethereum/crypto/ECDSASignatureSpec.scala @@ -1,7 +1,7 @@ package io.iohk.ethereum.crypto import akka.util.ByteString -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.ByteStringUtils import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary diff --git a/src/test/scala/io/iohk/ethereum/crypto/ECIESCoderSpec.scala b/src/test/scala/io/iohk/ethereum/crypto/ECIESCoderSpec.scala index 18ab9da5c7..d28341cb1e 100644 --- a/src/test/scala/io/iohk/ethereum/crypto/ECIESCoderSpec.scala +++ b/src/test/scala/io/iohk/ethereum/crypto/ECIESCoderSpec.scala @@ -2,7 +2,7 @@ package io.iohk.ethereum.crypto import java.math.BigInteger -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.bouncycastle.crypto.generators.ECKeyPairGenerator import org.bouncycastle.crypto.params.ECKeyGenerationParameters import org.bouncycastle.util.encoders.Hex diff --git a/src/test/scala/io/iohk/ethereum/db/storage/BlockBodiesStorageSpec.scala b/src/test/scala/io/iohk/ethereum/db/storage/BlockBodiesStorageSpec.scala index 1a06dea801..910a305d74 100644 --- a/src/test/scala/io/iohk/ethereum/db/storage/BlockBodiesStorageSpec.scala +++ b/src/test/scala/io/iohk/ethereum/db/storage/BlockBodiesStorageSpec.scala @@ -2,9 +2,9 @@ package io.iohk.ethereum.db.storage import io.iohk.ethereum.ObjectGenerators import io.iohk.ethereum.db.dataSource.EphemDataSource +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network.p2p.messages.CommonMessages import io.iohk.ethereum.network.p2p.messages.CommonMessages.NewBlock -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import org.bouncycastle.util.encoders.Hex import org.scalacheck.Gen import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks diff --git a/src/test/scala/io/iohk/ethereum/domain/SignedTransactionSpec.scala b/src/test/scala/io/iohk/ethereum/domain/SignedTransactionSpec.scala index 9f307d48f7..b70f3e8678 100644 --- a/src/test/scala/io/iohk/ethereum/domain/SignedTransactionSpec.scala +++ b/src/test/scala/io/iohk/ethereum/domain/SignedTransactionSpec.scala @@ -3,7 +3,7 @@ package io.iohk.ethereum.domain import io.iohk.ethereum.crypto import io.iohk.ethereum.crypto.generateKeyPair import io.iohk.ethereum.domain.SignedTransaction.FirstByteOfAddress -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.vm.Generators import org.scalacheck.Arbitrary import org.bouncycastle.crypto.params.ECPublicKeyParameters diff --git a/src/test/scala/io/iohk/ethereum/faucet/FaucetHandlerSpec.scala b/src/test/scala/io/iohk/ethereum/faucet/FaucetHandlerSpec.scala index cf4b1079c4..077479d944 100644 --- a/src/test/scala/io/iohk/ethereum/faucet/FaucetHandlerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/faucet/FaucetHandlerSpec.scala @@ -10,9 +10,9 @@ import io.iohk.ethereum.crypto.{generateKeyPair, keyPairToByteStrings} import io.iohk.ethereum.domain.Address import io.iohk.ethereum.faucet.FaucetHandler.{FaucetHandlerMsg, FaucetHandlerResponse} import io.iohk.ethereum.faucet.jsonrpc.WalletService +import io.iohk.ethereum.jsonrpc.client.RpcClient.{ParserError, RpcClientError} import io.iohk.ethereum.keystore.KeyStore.DecryptionFailed import io.iohk.ethereum.keystore.Wallet -import io.iohk.ethereum.mallet.common.{ParserError, RpcClientError} import io.iohk.ethereum.{NormalPatience, WithActorSystemShutDown, crypto} import monix.eval.Task import org.bouncycastle.util.encoders.Hex @@ -79,25 +79,25 @@ class FaucetHandlerSpec "should failed the payment if don't can parse the payload" in new TestSetup { withInitializedFaucet { - val errorMessage = "parser error" + val errorMessage = RpcClientError("parser error") (walletService.sendFunds _) .expects(wallet, paymentAddress) - .returning(Task.pure(Left(ParserError(errorMessage)))) + .returning(Task.pure(Left(errorMessage))) sender.send(faucetHandler, FaucetHandlerMsg.SendFunds(paymentAddress)) - sender.expectMsg(FaucetHandlerResponse.WalletRpcClientError(errorMessage)) + sender.expectMsg(FaucetHandlerResponse.WalletRpcClientError(errorMessage.msg)) } } "should failed the payment if throw rpc client error" in new TestSetup { withInitializedFaucet { - val errorMessage = "client timeout" + val errorMessage = ParserError("error parser") (walletService.sendFunds _) .expects(wallet, paymentAddress) - .returning(Task.pure(Left(RpcClientError(errorMessage)))) + .returning(Task.pure(Left(errorMessage))) sender.send(faucetHandler, FaucetHandlerMsg.SendFunds(paymentAddress)) - sender.expectMsg(FaucetHandlerResponse.WalletRpcClientError(errorMessage)) + sender.expectMsg(FaucetHandlerResponse.WalletRpcClientError(errorMessage.msg)) } } } diff --git a/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcServiceSpec.scala b/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcServiceSpec.scala index 32eafdd796..0754586e91 100644 --- a/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcServiceSpec.scala +++ b/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/FaucetRpcServiceSpec.scala @@ -45,7 +45,7 @@ class FaucetRpcServiceSpec val request: SendFundsRequest = SendFundsRequest(address) val txHash: ByteString = ByteString(Hex.decode("112233")) - fHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => + faucetHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => TransactionSent(txHash) }) faucetRpcService.sendFunds(request).runSyncUnsafe(Duration.Inf) match { @@ -59,7 +59,7 @@ class FaucetRpcServiceSpec val request: SendFundsRequest = SendFundsRequest(address) val clientError: String = "Parser error" - fHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => + faucetHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => WalletRpcClientError(clientError) }) faucetRpcService.sendFunds(request).runSyncUnsafe(Duration.Inf) match { @@ -72,7 +72,7 @@ class FaucetRpcServiceSpec val address: Address = Address("0x00") val request: SendFundsRequest = SendFundsRequest(address) - fHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => + faucetHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.SendFunds(`address`) => FaucetIsUnavailable }) faucetRpcService.sendFunds(request).runSyncUnsafe(Duration.Inf) match { @@ -83,7 +83,7 @@ class FaucetRpcServiceSpec } it should "answer FaucetIsUnavailable when tried to get status and the wallet is unavailable" in new TestSetup { - fHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.Status => + faucetHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.Status => FaucetIsUnavailable }) faucetRpcService.status(StatusRequest()).runSyncUnsafe(Duration.Inf) match { @@ -94,7 +94,7 @@ class FaucetRpcServiceSpec } it should "answer WalletAvailable when tried to get status and the wallet is available" in new TestSetup { - fHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.Status => + faucetHandler.setAutoPilot(simpleAutoPilot { case FaucetHandlerMsg.Status => StatusResponse(WalletAvailable) }) faucetRpcService.status(StatusRequest()).runSyncUnsafe(Duration.Inf) match { @@ -142,17 +142,16 @@ class FaucetRpcServiceSpec shutdownTimeout = 15.seconds ) - val fHandler = TestProbe() + val faucetHandler = TestProbe() val faucetRpcService: FaucetRpcService = new FaucetRpcService(config) { - - override def faucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { - Task(fHandler.ref) + override def selectFaucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { + Task(faucetHandler.ref) } } val faucetRpcServiceWithoutFaucetHandler: FaucetRpcService = new FaucetRpcService(config) { - override def faucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { + override def selectFaucetHandler()(implicit system: ActorSystem): Task[ActorRef] = { Task.raiseError(new RuntimeException("time out")) } } diff --git a/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/WalletServiceSpec.scala b/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/WalletServiceSpec.scala index b27bf209cd..8d6fc57578 100644 --- a/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/WalletServiceSpec.scala +++ b/src/test/scala/io/iohk/ethereum/faucet/jsonrpc/WalletServiceSpec.scala @@ -2,26 +2,24 @@ package io.iohk.ethereum.faucet.jsonrpc import java.security.SecureRandom -import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.util.ByteString import io.iohk.ethereum.crypto._ import io.iohk.ethereum.domain.{Address, Transaction} import io.iohk.ethereum.faucet.{FaucetConfig, SupervisorConfig} import io.iohk.ethereum.keystore.KeyStore.DecryptionFailed import io.iohk.ethereum.keystore.{KeyStore, Wallet} -import io.iohk.ethereum.mallet.service.RpcClient import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions.SignedTransactionEnc import io.iohk.ethereum.{crypto, rlp} +import monix.eval.Task import monix.execution.Scheduler.Implicits.global import org.bouncycastle.util.encoders.Hex import org.scalamock.scalatest.MockFactory -import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ -class WalletServiceSpec extends AnyFlatSpec with Matchers with MockFactory with ScalatestRouteTest with ScalaFutures { +class WalletServiceSpec extends AnyFlatSpec with Matchers with MockFactory { "Wallet Service" should "send a transaction" in new TestSetup { @@ -37,8 +35,8 @@ class WalletServiceSpec extends AnyFlatSpec with Matchers with MockFactory with val retTxId = ByteString(Hex.decode("112233")) - (mockRpcClient.getNonce _).expects(config.walletAddress).returning(Right(currentNonce)) - (mockRpcClient.sendTransaction _).expects(ByteString(expectedTx)).returning(Right(retTxId)) + (walletRpcClient.getNonce _).expects(config.walletAddress).returning(Task.pure(Right(currentNonce))) + (walletRpcClient.sendTransaction _).expects(ByteString(expectedTx)).returning(Task.pure(Right(retTxId))) val res = walletService.sendFunds(wallet, Address("0x99")).runSyncUnsafe() @@ -69,7 +67,7 @@ class WalletServiceSpec extends AnyFlatSpec with Matchers with MockFactory with val (prvKey, pubKey) = keyPairToByteStrings(walletKeyPair) val wallet = Wallet(Address(crypto.kec256(pubKey)), prvKey) - val mockRpcClient = mock[RpcClient] + val walletRpcClient = mock[WalletRpcClient] val mockKeyStore = mock[KeyStore] val config: FaucetConfig = FaucetConfig( @@ -87,7 +85,7 @@ class WalletServiceSpec extends AnyFlatSpec with Matchers with MockFactory with shutdownTimeout = 15.seconds ) - val walletService = new WalletService(mockRpcClient, mockKeyStore, config) + val walletService = new WalletService(walletRpcClient, mockKeyStore, config) } } diff --git a/src/test/scala/io/iohk/ethereum/jsonrpc/CheckpointingJRCSpec.scala b/src/test/scala/io/iohk/ethereum/jsonrpc/CheckpointingJRCSpec.scala index a0e3f53fdc..d1efc32a2b 100644 --- a/src/test/scala/io/iohk/ethereum/jsonrpc/CheckpointingJRCSpec.scala +++ b/src/test/scala/io/iohk/ethereum/jsonrpc/CheckpointingJRCSpec.scala @@ -5,7 +5,8 @@ import io.iohk.ethereum.crypto.ECDSASignature import io.iohk.ethereum.jsonrpc.CheckpointingService._ import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController.JsonRpcConfig import io.iohk.ethereum.jsonrpc.JsonRpcError.InvalidParams -import io.iohk.ethereum.nodebuilder.{ApisBuilder, SecureRandomBuilder} +import io.iohk.ethereum.security.SecureRandomBuilder +import io.iohk.ethereum.nodebuilder.ApisBuilder import io.iohk.ethereum.utils.{ByteStringUtils, Config} import io.iohk.ethereum.{Fixtures, NormalPatience, crypto} import monix.eval.Task diff --git a/src/test/scala/io/iohk/ethereum/jsonrpc/FilterManagerSpec.scala b/src/test/scala/io/iohk/ethereum/jsonrpc/FilterManagerSpec.scala index 6f31476782..c44ed7a17c 100644 --- a/src/test/scala/io/iohk/ethereum/jsonrpc/FilterManagerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/jsonrpc/FilterManagerSpec.scala @@ -15,8 +15,8 @@ import io.iohk.ethereum.consensus.blocks.{BlockGenerator, PendingBlock} import io.iohk.ethereum.{NormalPatience, Timeouts, WithActorSystemShutDown} import io.iohk.ethereum.crypto.{ECDSASignature, generateKeyPair} import io.iohk.ethereum.jsonrpc.FilterManager.LogFilterLogs +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.ledger.BloomFilter -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.transactions.PendingTransactionsManager import io.iohk.ethereum.transactions.PendingTransactionsManager.PendingTransaction import io.iohk.ethereum.utils.{FilterConfig, TxPoolConfig} diff --git a/src/test/scala/io/iohk/ethereum/jsonrpc/NetServiceSpec.scala b/src/test/scala/io/iohk/ethereum/jsonrpc/NetServiceSpec.scala index 4ecec066e2..f8c2d8869e 100644 --- a/src/test/scala/io/iohk/ethereum/jsonrpc/NetServiceSpec.scala +++ b/src/test/scala/io/iohk/ethereum/jsonrpc/NetServiceSpec.scala @@ -6,8 +6,8 @@ import java.util.concurrent.atomic.AtomicReference import akka.actor.ActorSystem import akka.testkit.TestProbe import io.iohk.ethereum.jsonrpc.NetService._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network.{Peer, PeerActor, PeerManagerActor} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.utils.{NodeStatus, ServerStatus} import io.iohk.ethereum.{NormalPatience, crypto} import monix.execution.Scheduler.Implicits.global diff --git a/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala b/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala index 52470d2578..911646b4ab 100644 --- a/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala @@ -98,9 +98,6 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout override val enabled: Boolean = true override val interface: String = "" override val port: Int = 123 - override val certificateKeyStorePath = None - override val certificateKeyStoreType = None - override val certificatePasswordFile = None override val corsAllowedOrigins = HttpOriginMatcher.* } diff --git a/src/test/scala/io/iohk/ethereum/keystore/EncryptedKeySpec.scala b/src/test/scala/io/iohk/ethereum/keystore/EncryptedKeySpec.scala index 96517cd2a3..63cf897332 100644 --- a/src/test/scala/io/iohk/ethereum/keystore/EncryptedKeySpec.scala +++ b/src/test/scala/io/iohk/ethereum/keystore/EncryptedKeySpec.scala @@ -2,7 +2,7 @@ package io.iohk.ethereum.keystore import io.iohk.ethereum.crypto import io.iohk.ethereum.domain.Address -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/io/iohk/ethereum/keystore/KeyStoreImplSpec.scala b/src/test/scala/io/iohk/ethereum/keystore/KeyStoreImplSpec.scala index 606e0263f2..343a5e78e1 100644 --- a/src/test/scala/io/iohk/ethereum/keystore/KeyStoreImplSpec.scala +++ b/src/test/scala/io/iohk/ethereum/keystore/KeyStoreImplSpec.scala @@ -6,7 +6,7 @@ import java.nio.file.{FileSystemException, FileSystems, Files, Path} import akka.util.ByteString import io.iohk.ethereum.domain.Address import io.iohk.ethereum.keystore.KeyStore.{DecryptionFailed, IOError, KeyNotFound, PassPhraseTooShort} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.{Config, KeyStoreConfig} import org.apache.commons.io.FileUtils import org.bouncycastle.util.encoders.Hex diff --git a/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala b/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala index 4099b624e9..1705442915 100644 --- a/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala +++ b/src/test/scala/io/iohk/ethereum/ledger/LedgerTestSetup.scala @@ -15,7 +15,7 @@ import io.iohk.ethereum.domain._ import io.iohk.ethereum.ledger.BlockExecutionError.ValidationAfterExecError import io.iohk.ethereum.ledger.Ledger.{PC, PR, VMImpl} import io.iohk.ethereum.mpt.MerklePatriciaTrie -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.Config.SyncConfig import io.iohk.ethereum.utils.{BlockchainConfig, Config, DaoForkConfig} import io.iohk.ethereum.vm.{ProgramError, ProgramResult} diff --git a/src/test/scala/io/iohk/ethereum/network/AsymmetricCipherKeyPairLoaderSpec.scala b/src/test/scala/io/iohk/ethereum/network/AsymmetricCipherKeyPairLoaderSpec.scala index 9bd526dd82..afd8c86cc4 100644 --- a/src/test/scala/io/iohk/ethereum/network/AsymmetricCipherKeyPairLoaderSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/AsymmetricCipherKeyPairLoaderSpec.scala @@ -3,8 +3,8 @@ package io.iohk.ethereum.network import java.io.File import java.nio.file.Files +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import org.bouncycastle.crypto.AsymmetricCipherKeyPair import org.bouncycastle.crypto.params.{ECPrivateKeyParameters, ECPublicKeyParameters} import org.scalatest.flatspec.AnyFlatSpec diff --git a/src/test/scala/io/iohk/ethereum/network/AuthHandshakerSpec.scala b/src/test/scala/io/iohk/ethereum/network/AuthHandshakerSpec.scala index 4eed6c0590..135cebeee3 100644 --- a/src/test/scala/io/iohk/ethereum/network/AuthHandshakerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/AuthHandshakerSpec.scala @@ -5,8 +5,8 @@ import java.net.URI import akka.util.ByteString import io.iohk.ethereum.crypto._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network.rlpx.{AuthHandshakeSuccess, AuthHandshaker, AuthResponseMessage, Secrets} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import org.bouncycastle.crypto.params.{ECPrivateKeyParameters, ECPublicKeyParameters} import org.bouncycastle.crypto.AsymmetricCipherKeyPair import org.bouncycastle.util.encoders.Hex diff --git a/src/test/scala/io/iohk/ethereum/network/AuthInitiateMessageSpec.scala b/src/test/scala/io/iohk/ethereum/network/AuthInitiateMessageSpec.scala index 3421558210..803d6ab6bd 100644 --- a/src/test/scala/io/iohk/ethereum/network/AuthInitiateMessageSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/AuthInitiateMessageSpec.scala @@ -2,8 +2,8 @@ package io.iohk.ethereum.network import akka.util.ByteString import io.iohk.ethereum.crypto._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network.rlpx.{AuthHandshaker, AuthInitiateMessage} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.utils.ByteUtils import org.bouncycastle.crypto.generators.ECKeyPairGenerator import org.bouncycastle.crypto.params.{ECKeyGenerationParameters, ECPublicKeyParameters} diff --git a/src/test/scala/io/iohk/ethereum/network/handshaker/EtcHandshakerSpec.scala b/src/test/scala/io/iohk/ethereum/network/handshaker/EtcHandshakerSpec.scala index 9485e8292e..1edc60af13 100644 --- a/src/test/scala/io/iohk/ethereum/network/handshaker/EtcHandshakerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/handshaker/EtcHandshakerSpec.scala @@ -19,8 +19,8 @@ import io.iohk.ethereum.network.p2p.messages.PV62.{BlockHeaders, GetBlockHeaders import io.iohk.ethereum.network.p2p.messages.WireProtocol.Hello.HelloEnc import io.iohk.ethereum.network.p2p.messages.WireProtocol.{Disconnect, Hello} import io.iohk.ethereum.network.p2p.messages.{Capability, CommonMessages, PV64, ProtocolVersions} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.utils._ +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/io/iohk/ethereum/network/p2p/MessageDecodersSpec.scala b/src/test/scala/io/iohk/ethereum/network/p2p/MessageDecodersSpec.scala index dc53f75079..46edd92f47 100644 --- a/src/test/scala/io/iohk/ethereum/network/p2p/MessageDecodersSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/p2p/MessageDecodersSpec.scala @@ -6,7 +6,7 @@ import io.iohk.ethereum.domain.ChainWeight import io.iohk.ethereum.network.p2p.messages.Capability.Capabilities._ import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions import io.iohk.ethereum.network.p2p.messages._ -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.bouncycastle.util.encoders.Hex import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/io/iohk/ethereum/network/p2p/PeerActorSpec.scala b/src/test/scala/io/iohk/ethereum/network/p2p/PeerActorSpec.scala index c0e29a0e47..7ab69dd152 100644 --- a/src/test/scala/io/iohk/ethereum/network/p2p/PeerActorSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/p2p/PeerActorSpec.scala @@ -32,7 +32,7 @@ import io.iohk.ethereum.network.p2p.messages.WireProtocol._ import io.iohk.ethereum.network.rlpx.RLPxConnectionHandler import io.iohk.ethereum.network.rlpx.RLPxConnectionHandler.RLPxConfiguration import io.iohk.ethereum.network.{ForkResolver, PeerActor, PeerEventBusActor, _} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.utils.{Config, NodeStatus, ServerStatus} import org.bouncycastle.crypto.AsymmetricCipherKeyPair import org.bouncycastle.crypto.params.ECPublicKeyParameters diff --git a/src/test/scala/io/iohk/ethereum/network/p2p/SecureChannelSetup.scala b/src/test/scala/io/iohk/ethereum/network/p2p/SecureChannelSetup.scala index 6a69c3aa6b..55b5e3b3cb 100644 --- a/src/test/scala/io/iohk/ethereum/network/p2p/SecureChannelSetup.scala +++ b/src/test/scala/io/iohk/ethereum/network/p2p/SecureChannelSetup.scala @@ -5,9 +5,9 @@ import java.net.URI import akka.util.ByteString import io.iohk.ethereum.crypto import io.iohk.ethereum.crypto._ +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network._ import io.iohk.ethereum.network.rlpx.{AuthHandshakeSuccess, AuthHandshaker, Secrets} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import org.bouncycastle.crypto.AsymmetricCipherKeyPair import org.bouncycastle.crypto.params.ECPublicKeyParameters import org.bouncycastle.util.encoders.Hex diff --git a/src/test/scala/io/iohk/ethereum/network/p2p/messages/NewBlockSpec.scala b/src/test/scala/io/iohk/ethereum/network/p2p/messages/NewBlockSpec.scala index 724c7049f9..3237068257 100644 --- a/src/test/scala/io/iohk/ethereum/network/p2p/messages/NewBlockSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/p2p/messages/NewBlockSpec.scala @@ -6,7 +6,7 @@ import io.iohk.ethereum.domain.{Block, BlockBody, BlockHeader, ChainWeight} import io.iohk.ethereum.network.p2p.messages.CommonMessages.NewBlock import org.bouncycastle.util.encoders.Hex import NewBlock._ -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.funsuite.AnyFunSuite diff --git a/src/test/scala/io/iohk/ethereum/network/rlpx/RLPxConnectionHandlerSpec.scala b/src/test/scala/io/iohk/ethereum/network/rlpx/RLPxConnectionHandlerSpec.scala index 856e64bd55..848e4f30c6 100644 --- a/src/test/scala/io/iohk/ethereum/network/rlpx/RLPxConnectionHandlerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/network/rlpx/RLPxConnectionHandlerSpec.scala @@ -12,7 +12,7 @@ import io.iohk.ethereum.network.p2p.{MessageDecoder, MessageSerializable} import io.iohk.ethereum.network.p2p.messages.ProtocolVersions import io.iohk.ethereum.network.p2p.messages.WireProtocol.Ping import io.iohk.ethereum.network.rlpx.RLPxConnectionHandler.RLPxConfiguration -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalamock.scalatest.MockFactory import scala.concurrent.duration.FiniteDuration diff --git a/src/test/scala/io/iohk/ethereum/security/SSLContextFactorySpec.scala b/src/test/scala/io/iohk/ethereum/security/SSLContextFactorySpec.scala new file mode 100644 index 0000000000..a797c26096 --- /dev/null +++ b/src/test/scala/io/iohk/ethereum/security/SSLContextFactorySpec.scala @@ -0,0 +1,224 @@ +package io.iohk.ethereum.security + +import java.io.{ByteArrayInputStream, File, FileInputStream, FileOutputStream} +import java.security.{KeyStore, SecureRandom} + +import javax.net.ssl.{KeyManager, TrustManager} +import org.scalamock.scalatest.MockFactory +import org.scalatest.BeforeAndAfterAll +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.io.BufferedSource + +class SSLContextFactorySpec extends AnyFlatSpec with Matchers with MockFactory with BeforeAndAfterAll { + + val fileName: String = "temp.txt" + var file: File = _ + + override def beforeAll { + new FileOutputStream(fileName, false).getFD + file = new File(fileName) + } + + override def afterAll { + file.delete() + } + + val keyStorePath = "mantisCA.p12" + val keyStoreType = "pkcs12" + val passwordFile = "password" + + it should "createSSLContext" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) match { + case Right(ssl) => + ssl.getProtocol shouldBe "TLS" + case Left(error) => fail(error.reason) + } + } + + it should "return a Error because keystore path and password are missing" in new TestSetup( + existingFiles = Nil, + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Certificate keystore path and password file configured but files are missing")) + } + + it should "return a Error because keystore path is missing" in new TestSetup( + existingFiles = List(passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Certificate keystore path configured but file is missing")) + } + + it should "return a Error because password file is missing" in new TestSetup( + existingFiles = List(keyStorePath), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Certificate password file configured but file is missing")) + } + + it should "return a Error because invalid KeyStore Type" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val invalidKeyStoreType = "invalidkeyStoreType" + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = invalidKeyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError(s"Certificate keystore invalid type set: $invalidKeyStoreType")) + } + + it should "return a Error because keystore file creation failed" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Left(new RuntimeException("Certificate keystore file creation failed")), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Certificate keystore file creation failed")) + } + + it should "return a Error because failed to load keystore" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Left(new RuntimeException("Failed to load keyStore")), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Failed to load keyStore")) + } + + it should "return a Error because KeyManager failure" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Left(new RuntimeException("Failed to get KeyManager")), + fGetTrustManager = () => Right(Array.empty) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Invalid Certificate keystore")) + } + + it should "return a Error because TrustManager failure" in new TestSetup( + existingFiles = List(keyStorePath, passwordFile), + fCreateFileInputStream = () => Right(new FileInputStream(file)), + fLoadKeyStore = () => Right(()), + fGetKeyManager = () => Right(Array.empty), + fGetTrustManager = () => Left(new RuntimeException("Failed to get TrustManager")) + ) { + + val sslConfig = SSLConfig( + keyStorePath = keyStorePath, + keyStoreType = keyStoreType, + passwordFile = passwordFile + ) + val response = sSLContextFactory.createSSLContext(sslConfig, new SecureRandom()) + response shouldBe Left(SSLError("Invalid Certificate keystore")) + } + + class TestSetup( + existingFiles: List[String], + fCreateFileInputStream: () => Either[Throwable, FileInputStream], + fLoadKeyStore: () => Either[Throwable, Unit], + fGetKeyManager: () => Either[Throwable, Array[KeyManager]], + fGetTrustManager: () => Either[Throwable, Array[TrustManager]] + ) { + + val sSLContextFactory = new SSLContextFactory { + + override def exist(pathName: String): Boolean = existingFiles.contains(pathName) + + override def createFileInputStream(pathName: String): Either[Throwable, FileInputStream] = + fCreateFileInputStream() + + override def getReader(passwordFile: String): BufferedSource = new BufferedSource( + new ByteArrayInputStream("password".getBytes) + ) + + override def loadKeyStore( + keyStoreFile: FileInputStream, + passwordCharArray: Array[Char], + keyStore: KeyStore + ): Either[Throwable, Unit] = + fLoadKeyStore() + + override def getKeyManager( + keyStore: KeyStore, + passwordCharArray: Array[Char] + ): Either[Throwable, Array[KeyManager]] = fGetKeyManager() + + override def getTrustManager(keyStore: KeyStore): Either[Throwable, Array[TrustManager]] = + fGetTrustManager() + } + } +} diff --git a/src/test/scala/io/iohk/ethereum/transactions/PendingTransactionsManagerSpec.scala b/src/test/scala/io/iohk/ethereum/transactions/PendingTransactionsManagerSpec.scala index b9cbaae7ad..ce300f2eb1 100644 --- a/src/test/scala/io/iohk/ethereum/transactions/PendingTransactionsManagerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/transactions/PendingTransactionsManagerSpec.scala @@ -7,6 +7,7 @@ import akka.pattern.ask import akka.testkit.TestProbe import akka.util.ByteString import io.iohk.ethereum.domain.{Address, SignedTransaction, SignedTransactionWithSender, Transaction} +import io.iohk.ethereum.security.SecureRandomBuilder import io.iohk.ethereum.network.PeerActor.Status.Handshaked import io.iohk.ethereum.network.PeerEventBusActor.PeerEvent import io.iohk.ethereum.network.PeerManagerActor.Peers @@ -15,7 +16,6 @@ import io.iohk.ethereum.network.p2p.messages.CommonMessages.SignedTransactions import io.iohk.ethereum.network.{EtcPeerManagerActor, Peer, PeerId, PeerManagerActor} import io.iohk.ethereum.transactions.PendingTransactionsManager._ import io.iohk.ethereum.{NormalPatience, Timeouts, crypto} -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.transactions.SignedTransactionsFilterActor.ProperSignedTransactions import io.iohk.ethereum.utils.TxPoolConfig import org.scalatest.concurrent.ScalaFutures diff --git a/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala b/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala index 0b829bfd4f..e339f909ea 100644 --- a/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala +++ b/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala @@ -5,10 +5,10 @@ import io.iohk.ethereum.crypto._ import io.iohk.ethereum.domain.{Account, Address, UInt256} import io.iohk.ethereum.Fixtures.{Blocks => BlockFixtures} import MockWorldState._ -import io.iohk.ethereum.nodebuilder.SecureRandomBuilder import io.iohk.ethereum.utils.ByteUtils import org.bouncycastle.util.encoders.Hex import Fixtures.blockchainConfig +import io.iohk.ethereum.security.SecureRandomBuilder import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers diff --git a/src/universal/conf/faucet.conf b/src/universal/conf/faucet.conf index bbfe4b91b0..cab8873ac4 100644 --- a/src/universal/conf/faucet.conf +++ b/src/universal/conf/faucet.conf @@ -21,8 +21,26 @@ faucet { # Transaction value tx-value = 1000000000000000000 - # Address of Ethereum node used to send the transaction - rpc-address = "http://127.0.0.1:8546/" + rpc-client { + # Address of Ethereum node used to send the transaction + rpc-address = "http://127.0.0.1:8546/" + + # certificate of Ethereum node used to send the transaction when use HTTP(S) + certificate = null + #certificate { + # Path to the keystore storing the certificates (used only for https) + # null value indicates HTTPS is not being used + # keystore-path = "tls/mantisCA.p12" + + # Type of certificate keystore being used + # null value indicates HTTPS is not being used + # keystore-type = "pkcs12" + + # File with the password used for accessing the certificate keystore (used only for https) + # null value indicates HTTPS is not being used + # password-file = "tls/password" + #} + } # How often can a single IP address send a request min-request-interval = 1.minute @@ -80,17 +98,21 @@ mantis { # Listening port of JSON-RPC HTTP(S) endpoint port = 8099 + # certificate when use JSON-RPC HTTP(S) + certificate = null + #certificate { # Path to the keystore storing the certificates (used only for https) # null value indicates HTTPS is not being used - certificate-keystore-path = null + # keystore-path = "tls/mantisCA.p12" # Type of certificate keystore being used # null value indicates HTTPS is not being used - certificate-keystore-type = null + # keystore-type = "pkcs12" # File with the password used for accessing the certificate keystore (used only for https) # null value indicates HTTPS is not being used - certificate-password-file = null + # password-file = "tls/password" + #} # Domains allowed to query RPC endpoint. Use "*" to enable requests from # any domain. diff --git a/tls/gen-cert.sh b/tls/gen-cert.sh new file mode 100755 index 0000000000..3c9a54ecd2 --- /dev/null +++ b/tls/gen-cert.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +echo `dirname $0` +cd `dirname $0` + +export PW=`pwgen -Bs 10 1` +echo $PW > ./password + +rm ./mantisCA.p12 + +keytool -genkeypair \ + -keystore mantisCA.p12 \ + -storetype PKCS12 \ + -dname "CN=127.0.0.1" \ + -ext "san=ip:127.0.0.1,dns:localhost" \ + -keypass:env PW \ + -storepass:env PW \ + -keyalg RSA \ + -keysize 4096 \ + -validity 9999 \ + -ext KeyUsage:critical="keyCertSign" \ + -ext BasicConstraints:critical="ca:true" diff --git a/tls/mantisCA.p12 b/tls/mantisCA.p12 new file mode 100644 index 0000000000..fa57879aa9 Binary files /dev/null and b/tls/mantisCA.p12 differ diff --git a/tls/password b/tls/password new file mode 100644 index 0000000000..a779858077 --- /dev/null +++ b/tls/password @@ -0,0 +1 @@ +gJWA4qV4P4