solution: allow TLS configuration for proxy

This commit is contained in:
Igor Artamonov
2020-03-20 20:22:01 -04:00
parent 26f8dd294e
commit d4bd180e23
25 changed files with 882 additions and 72 deletions

View File

@@ -29,6 +29,31 @@ It configures following:
- no JSON RPC is configured
- upstreams configuration is configured in the file `upstreams.yaml`
.Configuration
|===
| Name | Example | Description
a| `version`
|
| Version of the format of the config
a| `port`
a| `port: 12449`
| Port to bind gRPC server. `2449` by default
a| `tls`
|
| TLS configuration for gRPC. See link:07-authentication.adoc[Authentication] for details
a| `proxy`
|
| Proxy configuration
a| `upstreams`
|
| Upstreams configuration
|===
=== Enabling JSON RPC proxy
.Example proxy:
@@ -51,4 +76,49 @@ upstreams:
With that configuration Dshackle starts a JSON RPC proxy:
- JSON RPC server is listening on `0.0.0.0:8080`
- `http://0.0.0.0:8080/eth` provides access to Ethereum API routed to an available upstream
- `http://0.0.0.0:8080/eth` provides access to Ethereum API routed to an available upstream
.Full configuration:
[source,yaml]
----
proxy:
port: 8080
tls:
enabled: true
server:
certificate: server.crt
key: server.p8.key
client:
require: true
ca: ca.crt
routes:
- id: eth
blockchain: ethereum
- id: etc
blockchain: ethereum_classic
----
.Proxy configuration
|===
| Name | Example | Description
a| `host`
a| `host: 0.0.0.0`
| Host to bind proxy server. `127.0.0.1` by default
a| `port`
a| `port: 8545`
| Port to bind proxy server. `8080` by default
a| `enabled`
a| `enabled: true`
| Enable/disable proxy server
a| `tls`
|
| TLS configuration for proxy. See link:07-authentication.adoc[Authentication] for details
a| `routes`
|
| List of endpoints to proxy
|===

View File

@@ -12,16 +12,22 @@ generate certificates, but the traditional `openssl` tool can be used as well.
==== Setup Server certificate
.Generate a server certificate
.Generate a Certificate Authority
[source,bash]
----
SERVER_CA="ca.myhost.dev"
SERVER_IP="127.0.0.1"
ORG="My Company"
ORG_UNIT="Blockchain"
export SERVER_CA="ca.myhost.dev"
export ORG="My Company"
export ORG_UNIT="Blockchain"
certstrap init --common-name "$SERVER_CA" --passphrase "" -o "$ORG" -ou "$ORG_UNIT CA"
certstrap request-cert -ip $SERVER_IP --passphrase "" -o "$ORG" -ou "$ORG_UNIT Server"
openssl pkcs8 -topk8 -inform PEM -outform PEM -in out/$SERVER_CA.key -out out/$SERVER_CA.p8.key -nocrypt
----
.Generate a Server Certificate
----
export SERVER_IP="127.0.0.1"
certstrap request-cert -ip $SERVER_IP --common-name $SERVER_IP --passphrase "" -o "$ORG" -ou "$ORG_UNIT Server"
certstrap sign $SERVER_IP --CA $SERVER_CA
openssl pkcs8 -topk8 -inform PEM -outform PEM -in out/$SERVER_IP.key -out out/$SERVER_IP.p8.key -nocrypt
@@ -30,8 +36,9 @@ openssl pkcs8 -topk8 -inform PEM -outform PEM -in out/$SERVER_IP.key -out out/$S
You are going to get following files now in `out` directory:
- `ca.myhost.dev.crt` your Certification Authority to sign or verify other certificates
- `127.0.0.1.crt` server certificate, 127.0.0.1 is an IP address supposed to be used by that instance, and we use local
host for demo purposes. For production use a real IP must be used.
- `ca.myhost.dev.p8.key` is the private key for Certification Authority in PKSC 8 format
- `127.0.0.1.crt` server certificate, 127.0.0.1 is an IP address supposed to be used by that instance, and we use local host for demo purposes.
For production use a real IP must be used.
- `127.0.0.1.p8.key` is private key for certificate in PKCS 8 format, it needed by Dshackle server to use certificate
Copy those files to directory with Dshackle and update configuration.
@@ -144,5 +151,43 @@ upstreams:
key: client_1.p8.key
----
Now if you run second server it will connect to first server ("upstream") running on port 2449, will verify upstream
with certificate `ca.myhost.dev.crt` and authenticate itself by using pair of `client_1.crt` and `client_1.p8.key`
Now if you run second server it will connect to first server ("upstream") running on port 2449, will verify upstream with certificate `ca.myhost.dev.crt` and authenticate itself by using pair of `client_1.crt` and `client_1.p8.key`
=== Server TLS configuration
|===
| Name | Example | Description
a| `enabled`
a|
[source,yaml]
----
tls:
enabled: true
----
| Enabled or disable TLS. By default it checks if certificate is set, and then enables it. But if you enable the TLS
but didn't specify the certificate or key, then the DShackle will fails to start with error.
a| `server.certificate`, `server.key`
a|
[source,yaml]
----
tls:
server:
certificate: server.com.crt
key: server.com.p8.key
----
| Path to certificate and certificate private key
a| `client.ca`, `client.required`
a|
[source,yaml]
----
tls:
client:
ca: ca.crt
required: true
----
a| Path to CA used to authenticate incoming connections, used if `required: true`
|===

View File

@@ -34,7 +34,8 @@ class ProxyStarter(
@Autowired private val env: Environment,
@Autowired private val readRpcJson: ReadRpcJson,
@Autowired private val writeRpcJson: WriteRpcJson,
@Autowired private val nativeCall: NativeCall
@Autowired private val nativeCall: NativeCall,
@Autowired private val tlsSetup: TlsSetup
) {
companion object {
@@ -48,7 +49,7 @@ class ProxyStarter(
log.debug("Proxy server is not configured")
return
}
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall)
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall, tlsSetup)
server.start()
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.config.AuthConfig
import io.grpc.netty.GrpcSslContexts
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class TlsSetup(
@Autowired val fileResolver: FileResolver
) {
companion object {
private val log = LoggerFactory.getLogger(TlsSetup::class.java)
}
fun setupServer(category: String, config: AuthConfig.ServerTlsAuth): SslContext? {
val mustBeSecure = config.enabled != null && config.enabled!!
val tlsDisabled = config.enabled != null && !config.enabled!!
var hasServerCertificate = true
if (!tlsDisabled) {
if (StringUtils.isEmpty(config.certificate)) {
if (mustBeSecure) {
log.error("tls.server.certificate property for $category is not set (path to server TLS certificate) but TLS is enabled")
throw IllegalArgumentException("Certificate not set")
}
hasServerCertificate = false
}
if (StringUtils.isEmpty(config.key)) {
if (mustBeSecure) {
log.error("tls.server.key property for $category is not set (path to server TLS certificate key) but TLS is enabled")
throw IllegalArgumentException("Certificate Key not set")
}
hasServerCertificate = false
}
}
if (mustBeSecure || (!tlsDisabled && hasServerCertificate)) {
log.info("Using TLS for $category")
val sslContextBuilder = SslContextBuilder.forServer(
fileResolver.resolve(config.certificate!!),
fileResolver.resolve(config.key!!)
)
if (StringUtils.isNotEmpty(config.clientCa)) {
log.info("Using TLS for client authentication for $category")
sslContextBuilder.trustManager(
fileResolver.resolve(config.clientCa!!)
)
if (config.clientRequire != null && config.clientRequire!!) {
sslContextBuilder.clientAuth(ClientAuth.REQUIRE)
}
} else if (config.clientRequire != null && config.clientRequire!!) {
throw IllegalArgumentException("Client Certificate not set")
} else {
log.warn("Trust all clients for $category")
}
return sslContextBuilder.build()
} else {
log.warn("Using insecure transport for $category")
}
return null
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import org.slf4j.LoggerFactory
class AuthConfig {
companion object {
private val log = LoggerFactory.getLogger(AuthConfig::class.java)
}
open class ClientAuth {
var type: String? = null
}
class ClientBasicAuth(
val username: String,
val password: String
) : ClientAuth()
class ClientTlsAuth(
var ca: String? = null,
var certificate: String? = null,
var key: String? = null
) : ClientAuth()
/**
* Example config:
* ```
* enabled: false
* server:
* certificate: "127.0.0.1.crt"
* key: "127.0.0.1.p8.key"
* client:
* require: false
* ca: "ca.dshackle.test.crt"
* ```
*/
open class ServerTlsAuth {
var enabled: Boolean? = null
var certificate: String? = null
var key: String? = null
var clientRequire: Boolean? = null
var clientCa: String? = null
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.nodes.MappingNode
class AuthConfigReader : YamlConfigReader() {
companion object {
private val log = LoggerFactory.getLogger(AuthConfigReader::class.java)
}
fun readClientBasicAuth(node: MappingNode?): AuthConfig.ClientBasicAuth? {
return getMapping(node, "basic-auth")?.let { authNode ->
val username = getValueAsString(authNode, "username")
val password = getValueAsString(authNode, "password")
if (username != null && password != null) {
AuthConfig.ClientBasicAuth(username, password)
} else {
log.warn("Basic auth is not fully configured")
null
}
}
}
fun readClientTls(node: MappingNode?): AuthConfig.ClientTlsAuth? {
return getMapping(node, "tls")?.let { authNode ->
val auth = AuthConfig.ClientTlsAuth()
auth.ca = getValueAsString(authNode, "ca")
auth.certificate = getValueAsString(authNode, "certificate")
auth.key = getValueAsString(authNode, "key")
auth
}
}
/**
* Example config:
* ```
* enabled: false
* server:
* certificate: "127.0.0.1.crt"
* key: "127.0.0.1.p8.key"
* client:
* require: false
* ca: "ca.dshackle.test.crt"
* ```
*/
fun readServerTls(node: MappingNode?): AuthConfig.ServerTlsAuth? {
return getMapping(node, "tls")?.let { node ->
val auth = AuthConfig.ServerTlsAuth()
getValueAsBool(node, "enabled")?.let {
auth.enabled = it
}
getMapping(node, "server")?.let { node ->
auth.certificate = getValueAsString(node, "certificate")
auth.key = getValueAsString(node, "key")
}
getMapping(node, "client")?.let { node ->
getValueAsBool(node, "require")?.let {
auth.clientRequire = it
}
auth.clientCa = getValueAsString(node, "ca")
}
auth
}
}
}

View File

@@ -38,6 +38,11 @@ class ProxyConfig {
*/
var port: Int = 8080
/**
* TLS Auth required from clients.
*/
var tls: AuthConfig.ServerTlsAuth? = null
/**
* List of available routes
*/

View File

@@ -33,10 +33,10 @@ class ProxyConfigReader : YamlConfigReader() {
}
private var filename = "dshackle.yaml"
private val authConfigReader = AuthConfigReader()
fun read(input: InputStream): ProxyConfig? {
val yaml = Yaml()
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
val configNode = readNode(input)
return read(getMapping(configNode, "proxy"))
}
@@ -73,8 +73,10 @@ class ProxyConfigReader : YamlConfigReader() {
}
}
if (config.routes.isEmpty()) {
log.warn("Proxy config has no routes")
return null
}
config.tls = authConfigReader.readServerTls(input)
return config
}

View File

@@ -17,7 +17,6 @@ package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.Defaults
import java.net.URI
import java.time.Duration
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
@@ -81,7 +80,7 @@ class UpstreamsConfig {
class GrpcConnection : UpstreamConnection() {
var host: String? = null
var port: Int = 0
var auth: TlsAuth? = null
var auth: AuthConfig.ClientTlsAuth? = null
}
class EthereumConnection : UpstreamConnection() {
@@ -90,29 +89,16 @@ class UpstreamsConfig {
}
class HttpEndpoint(val url: URI) {
var basicAuth: BasicAuth? = null
var tls: TlsAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
var tls: AuthConfig.ClientTlsAuth? = null
}
class WsEndpoint(val url: URI) {
var origin: URI? = null
var basicAuth: BasicAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
}
open class Auth {
var type: String? = null
}
class BasicAuth(
val username: String,
val password: String
) : Auth()
class TlsAuth(
var ca: String? = null,
var certificate: String? = null,
var key: String? = null
) : Auth()
//TODO make it unmodifiable after initial load
class Labels: HashMap<String, String>() {

View File

@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.config
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.ScalarNode
import reactor.util.function.Tuples
import java.io.InputStream
import java.io.InputStreamReader
import java.lang.IllegalArgumentException
import java.net.URI
import java.time.Duration
class UpstreamsConfigReader : YamlConfigReader() {
private val log = LoggerFactory.getLogger(UpstreamsConfigReader::class.java)
private val authConfigReader = AuthConfigReader()
fun read(input: InputStream): UpstreamsConfig {
val yaml = Yaml()
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
val configNode = readNode(input)
val config = UpstreamsConfig()
config.version = getValueAsString(configNode, "version")
@@ -65,8 +62,8 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsString(node, "url")?.let { url ->
val http = UpstreamsConfig.HttpEndpoint(URI(url))
connection.rpc = http
http.basicAuth = readBasicAuth(node)
http.tls = readTls(node)
http.basicAuth = authConfigReader.readClientBasicAuth(node)
http.tls = authConfigReader.readClientTls(node)
}
}
getMapping(connConfigNode, "ws")?.let { node ->
@@ -76,7 +73,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsString(node, "origin")?.let { origin ->
ws.origin = URI(origin)
}
ws.basicAuth = readBasicAuth(node)
ws.basicAuth = authConfigReader.readClientBasicAuth(node)
}
}
} else {
@@ -97,7 +94,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsInt(connConfigNode, "port")?.let {
connection.port = it
}
connection.auth = readTls(connConfigNode)
connection.auth = authConfigReader.readClientTls(connConfigNode)
} else {
log.error("Upstream at #0 has invalid configuration")
}
@@ -194,27 +191,4 @@ class UpstreamsConfigReader : YamlConfigReader() {
return options
}
private fun readBasicAuth(node: MappingNode?): UpstreamsConfig.BasicAuth? {
return getMapping(node, "basic-auth")?.let { authNode ->
val username = getValueAsString(authNode, "username")
val password = getValueAsString(authNode, "password")
if (username != null && password != null) {
UpstreamsConfig.BasicAuth(username, password)
} else {
log.warn("Basic auth is not fully configured")
null
}
}
}
private fun readTls(node: MappingNode?): UpstreamsConfig.TlsAuth? {
return getMapping(node, "tls")?.let { authNode ->
val auth = UpstreamsConfig.TlsAuth()
auth.ca = getValueAsString(authNode, "ca")
auth.certificate = getValueAsString(authNode, "certificate")
auth.key = getValueAsString(authNode, "key")
auth
}
}
}

View File

@@ -16,14 +16,26 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.grpc.Chain
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.ScalarNode
import java.io.InputStream
import java.io.InputStreamReader
open class YamlConfigReader {
private val envVariables = EnvVariables()
fun readNode(input: String): MappingNode {
return readNode(input.byteInputStream())
}
fun readNode(input: InputStream): MappingNode {
val yaml = Yaml()
return asMappingNode(yaml.compose(InputStreamReader(input)))
}
protected fun hasAny(mappingNode: MappingNode?, key: String): Boolean {
if (mappingNode == null) {
return false

View File

@@ -17,9 +17,11 @@ package io.emeraldpay.dshackle.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall
import io.netty.buffer.Unpooled
import io.netty.handler.ssl.SslContextBuilder
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
@@ -29,6 +31,7 @@ import reactor.netty.http.server.HttpServer
import reactor.netty.http.server.HttpServerRequest
import reactor.netty.http.server.HttpServerResponse
import reactor.netty.http.server.HttpServerRoutes
import java.io.File
import java.util.function.BiFunction
/**
@@ -38,7 +41,8 @@ class ProxyServer(
private var config: ProxyConfig,
private val readRpcJson: ReadRpcJson,
private val writeRpcJson: WriteRpcJson,
private val nativeCall: NativeCall
private val nativeCall: NativeCall,
private val tlsSetup: TlsSetup
) {
companion object {
@@ -51,9 +55,17 @@ class ProxyServer(
return
}
log.info("Listening Proxy on ${config.host}:${config.port}")
val server: DisposableServer = HttpServer.create()
var serverBuilder = HttpServer.create()
.host(config.host)
.port(config.port)
config.tls?.let { tls ->
tlsSetup.setupServer("proxy", tls)?.let { sslContext ->
serverBuilder = serverBuilder.secure { secure -> secure.sslContext(sslContext) }
}
}
val server: DisposableServer = serverBuilder
.route(this::setupRoutes)
.bindNow()
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.EmptyReader
import io.emeraldpay.dshackle.reader.Reader
@@ -45,7 +46,7 @@ class EthereumWs(
.builder<BlockJson<TransactionRefJson>>()
.name("new-blocks")
.build()
var basicAuth: UpstreamsConfig.BasicAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
private var blockCache: Reader<BlockHash, BlockJson<TransactionRefJson>> = EmptyReader()

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.startup.UpstreamChange
@@ -45,7 +46,7 @@ class GrpcUpstreams(
private val host: String,
private val port: Int,
private val objectMapper: ObjectMapper,
private val auth: UpstreamsConfig.TlsAuth? = null,
private val auth: AuthConfig.ClientTlsAuth? = null,
private val fileResolver: FileResolver
) {
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
@@ -133,7 +134,7 @@ class GrpcUpstreams(
return Flux.fromIterable(removed + added)
}
internal fun withTls(auth: UpstreamsConfig.TlsAuth): SslContext {
internal fun withTls(auth: AuthConfig.ClientTlsAuth): SslContext {
val sslContext = SslContextBuilder.forClient()
.clientAuth(ClientAuth.REQUIRE)
sslContext.trustManager(fileResolver.resolve(auth.ca!!).inputStream())

View File

@@ -0,0 +1,191 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.config.AuthConfig
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.OpenSslServerContext
import spock.lang.Specification
import sun.security.x509.X509CertImpl
class TlsSetupSpec extends Specification {
TlsSetup tlsSetup = new TlsSetup(new FileResolver(new File("src/test/resources/tls-local")))
def "TLS disabled"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: false
)
when:
def act = tlsSetup.setupServer("test", config)
then:
act == null
}
def "TLS enabled"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.p8.key"
)
when:
def act = tlsSetup.setupServer("test", config)
then:
act != null
act.server
!act.client
with((OpenSslServerContext) act) {
act.clientAuth == ClientAuth.NONE
with((X509CertImpl) keyCertChain[0]) {
getIssuerDN().name == "CN=ca.myhost.dev, OU=Blockchain CA, O=My Company"
}
}
}
def "TLS enabled and required from client"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.p8.key",
clientRequire: true,
clientCa: "ca.myhost.dev.crt"
)
when:
def act = tlsSetup.setupServer("test", config)
then:
act != null
act.server
!act.client
with((OpenSslServerContext) act) {
act.clientAuth == ClientAuth.REQUIRE
with((X509CertImpl) keyCertChain[0]) {
getIssuerDN().name == "CN=ca.myhost.dev, OU=Blockchain CA, O=My Company"
}
}
}
def "Fail if certificate not set"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
key: "127.0.0.1.p8.key",
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
t.message == "Certificate not set"
}
def "Fail if certificate key not set"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt"
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
t.message == "Certificate Key not set"
}
def "Fail if certificate not exists"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "none.crt",
key: "127.0.0.1.p8.key",
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
def "Fail if certificate key not exists"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "none.p8.key",
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
def "Fail if certificate key is invalid"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.key",
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
def "Fail if client certificate not set but required"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.p8.key",
clientRequire: true
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
def "Fail if client certificate not exists"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.p8.key",
clientRequire: true,
clientCa: "none.crt"
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
def "Fail if client certificate is invalid"() {
setup:
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.p8.key",
clientRequire: true,
clientCa: "ca.myhost.dev.key"
)
when:
tlsSetup.setupServer("test", config)
then:
def t = thrown(IllegalArgumentException)
}
}

View File

@@ -0,0 +1,62 @@
package io.emeraldpay.dshackle.config
import spock.lang.Specification
class AuthConfigReaderSpec extends Specification {
AuthConfigReader reader = new AuthConfigReader()
def "Read basic-auth for client"() {
setup:
def yaml =
"basic-auth:\n" +
" username: 9c199ad8f281f20154fc258fe41a6814\n" +
" password: 258fe4149c199ad8f2811a68f20154fc"
when:
def act = reader.readClientBasicAuth(reader.readNode(yaml))
then:
act != null
act.username == "9c199ad8f281f20154fc258fe41a6814"
act.password == "258fe4149c199ad8f2811a68f20154fc"
}
def "Read tls for client"() {
setup:
def yaml =
"tls:\n" +
" ca: /etc/ca.myservice.com.crt\n" +
" certificate: /etc/client1.myservice.com.crt\n" +
" key: /etc/client1.myservice.com.key"
when:
def act = reader.readClientTls(reader.readNode(yaml))
then:
act != null
act.ca == "/etc/ca.myservice.com.crt"
act.certificate == "/etc/client1.myservice.com.crt"
act.key == "/etc/client1.myservice.com.key"
}
def "Read tls for server"() {
setup:
def yaml =
"tls:\n" +
" enabled: true\n" +
" server:\n" +
" certificate: \"/etc/client1.myservice.com.crt\"\n" +
" key: \"/etc/client1.myservice.com.key\"\n" +
" client:\n" +
" require: false\n" +
" ca: /etc/ca.myservice.com.crt"
when:
def act = reader.readServerTls(reader.readNode(yaml))
then:
act != null
act.enabled != null
act.enabled
act.certificate == "/etc/client1.myservice.com.crt"
act.key == "/etc/client1.myservice.com.key"
act.clientRequire != null
!act.clientRequire
act.clientCa == "/etc/ca.myservice.com.crt"
}
}

View File

@@ -64,7 +64,7 @@ class UpstreamsConfigReaderSpec extends Specification {
with((UpstreamsConfig.EthereumConnection)connection) {
rpc.url == new URI("https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2")
rpc.basicAuth != null
with((UpstreamsConfig.BasicAuth)rpc.basicAuth) {
with((AuthConfig.ClientBasicAuth) rpc.basicAuth) {
username == "4fc258fe41a68149c199ad8f281f2015"
password == "1a68f20154fc258fe4149c199ad8f281"
}

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons
@@ -42,7 +43,8 @@ class ProxyServerSpec extends Specification {
new ProxyConfig(),
new ReadRpcJson(TestingCommons.objectMapper()),
writeRpcJson,
nativeCall
nativeCall,
new TlsSetup(TestingCommons.fileResolver())
)
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesFactory
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
@@ -79,4 +80,8 @@ class TestingCommons {
static CachesFactory emptyCaches() {
return new CachesFactory(objectMapper(), new StandardEnvironment())
}
static FileResolver fileResolver() {
return new FileResolver(new File("src/test/resources"))
}
}

View File

@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEmDCCAoCgAwIBAgIQDTubkUXjTKQ5k1GApHel5jANBgkqhkiG9w0BAQsFADBF
MRMwEQYDVQQKEwpNeSBDb21wYW55MRYwFAYDVQQLEw1CbG9ja2NoYWluIENBMRYw
FAYDVQQDEw1jYS5teWhvc3QuZGV2MB4XDTIwMDMyMDIzMTEyN1oXDTIxMDkyMDIz
MTAzNFowRTETMBEGA1UEChMKTXkgQ29tcGFueTEaMBgGA1UECxMRQmxvY2tjaGFp
biBTZXJ2ZXIxEjAQBgNVBAMTCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANMdYDhfCvUhcEPcKIQXvxFrIy2HUOM9soplutOAUv6dbTOs
TeCcpUVgt62sJZcPnE2Iy1nYHeVl+N7vuovUxtzYtfW4rGWprjth4rVcRHkWPwz5
QcyH6GwxsUi/0dEtboOxyqqf3W5C+V0+AJYGZ/bI94pGFOMC4+2HTkF8gCDORWKu
SG67SqiD9HL0LDrEdpq+IJOevW7t/Oqz+DWcO+VAi1T62xup4JL+WJI1UGtDPeET
0SRq3kXb8n4te58Y6NXjyodYlon+BESY42JPw4Uw6rjcmDHQVphg5Dtl343OcfN9
CNehtXavuszQdRU9uY1RUdVsvg44xhR60huFJ80CAwEAAaOBgzCBgDAOBgNVHQ8B
Af8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW
BBT9J7UEZRalIioijC1h4WsrRr9bpDAfBgNVHSMEGDAWgBRkRRFBxmIJWihWWm42
q3T1nI5IYDAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQAC3AN0
8Ptp6xCOhZ8ja/RLKdF97WGyJS04KoPXyyB0qmh1zpYzHtcUcy6Zw41CiwS9d+7s
XfEi780ifmL9XOGxNWTFBvwLMJGq4ukdt//q/y5nhNdINbbZ+LNvZna9gPEBW0cK
YR5HEqMMk2eW2Lgo/pg4ska7leI/7ESoVqehNv3Pt9ewVLq38xecRC1K/Q2Z6vlU
zt+RbLYW7W7jIXc/cH7TUCDG9sT4tRptTlJdq9sBPVFFBXEZ+QZlY6b0ipeelqnL
Wrbsc8XWc/WogEEovBgDpqtqas+jfPI5x9DnopfaSeMP9m9BXYEk+D2nnukw5anp
tid4kSqTUOL8q8wTkEnvTd8nqXfFTC3H+Xe5OMGifXkJTRoQsqFS4M/2jLI36MpN
KHlNG3O5B1ML+vB0SedxaOD4PwWYQwq0kDWMOHOhe4OwyGgdr8MmPCzNFLbx5ppm
apMAco1RXzN/MaBJsYaq5MrTJw4XF8H1CMIEb4GHWb/9gLf4cPetFWzud48eg+YL
XADs+uzCGB0+lDMllVpyU+BSZtDUF77PWk1C5/33JNMg8Wzdj/tXs489B6AG1yED
fqcihzkQNp5WvwCtO5gY/+AYWZQ/zD0ICqjPfzofs7rb+s3qi8mRVfJJ/qOdGzxw
9NZPNkcfB8utd/pGbr4jEeHh+8Ds9vUgZFLXNw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA0x1gOF8K9SFwQ9wohBe/EWsjLYdQ4z2yimW604BS/p1tM6xN
4JylRWC3rawllw+cTYjLWdgd5WX43u+6i9TG3Ni19bisZamuO2HitVxEeRY/DPlB
zIfobDGxSL/R0S1ug7HKqp/dbkL5XT4AlgZn9sj3ikYU4wLj7YdOQXyAIM5FYq5I
brtKqIP0cvQsOsR2mr4gk569bu386rP4NZw75UCLVPrbG6ngkv5YkjVQa0M94RPR
JGreRdvyfi17nxjo1ePKh1iWif4ERJjjYk/DhTDquNyYMdBWmGDkO2Xfjc5x830I
16G1dq+6zNB1FT25jVFR1Wy+DjjGFHrSG4UnzQIDAQABAoIBAQCX1uj9ol4fMI2u
QQpi9zFVNdl3RXvH9PgU0lYtCH6o4lFIeQUKJ6A25fk10Dq5C2E/4sNfOzFFbLIy
pfll2QOuk69LrCdSd1f5Hc4Q4uvcq0Nt8ViB4r4oExWPXWdrK2HxFk7NqW15gHIZ
vh5tyO29cY2Yxg7/t3R3wnlmYEVHUcS7HmhzgDveNzA0VLza3765ntgwXypY8N2j
heEQC1h5kMCurcKJyRXmlsXPRWizX0UBWDrMHFeqyhrH0BlRSFTNC3sKmyYaJQmp
daPNRr4zO0yfm8utVSbNHX2OM5DpIO1Ecq9Sd43QI+ATAtxFrhPoYK1rwll267CV
cJCRbz+BAoGBAPzz/1Xq8s1lGZ6eWz7H1JlzxYLH+TzQfrV1ym7xJgR/b3rVXiJ+
D+qL8zUJDa5xZyflXB7zCg4I7ALmNwMJzVLdIOpEntHK2NtRJ2rp4qH+kMXojays
zOGYfbRLNVe+mgAK9Pu8eOi8NzXqkB/S8rml3xqSOpUFsvlc2qiYhGdXAoGBANWo
XcQDpisRFcrn3J0+pKU57ZIRjxyOTDlEwH7k+x+PprCRFki80kW22u4l22FdDaip
s4vCuAm5tmEogEjINU6ZhSKHxonjaGXfzuZ3gAMk/PN7zFazlgfYEKng+fa1YuZ+
3Ubzq6py8enoffJ/PSF/lClKlV5sxjyilxeZmOd7AoGBAMtaJHUf0l4I3tXDnLsV
zvYmOixvMxEO1C5xKXKS7utCv45SJcE48vat17FVO+h3RmSuYKaI4BZ0Wbfi92q7
4JKzLpjm3X7uwfNehH/Q0t9EVYKk9/BPYs4h1zywEYwesJNEO7p8w/7mAMSZc5AB
+BwDGx6zW+EdmcoaOba8FgU1AoGAV2WftWaoukUq3O0rWUceolenznBQUiYDGAn/
k+imsKpaTS+MJgTXHp1FwNTLgHBH/g4s26azEYdeCzA+CYecBqLVyuIvXIghVErQ
n4WSX7bpoc+qLm0Xme3QIy1cEobwBckvSq6yMe8C9eOcYW2a2/EL8jgIEa/9ByCb
HZQ+77ECgYEAxR1eoxc/XV9rcftdaRl7+Db9Qvnhfwu7MFQ3lgomol1N1ckSjwnO
wo/HX4+8cMS5QN11d8l2hf+7TyuRCrQBLrYYkrVWb4Z+ote3ejrAsDg90xjOFYWf
MWSy7kPeDl3JTEdNPFIIa28EQhZYupD0ihBoVspz2eQ1Y66BOfqC9nU=
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTHWA4Xwr1IXBD
3CiEF78RayMth1DjPbKKZbrTgFL+nW0zrE3gnKVFYLetrCWXD5xNiMtZ2B3lZfje
77qL1Mbc2LX1uKxlqa47YeK1XER5Fj8M+UHMh+hsMbFIv9HRLW6Dscqqn91uQvld
PgCWBmf2yPeKRhTjAuPth05BfIAgzkVirkhuu0qog/Ry9Cw6xHaaviCTnr1u7fzq
s/g1nDvlQItU+tsbqeCS/liSNVBrQz3hE9Ekat5F2/J+LXufGOjV48qHWJaJ/gRE
mONiT8OFMOq43Jgx0FaYYOQ7Zd+NznHzfQjXobV2r7rM0HUVPbmNUVHVbL4OOMYU
etIbhSfNAgMBAAECggEBAJfW6P2iXh8wja5BCmL3MVU12XdFe8f0+BTSVi0Ifqji
UUh5BQonoDbl+TXQOrkLYT/iw187MUVssjKl+WXZA66Tr0usJ1J3V/kdzhDi69yr
Q23xWIHivigTFY9dZ2srYfEWTs2pbXmAchm+Hm3I7b1xjZjGDv+3dHfCeWZgRUdR
xLseaHOAO943MDRUvNrfvrme2DBfKljw3aOF4RALWHmQwK6twonJFeaWxc9FaLNf
RQFYOswcV6rKGsfQGVFIVM0LewqbJholCal1o81GvjM7TJ+by61VJs0dfY4zkOkg
7URyr1J3jdAj4BMC3EWuE+hgrWvCWXbrsJVwkJFvP4ECgYEA/PP/VeryzWUZnp5b
PsfUmXPFgsf5PNB+tXXKbvEmBH9vetVeIn4P6ovzNQkNrnFnJ+VcHvMKDgjsAuY3
AwnNUt0g6kSe0crY21Enauniof6QxeiNrKzM4Zh9tEs1V76aAAr0+7x46Lw3NeqQ
H9LyuaXfGpI6lQWy+VzaqJiEZ1cCgYEA1ahdxAOmKxEVyufcnT6kpTntkhGPHI5M
OUTAfuT7H4+msJEWSLzSRbba7iXbYV0NqKmzi8K4Cbm2YSiASMg1TpmFIofGieNo
Zd/O5neAAyT883vMVrOWB9gQqeD59rVi5n7dRvOrqnLx6eh98n89IX+UKUqVXmzG
PKKXF5mY53sCgYEAy1okdR/SXgje1cOcuxXO9iY6LG8zEQ7ULnEpcpLu60K/jlIl
wTjy9q3XsVU76HdGZK5gpojgFnRZt+L3arvgkrMumObdfu7B816Ef9DS30RVgqT3
8E9iziHXPLARjB6wk0Q7unzD/uYAxJlzkAH4HAMbHrNb4R2Zyho5trwWBTUCgYBX
ZZ+1Zqi6RSrc7StZRx6iV6fOcFBSJgMYCf+T6KawqlpNL4wmBNcenUXA1MuAcEf+
DizbprMRh14LMD4Jh5wGotXK4i9ciCFUStCfhZJftumhz6oubReZ7dAjLVwShvAF
yS9KrrIx7wL145xhbZrb8QvyOAgRr/0HIJsdlD7vsQKBgQDFHV6jFz9dX2tx+11p
GXv4Nv1C+eF/C7swVDeWCiaiXU3VyRKPCc7Cj8dfj7xwxLlA3XV3yXaF/7tPK5EK
tAEuthiStVZvhn6i17d6OsCwOD3TGM4VhZ8xZLLuQ94OXclMR008UghrbwRCFli6
kPSKEGhWynPZ5DVjroE5+oL2dQ==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFSjCCAzKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQKEwpNeSBD
b21wYW55MRYwFAYDVQQLEw1CbG9ja2NoYWluIENBMRYwFAYDVQQDEw1jYS5teWhv
c3QuZGV2MB4XDTIwMDMyMDIzMTAzNloXDTIxMDkyMDIzMTAzNFowRTETMBEGA1UE
ChMKTXkgQ29tcGFueTEWMBQGA1UECxMNQmxvY2tjaGFpbiBDQTEWMBQGA1UEAxMN
Y2EubXlob3N0LmRldjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL+v
wvbjRXpdjlBioUFFnnL3uCgfVMHw1zad8z36bBXr/MQDjAVgTfGrzI3FM3YVPTqb
peS7DuVfXOr+ULBU/mq6QuRgLTk9ynVbqcezajECtLZgbudkStF4ESFArjuaJ3IY
kYtL8TEKID31+EJIDyjZOwBjb87QoPaEICTIXLDYkoFv3heg9k8npYItWiKJpuJp
BSj23gCBm8zx/uqTZQOa1bEhhSBSI/okmM0dy+rGgquwZamH1HVH5xm7rok/wYry
EVb1AAT+j42Ug0qihF8mJrvlXLaN0G0QHAiqCf+wnS3DzBnaLPVLNcGJSeY6O8m+
HdCo+/y8WBJU6y+50HFk4qLVAoTYswn3X362W3q48DeCjvImty3g2vvfsVqvKkVd
bQy6C4ok3PXjXyhaxdFNx8f4SUXuXIfXyUMdSt2MNN0r4QY8KAWC9Kkic75Ui8sG
wHw2sBtEQxj1EX/QRAiqWeLT5J/CL3fHhmas+NaICeU3lH7pyLgKjs8+aZ44g9dt
g+AHEBqQvFMRW6wcIJLlWPT4zUo//LWGIbnSUGzz34frzVljg5f/ZsxxTTa10qGT
VOb6Jdln12JvnlObMdr45HSFFAXZrJB+g3z6/K9mFml29Py6dnDgQAFdlRW/LLns
4yEx5YXYrkCRse5PUHqt/APVvUp9kbWGnSB0EeoNAgMBAAGjRTBDMA4GA1UdDwEB
/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRkRRFBxmIJWihW
Wm42q3T1nI5IYDANBgkqhkiG9w0BAQsFAAOCAgEAFsXRgMoPT4RYSaB9oO2e/1vc
3e2rkNG2Bd3twKV33r8hUuqH9lm1EcBOIoWkWSZC4bbAvNtaoTm/pEHgBjKHTiPg
liVCuL66OmXWpL0LjGibialKeyowMkJ5S4K/p7vdPvIYA7jAxL7LNDwcoLyMm+22
GmCqrFp8tmy3ELj1uJe5fvI69WME7zGG6rLRGVT+Hc7gtKwk/iDIPvCF4UkQ0Jai
8RWlNy4G5BBSMCY8AWkRsnmVg/njZr8e/CMuOlLbRtSX8Nx/JAoR8bwszy7N0vCi
OkTELZNjo7w0lXWtINxNb5ySnHF6WSxFjmdiAgWv0ka/KBtR/+9PMMTv1g0y6c5W
dQpSTCJyzHhYb8Yu12tj1jaZDrqT0rRD8klZTuPxDtIbuhnpairjIRygRsVOEtYv
RXFWdjp/keD0A41TNtGj5eNsACPBOcYZlsb9AGqqmSQfRJ4R+YJGsBnECmIZpsva
VvX2eKI7jAZcYjHZIzqASnNFxlIM8jTxi2ifpS/7T93/RIl2Es5PJ3SX+FniOtN5
ajlzWSo7SVvgaYOOeOybKZq4cBmQ/R3XAnK1NSgMgHWZv1NOgHZEhC9kpvj0QQnE
WzjC3KDBLzhRpe678C5iorf9FXMkvYfb1NVl5UYYa9/C4aaj4Jt4SiaIaqsq27fP
wkLM79un1zrcbPHQUcA=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
/rUweL14cs7iUki+lWEGjjvXmOFkORI=
-----END PRIVATE KEY-----