CORDA-1343 Make the RPCClient ssl constructors public. Clean up broke… (#3039)

* CORDA-1343 Make the RPCClient ssl constructors public. Clean up broker authentication logic

* CORDA-1343 small fix

* CORDA-1343 cleanup

* CORDA-1343 fixed api changes script

* CORDA-1343 fixed merge

* CORDA-1343 removed unused property

* CORDA-1343 add separate p2p and rpc node users

* CORDA-1343 remove test configuration

* CORDA-1343 fix tests

* CORDA-1343 address core review comments

* CORDA-1343 some documentation and adding createWithSsl method for a haAddressPool

* CORDA-1343 clean up the CordaRPCClient interface

* CORDA-1343 add internal shell test

* CORDA-1343 address code review comments

* CORDA-1343 split the internalShell user from the System Rpc user

* CORDA-1343 fix test

* CORDA-1343 Add warning when certificateChainCheckPolicies is being configured

* CORDA-1343 Address code review changes

* CORDA-1343 fix merge

* CORDA-1343 added test, docs, clarify comments

* CORDA-1343 clean up docs

* CORDA-1343 fix api

* CORDA-1343 fix merge

* CORDA-1343 fix merge

* CORDA-1343 fix merge

* CORDA-1343 fix merge
This commit is contained in:
Tudor Malene
2018-05-21 13:05:08 +03:00
committed by GitHub
parent fc88cefbc8
commit 455221629b
49 changed files with 1002 additions and 1045 deletions

View File

@ -2504,6 +2504,26 @@ public final class net.corda.core.identity.PartyAndCertificate extends java.lang
@CordaSerializable @CordaSerializable
public interface net.corda.core.messaging.AllPossibleRecipients extends net.corda.core.messaging.MessageRecipients public interface net.corda.core.messaging.AllPossibleRecipients extends net.corda.core.messaging.MessageRecipients
## ##
public final class net.corda.core.messaging.ClientRpcSslOptions extends java.lang.Object
public <init>(java.nio.file.Path, String, String)
@NotNull
public final java.nio.file.Path component1()
@NotNull
public final String component2()
@NotNull
public final String component3()
@NotNull
public final net.corda.core.messaging.ClientRpcSslOptions copy(java.nio.file.Path, String, String)
public boolean equals(Object)
@NotNull
public final String getTrustStorePassword()
@NotNull
public final java.nio.file.Path getTrustStorePath()
@NotNull
public final String getTrustStoreProvider()
public int hashCode()
public String toString()
##
@DoNotImplement @DoNotImplement
public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.messaging.RPCOps public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.messaging.RPCOps
public abstract void acceptNewNetworkParameters(net.corda.core.crypto.SecureHash) public abstract void acceptNewNetworkParameters(net.corda.core.crypto.SecureHash)
@ -6494,6 +6514,8 @@ public final class net.corda.testing.node.User extends java.lang.Object
public String toString() public String toString()
## ##
public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object
public <init>(java.util.List<net.corda.core.utilities.NetworkHostAndPort>)
public <init>(java.util.List<net.corda.core.utilities.NetworkHostAndPort>, net.corda.client.rpc.CordaRPCClientConfiguration)
public <init>(net.corda.core.utilities.NetworkHostAndPort) public <init>(net.corda.core.utilities.NetworkHostAndPort)
public <init>(net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) public <init>(net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration)
@NotNull @NotNull
@ -6504,6 +6526,10 @@ public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object
public static final net.corda.client.rpc.CordaRPCClient$Companion Companion public static final net.corda.client.rpc.CordaRPCClient$Companion Companion
## ##
public static final class net.corda.client.rpc.CordaRPCClient$Companion extends java.lang.Object public static final class net.corda.client.rpc.CordaRPCClient$Companion extends java.lang.Object
@NotNull
public final net.corda.client.rpc.CordaRPCClient createWithSsl(java.util.List<net.corda.core.utilities.NetworkHostAndPort>, net.corda.core.messaging.ClientRpcSslOptions, net.corda.client.rpc.CordaRPCClientConfiguration)
@NotNull
public final net.corda.client.rpc.CordaRPCClient createWithSsl(net.corda.core.utilities.NetworkHostAndPort, net.corda.core.messaging.ClientRpcSslOptions, net.corda.client.rpc.CordaRPCClientConfiguration)
## ##
public interface net.corda.client.rpc.CordaRPCClientConfiguration public interface net.corda.client.rpc.CordaRPCClientConfiguration
public abstract int getCacheConcurrencyLevel() public abstract int getCacheConcurrencyLevel()

View File

@ -48,7 +48,7 @@ EOF
#an internal class #an internal class
#TODO: check that only classes in a whitelist are part of the API rather than look for specific invalid cases going forward #TODO: check that only classes in a whitelist are part of the API rather than look for specific invalid cases going forward
newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." ) newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." )
newNodeExposures=$(echo "$userDiffContents" | grep "^+" | grep "net.corda.node" ) newNodeExposures=$(echo "$userDiffContents" | grep "^+" | grep "net\.corda\.node\.")
internalCount=`grep -v "^$" <<EOF | wc -l internalCount=`grep -v "^$" <<EOF | wc -l
$newInternalExposures $newInternalExposures

View File

@ -8,8 +8,8 @@ import net.corda.core.context.Trace
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.serialization.internal.effectiveSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
import java.time.Duration import java.time.Duration
@ -98,7 +98,7 @@ interface CordaRPCClientConfiguration {
* *
* @param hostAndPort The network address to connect to. * @param hostAndPort The network address to connect to.
* @param configuration An optional configuration used to tweak client behaviour. * @param configuration An optional configuration used to tweak client behaviour.
* @param sslConfiguration An optional [SSLConfiguration] used to enable secure communication with the server. * @param sslConfiguration An optional [ClientRpcSslOptions] used to enable secure communication with the server.
* @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode. * @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode.
* The client will attempt to connect to a live server by trying each address in the list. If the servers are not in * The client will attempt to connect to a live server by trying each address in the list. If the servers are not in
* HA mode, the client will round-robin from the beginning of the list and try all servers. * HA mode, the client will round-robin from the beginning of the list and try all servers.
@ -106,9 +106,11 @@ interface CordaRPCClientConfiguration {
class CordaRPCClient private constructor( class CordaRPCClient private constructor(
private val hostAndPort: NetworkHostAndPort, private val hostAndPort: NetworkHostAndPort,
private val configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), private val configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
private val sslConfiguration: SSLConfiguration? = null, private val sslConfiguration: ClientRpcSslOptions? = null,
private val nodeSslConfiguration: SSLConfiguration? = null,
private val classLoader: ClassLoader? = null, private val classLoader: ClassLoader? = null,
private val haAddressPool: List<NetworkHostAndPort> = emptyList() private val haAddressPool: List<NetworkHostAndPort> = emptyList(),
private val internalConnection: Boolean = false
) { ) {
@JvmOverloads @JvmOverloads
constructor(hostAndPort: NetworkHostAndPort, constructor(hostAndPort: NetworkHostAndPort,
@ -122,24 +124,41 @@ class CordaRPCClient private constructor(
* @param configuration An optional configuration used to tweak client behaviour. * @param configuration An optional configuration used to tweak client behaviour.
*/ */
@JvmOverloads @JvmOverloads
constructor(haAddressPool: List<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(haAddressPool.first(), configuration, null, null, haAddressPool) constructor(haAddressPool: List<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool)
companion object { companion object {
internal fun createWithSsl( fun createWithSsl(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), sslConfiguration: ClientRpcSslOptions,
sslConfiguration: SSLConfiguration? = null configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()
): CordaRPCClient { ): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, sslConfiguration) return CordaRPCClient(hostAndPort, configuration, sslConfiguration)
} }
fun createWithSsl(
haAddressPool: List<NetworkHostAndPort>,
sslConfiguration: ClientRpcSslOptions,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()
): CordaRPCClient {
return CordaRPCClient(haAddressPool.first(), configuration, sslConfiguration, haAddressPool = haAddressPool)
}
internal fun createWithSslAndClassLoader( internal fun createWithSslAndClassLoader(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration? = null, sslConfiguration: ClientRpcSslOptions? = null,
classLoader: ClassLoader? = null classLoader: ClassLoader? = null
): CordaRPCClient { ): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader) return CordaRPCClient(hostAndPort, configuration, sslConfiguration, null, classLoader)
}
internal fun createWithInternalSslAndClassLoader(
hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration?,
classLoader: ClassLoader? = null
): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, null, sslConfiguration, classLoader, internalConnection = true)
} }
} }
@ -155,17 +174,22 @@ class CordaRPCClient private constructor(
} }
} }
private fun getRpcClient() : RPCClient<CordaRPCOps> { private fun getRpcClient(): RPCClient<CordaRPCOps> {
return if (haAddressPool.isEmpty()) { return when {
RPCClient( // Node->RPC broker, mutually authenticated SSL. This is used when connecting the integrated shell
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration), internalConnection == true -> RPCClient(hostAndPort, nodeSslConfiguration!!)
configuration,
if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT) // Client->RPC broker
} else { haAddressPool.isEmpty() -> RPCClient(
RPCClient(haAddressPool, rpcConnectorTcpTransport(hostAndPort, config = sslConfiguration),
sslConfiguration,
configuration, configuration,
if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT) if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
else -> {
RPCClient(haAddressPool,
sslConfiguration,
configuration,
if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
}
} }
} }

View File

@ -5,22 +5,24 @@ import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.pendingFlowsCount import net.corda.core.messaging.pendingFlowsCount
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import rx.Observable import rx.Observable
/** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ /** Utility which exposes the internal Corda RPC constructor to other internal Corda components */
fun createCordaRPCClientWithSsl( fun createCordaRPCClientWithSslAndClassLoader(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration? = null sslConfiguration: ClientRpcSslOptions? = null,
) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration) classLoader: ClassLoader? = null
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
fun createCordaRPCClientWithSslAndClassLoader( fun createCordaRPCClientWithInternalSslAndClassLoader(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration? = null, sslConfiguration: SSLConfiguration? = null,
classLoader: ClassLoader? = null classLoader: ClassLoader? = null
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) ) = CordaRPCClient.createWithInternalSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
fun CordaRPCOps.drainAndShutdown(): Observable<Unit> { fun CordaRPCOps.drainAndShutdown(): Observable<Unit> {

View File

@ -13,10 +13,11 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransportsFromList import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration
@ -58,6 +59,9 @@ data class CordaRPCClientConfigurationImpl(
} }
} }
/**
* This runs on the client JVM
*/
class RPCClient<I : RPCOps>( class RPCClient<I : RPCOps>(
val transport: TransportConfiguration, val transport: TransportConfiguration,
val rpcConfiguration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default, val rpcConfiguration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
@ -66,18 +70,25 @@ class RPCClient<I : RPCOps>(
) { ) {
constructor( constructor(
hostAndPort: NetworkHostAndPort, hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration? = null, sslConfiguration: ClientRpcSslOptions? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default, configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration, serializationContext) ) : this(rpcConnectorTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext)
constructor(
hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(rpcInternalClientTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext)
constructor( constructor(
haAddressPool: List<NetworkHostAndPort>, haAddressPool: List<NetworkHostAndPort>,
sslConfiguration: SSLConfiguration? = null, sslConfiguration: ClientRpcSslOptions? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default, configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(tcpTransport(ConnectionDirection.Outbound(), haAddressPool.first(), sslConfiguration), ) : this(rpcConnectorTcpTransport(haAddressPool.first(), sslConfiguration),
configuration, serializationContext, tcpTransportsFromList(ConnectionDirection.Outbound(), haAddressPool, sslConfiguration)) configuration, serializationContext, rpcConnectorTcpTransportsFromList(haAddressPool, sslConfiguration))
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()

View File

@ -0,0 +1,6 @@
package net.corda.core.messaging
import java.nio.file.Path
/** As an RPC Client, use this class to point to the truststore that contains the RPC SSL certificate provided by the node admin */
data class ClientRpcSslOptions(val trustStorePath: Path, val trustStorePassword: String, val trustStoreProvider: String = "JKS")

View File

@ -49,7 +49,11 @@ Unreleased
* The deprecated web server now has its own ``web-server.conf`` file, separate from ``node.conf``. * The deprecated web server now has its own ``web-server.conf`` file, separate from ``node.conf``.
* Property keys with double quotes (e.g. `"key"`) in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. * Property keys with double quotes (e.g. `"key"`) in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`.
* More types can be serialized now: java.security.cert.CRLReason, java.security.cert.X509CRL, java.math.BigInteger * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications
a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api`
* The node RPC broker opens 2 endpoints that are configured with ``address`` and ``adminAddress``. RPC Clients would connect to the address, while the node will connect
to the adminAddress. Previously if ssl was enabled for RPC the ``adminAddress`` was equal to ``address``.
* Upgraded H2 to v1.4.197 * Upgraded H2 to v1.4.197

View File

@ -307,9 +307,17 @@ The client RPC wire protocol is defined and documented in ``net/corda/client/rpc
Wire security Wire security
------------- -------------
``CordaRPCClient`` has an optional constructor parameter of type ``SSLConfiguration``, defaulted to ``null``, which allows ``CordaRPCClient`` has an optional constructor parameter of type ``ClientRpcSslOptions``, defaulted to ``null``, which allows
communication with the node using SSL. Default ``null`` value means no SSL used in the context of RPC. communication with the node using SSL. Default ``null`` value means no SSL used in the context of RPC.
To use this feature, the ``CordaRPCClient`` object provides a static factory method ``createWithSsl``.
In order for this to work, the client needs to provide a truststore containing a certificate received from the node admin.
(The Node does not expect the RPC client to present a certificate, as the client already authenticates using the mechanism described above.)
For the communication to be secure, we recommend using the standard SSL best practices for key management.
Whitelisting classes with the Corda node Whitelisting classes with the Corda node
---------------------------------------- ----------------------------------------
CorDapps must whitelist any classes used over RPC with Corda's serialization framework, unless they are whitelisted by CorDapps must whitelist any classes used over RPC with Corda's serialization framework, unless they are whitelisted by

View File

@ -1,3 +0,0 @@
myLegalName : "O=Bank A,L=London,C=GB"
p2pAddress : "my-corda-node:10002"
verifierType: "OutOfProcess"

View File

@ -28,8 +28,7 @@ class ExampleConfigTest {
@Test @Test
fun `example node_confs parses fine`() { fun `example node_confs parses fine`() {
readAndCheckConfigurations( readAndCheckConfigurations(
"example-node.conf", "example-node.conf"
"example-out-of-process-verifier-node.conf"
) { ) {
val baseDirectory = Paths.get("some-example-base-dir") val baseDirectory = Paths.get("some-example-base-dir")
ConfigHelper.loadConfig( ConfigHelper.loadConfig(

View File

@ -79,7 +79,7 @@ Security
Clients attempting to connect to the node's broker fall in one of four groups: Clients attempting to connect to the node's broker fall in one of four groups:
#. Anyone connecting with the username ``SystemUsers/Node`` is treated as the node hosting the broker, or a logical #. Anyone connecting with the username ``SystemUsers/Node`` or ``SystemUsers/NodeRPC`` is treated as the node hosting the brokers, or a logical
component of the node. The TLS certificate they provide must match the one broker has for the node. If that's the case component of the node. The TLS certificate they provide must match the one broker has for the node. If that's the case
they are given full access to all valid queues, otherwise they are rejected. they are given full access to all valid queues, otherwise they are rejected.
@ -101,3 +101,21 @@ this to determine what permissions the user has.
The broker also does host verification when connecting to another peer. It checks that the TLS certificate subject matches The broker also does host verification when connecting to another peer. It checks that the TLS certificate subject matches
with the advertised X.500 legal name from the network map service. with the advertised X.500 legal name from the network map service.
Implementation details
----------------------
The components of the system that need to communicate and authenticate each other are:
- The Artemis P2P broker (Currently runs inside the Nodes JVM process, but in the future it will be able to run as a separate server)
* opens Acceptor configured with the doorman's certificate in the truststore and the node's ssl certificate in the keystore
- The Artemis RPC broker (Currently runs inside the Nodes JVM process, but in the future it will be able to run as a separate server)
* opens "Admin" Acceptor configured with the doorman's certificate in the truststore and the node's ssl certificate in the keystore
* opens "Client" Acceptor with the ssl settings configurable. This acceptor does not require ssl client-auth.
- The current node hosting the brokers
* connects to the P2P broker using the ``SystemUsers/Node`` user and the node's keystore and trustore
* connects to the "Admin" Acceptor of the RPC broker using the ``SystemUsers/NodeRPC`` user and the node's keystore and trustore
- RPC clients ( Third party applications that need to communicate with the Node. )
* connect to the "Client" Acceptor of the RPC broker using the username/password provided by the node's admin. The client verifies the node's certificate using a truststore provided by the node's admin.
- Peer nodes (Other nodes on the network)
* connect to the P2P broker using the ``SystemUsers/Peer`` user and a doorman signed certificate. The authentication is performed based on the root CA.

View File

@ -1,6 +1,6 @@
package net.corda.nodeapi package net.corda.nodeapi
import net.corda.core.identity.CordaX500Name import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
@ -8,20 +8,11 @@ import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import java.nio.file.Path
sealed class ConnectionDirection {
data class Inbound(val acceptorFactoryClassName: String) : ConnectionDirection()
data class Outbound(
val expectedCommonNames: Set<CordaX500Name> = emptySet(), // TODO SNI? Or we need a notion of node's network identity?
val connectorFactoryClassName: String = NettyConnectorFactory::class.java.name
) : ConnectionDirection()
}
/** Class to set Artemis TCP configuration options. */ /** Class to set Artemis TCP configuration options. */
class ArtemisTcpTransport { class ArtemisTcpTransport {
companion object { companion object {
const val VERIFY_PEER_LEGAL_NAME = "corda.verifyPeerCommonName"
/** /**
* Corda supported TLS schemes. * Corda supported TLS schemes.
* <p><ul> * <p><ul>
@ -47,69 +38,114 @@ class ArtemisTcpTransport {
/** Supported TLS versions, currently TLSv1.2 only. */ /** Supported TLS versions, currently TLSv1.2 only. */
val TLS_VERSIONS = listOf("TLSv1.2") val TLS_VERSIONS = listOf("TLSv1.2")
/** Specify [TransportConfiguration] for TCP communication. */ private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf(
fun tcpTransport( // Basic TCP target details.
direction: ConnectionDirection, TransportConstants.HOST_PROP_NAME to hostAndPort.host,
hostAndPort: NetworkHostAndPort, TransportConstants.PORT_PROP_NAME to hostAndPort.port,
config: SSLConfiguration?,
enableSSL: Boolean = true
): TransportConfiguration {
val options = mutableMapOf<String, Any?>(
// Basic TCP target details.
TransportConstants.HOST_PROP_NAME to hostAndPort.host,
TransportConstants.PORT_PROP_NAME to hostAndPort.port,
// Turn on AMQP support, which needs the protocol jar on the classpath. // Turn on AMQP support, which needs the protocol jar on the classpath.
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop. // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop.
// It does not use AMQP messages for its own messages e.g. topology and heartbeats. // It does not use AMQP messages for its own messages e.g. topology and heartbeats.
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications.
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP",
TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null),
TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1), TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1),
// turn off direct delivery in Artemis - this is latency optimisation that can lead to // turn off direct delivery in Artemis - this is latency optimisation that can lead to
//hick-ups under high load (CORDA-1336) //hick-ups under high load (CORDA-1336)
TransportConstants.DIRECT_DELIVER to false TransportConstants.DIRECT_DELIVER to false)
)
private val defaultSSLOptions = mapOf(
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","))
private fun SSLConfiguration.toTransportOptions() = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.KEYSTORE_PATH_PROP_NAME to sslKeystore,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStoreFile,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true)
private fun ClientRpcSslOptions.toTransportOptions() = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider,
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword)
private fun BrokerRpcSslOptions.toTransportOptions() = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false)
private val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory"
private val connectorFactoryClassName = NettyConnectorFactory::class.java.name
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (config != null && enableSSL) { if (config != null && enableSSL) {
config.sslKeystore.requireOnDefaultFileSystem() config.sslKeystore.requireOnDefaultFileSystem()
config.trustStoreFile.requireOnDefaultFileSystem() config.trustStoreFile.requireOnDefaultFileSystem()
val tlsOptions = mapOf( options.putAll(defaultSSLOptions)
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake options.putAll(config.toTransportOptions())
// and AES encryption.
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.KEYSTORE_PATH_PROP_NAME to config.sslKeystore,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password.
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","),
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames
)
options.putAll(tlsOptions)
} }
val factoryName = when (direction) { return TransportConfiguration(acceptorFactoryClassName, options)
is ConnectionDirection.Inbound -> direction.acceptorFactoryClassName }
is ConnectionDirection.Outbound -> direction.connectorFactoryClassName
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (config != null && enableSSL) {
config.sslKeystore.requireOnDefaultFileSystem()
config.trustStoreFile.requireOnDefaultFileSystem()
options.putAll(defaultSSLOptions)
options.putAll(config.toTransportOptions())
} }
return TransportConfiguration(factoryName, options) return TransportConfiguration(connectorFactoryClassName, options)
}
/** [TransportConfiguration] for RPC TCP communication - server side. */
fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (config != null && enableSSL) {
config.keyStorePath.requireOnDefaultFileSystem()
options.putAll(config.toTransportOptions())
options.putAll(defaultSSLOptions)
}
return TransportConfiguration(acceptorFactoryClassName, options)
}
/** [TransportConfiguration] for RPC TCP communication
* This is the Transport that connects the client JVM to the broker. */
fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (config != null && enableSSL) {
config.trustStorePath.requireOnDefaultFileSystem()
options.putAll(config.toTransportOptions())
options.putAll(defaultSSLOptions)
}
return TransportConfiguration(connectorFactoryClassName, options)
} }
/** Create as list of [TransportConfiguration]. **/ /** Create as list of [TransportConfiguration]. **/
fun tcpTransportsFromList( fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map {
direction: ConnectionDirection, rpcConnectorTcpTransport(it, config, enableSSL)
hostAndPortList: List<NetworkHostAndPort>, }
config: SSLConfiguration?,
enableSSL: Boolean = true): List<TransportConfiguration> {
val tcpTransports = ArrayList<TransportConfiguration>(hostAndPortList.size)
hostAndPortList.forEach {
tcpTransports.add(tcpTransport(direction, it, config, enableSSL))
}
return tcpTransports fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
}
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
} }
} }
} }
data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String)

View File

@ -4,8 +4,7 @@ import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@ -35,7 +34,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration,
check(started == null) { "start can't be called twice" } check(started == null) { "start can't be called twice" }
log.info("Connecting to message broker: $serverAddress") log.info("Connecting to message broker: $serverAddress")
// TODO Add broker CN to config for host verification in case the embedded broker isn't used // TODO Add broker CN to config for host verification in case the embedded broker isn't used
val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, config) val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted. // would be the default and the two lines below can be deleted.
@ -50,7 +49,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration,
// using our TLS certificate. // using our TLS certificate.
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer // Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
// size of 1MB is acknowledged. // size of 1MB is acknowledged.
val session = sessionFactory!!.createSession(NODE_USER, NODE_USER, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE) val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE)
session.start() session.start()
// Create a general purpose producer. // Create a general purpose producer.
val producer = session.createProducer() val producer = session.createProducer()

View File

@ -22,8 +22,12 @@ class ArtemisMessagingComponent {
// System users must contain an invalid RPC username character to prevent any chance of name clash which in this // System users must contain an invalid RPC username character to prevent any chance of name clash which in this
// case is a forward slash // case is a forward slash
const val NODE_USER = "SystemUsers/Node" const val NODE_P2P_USER = "SystemUsers/Node"
const val NODE_RPC_USER = "SystemUsers/NodeRPC"
const val PEER_USER = "SystemUsers/Peer" const val PEER_USER = "SystemUsers/Peer"
// User used only in devMode when nodes have a shell attached by default.
const val INTERNAL_SHELL_USER = "internalShell"
const val INTERNAL_PREFIX = "internal." const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
const val P2P_PREFIX = "p2p.inbound." const val P2P_PREFIX = "p2p.inbound."

View File

@ -9,7 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
@ -112,7 +112,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val maxMessageSize
if (connected) { if (connected) {
log.info("Bridge Connected") log.info("Bridge Connected")
val sessionFactory = artemis.started!!.sessionFactory val sessionFactory = artemis.started!!.sessionFactory
val session = sessionFactory.createSession(NODE_USER, NODE_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
this.session = session this.session = session
val consumer = session.createConsumer(queueName) val consumer = session.createConsumer(queueName)
this.consumer = consumer this.consumer = consumer

View File

@ -175,7 +175,6 @@ class AMQPBridgeTest {
doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn(artemisAddress).whenever(it).p2pAddress doReturn(artemisAddress).whenever(it).p2pAddress
doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()
val artemisServer = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), MAX_MESSAGE_SIZE) val artemisServer = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), MAX_MESSAGE_SIZE)

View File

@ -365,7 +365,6 @@ class ProtonWrapperTests {
doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress
doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(true).whenever(it).crlCheckSoftFail doReturn(true).whenever(it).crlCheckSoftFail
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()

View File

@ -1,69 +1,144 @@
package net.corda.node.services.rpc package net.corda.node.services.rpc
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl import net.corda.core.internal.div
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.all
import net.corda.testing.common.internal.withCertificates import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.testing.common.internal.withKeyStores import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree
import net.corda.testing.internal.createKeyPairAndSelfSignedCertificate
import net.corda.testing.internal.saveToKeyStore
import net.corda.testing.internal.saveToTrustStore
import net.corda.testing.internal.useSslRpcOverrides import net.corda.testing.internal.useSslRpcOverrides
import net.corda.testing.node.User import net.corda.testing.node.User
import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
class RpcSslTest { class RpcSslTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test @Test
fun rpc_client_using_ssl() { fun `RPC client using ssl is able to run a command`() {
val user = User("mark", "dadada", setOf(all())) val user = User("mark", "dadada", setOf(all()))
withCertificates { server, client, createSelfSigned, createSignedBy -> var successfulLogin = false
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB")) var failedLogin = false
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works... val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
server.keyStore["cordaclienttls"] = rootCertificate val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
server.trustStore["cordaclienttls"] = rootCertificate val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
server.trustStore["mark"] = markCertificate
client.keyStore["mark"] = markCertificate val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
client.trustStore["cordaclienttls"] = rootCertificate val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
var successful = false val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { val client = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions)
val node = startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow() val connection = client.start(user.username, user.password)
val client = createCordaRPCClientWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions)
val connection = client.start(user.username, user.password)
connection.proxy.apply {
nodeInfo()
successful = true
}
connection.close() connection.proxy.apply {
} val nodeInfo = nodeInfo()
assertThat(successful).isTrue() assertThat(nodeInfo.legalIdentities).isNotEmpty
successfulLogin = true
} }
connection.close()
Assertions.assertThatThrownBy {
val connection2 = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, "wrong")
connection2.proxy.apply {
nodeInfo()
failedLogin = true
}
connection2.close()
}.isInstanceOf(ActiveMQSecurityException::class.java)
} }
assertThat(successfulLogin).isTrue()
assertThat(failedLogin).isFalse()
} }
@Test @Test
fun rpc_client_not_using_ssl() { fun `RPC client using ssl will fail if connecting to a node that cannot present a matching certificate`() {
val user = User("mark", "dadada", setOf(all()))
var successful = false
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
val (_, cert1) = createKeyPairAndSelfSignedCertificate()
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1)
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
Assertions.assertThatThrownBy {
val connection = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password)
connection.proxy.apply {
nodeInfo()
successful = true
}
connection.close()
}.isInstanceOf(ActiveMQNotConnectedException::class.java)
}
assertThat(successful).isFalse()
}
@Test
fun `RPC client not using ssl can run commands`() {
val user = User("mark", "dadada", setOf(all())) val user = User("mark", "dadada", setOf(all()))
var successful = false var successful = false
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
val node = startNode(rpcUsers = listOf(user)).getOrThrow() val node = startNode(rpcUsers = listOf(user)).getOrThrow()
val client = CordaRPCClient(node.rpcAddress) val connection = CordaRPCClient(node.rpcAddress).start(user.username, user.password)
val connection = client.start(user.username, user.password)
connection.proxy.apply { connection.proxy.apply {
nodeInfo() nodeInfo()
successful = true successful = true
} }
connection.close() connection.close()
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
} }
@Test
fun `The system RPC user can not connect to the rpc broker without the node's key`() {
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
val node = startNode(customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
val client = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions)
Assertions.assertThatThrownBy {
client.start(NODE_RPC_USER, NODE_RPC_USER).use { connection ->
connection.proxy.nodeInfo()
}
}.isInstanceOf(ActiveMQException::class.java)
val clientAdmin = CordaRPCClient.createWithSsl(node.rpcAdminAddress, sslConfiguration = clientSslOptions)
Assertions.assertThatThrownBy {
clientAdmin.start(NODE_RPC_USER, NODE_RPC_USER).use { connection ->
connection.proxy.nodeInfo()
}
}.isInstanceOf(ActiveMQException::class.java)
}
}
} }

View File

@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.exists import net.corda.core.internal.exists
import net.corda.core.internal.x500Name import net.corda.core.internal.x500Name
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.DEV_ROOT_CA
@ -43,10 +43,10 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() {
} }
@Test @Test
fun `only the node running the broker can login using the special node user`() { fun `only the node running the broker can login using the special P2P node user`() {
val attacker = clientTo(alice.internals.configuration.p2pAddress) val attacker = clientTo(alice.internals.configuration.p2pAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(NODE_USER, NODE_USER) attacker.start(NODE_P2P_USER, NODE_P2P_USER)
} }
} }
@ -70,7 +70,7 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() {
fun `login to a non ssl port as a node user`() { fun `login to a non ssl port as a node user`() {
val attacker = clientTo(alice.internals.configuration.rpcOptions.address!!, sslConfiguration = null) val attacker = clientTo(alice.internals.configuration.rpcOptions.address!!, sslConfiguration = null)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(NODE_USER, NODE_USER, enableSSL = false) attacker.start(NODE_P2P_USER, NODE_P2P_USER, enableSSL = false)
} }
} }

View File

@ -4,7 +4,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.internal.configureTestSSL import net.corda.testing.internal.configureTestSSL
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.*
@ -23,7 +22,7 @@ class SimpleMQClient(val target: NetworkHostAndPort,
lateinit var producer: ClientProducer lateinit var producer: ClientProducer
fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) { fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) {
val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), target, config, enableSSL = enableSSL) val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(target, config, enableSSL = enableSSL)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
isBlockOnNonDurableSend = true isBlockOnNonDurableSend = true
threadPoolMaxSize = 1 threadPoolMaxSize = 1

View File

@ -313,10 +313,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
open fun startShell() { open fun startShell() {
if (configuration.shouldInitCrashShell()) { if (configuration.shouldInitCrashShell()) {
if (configuration.rpcOptions.address == null) { InteractiveShell.startShellInternal(configuration.toShellConfig(), cordappLoader.appClassLoader)
throw ConfigurationException("Cannot init CrashShell because node RPC address is not set (via 'rpcSettings' option).")
}
InteractiveShell.startShell(configuration.toShellConfig(), cordappLoader.appClassLoader)
} }
} }

View File

@ -3,6 +3,7 @@ package net.corda.node.internal
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.concurrent.thenMatch
@ -28,19 +29,22 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.serialization.kryo.KryoServerSerializationScheme
import net.corda.node.services.Permissions
import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.NodePropertiesStore
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.config.shell.localShellUser
import net.corda.node.services.messaging.* import net.corda.node.services.messaging.*
import net.corda.node.services.rpc.ArtemisRpcBroker import net.corda.node.services.rpc.ArtemisRpcBroker
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AddressUtils
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.DemoClock import net.corda.node.utilities.DemoClock
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.bridging.BridgeControlListener
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.serialization.internal.* import net.corda.serialization.internal.*
import org.slf4j.Logger import org.slf4j.Logger
@ -164,10 +168,11 @@ open class Node(configuration: NodeConfiguration,
networkParameters: NetworkParameters): MessagingService { networkParameters: NetworkParameters): MessagingService {
// Construct security manager reading users data either from the 'security' config section // Construct security manager reading users data either from the 'security' config section
// if present or from rpcUsers list if the former is missing from config. // if present or from rpcUsers list if the former is missing from config.
val securityManagerConfig = configuration.security?.authService ?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) val securityManagerConfig = configuration.security?.authService
?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
securityManager = with(RPCSecurityManagerImpl(securityManagerConfig)) { securityManager = with(RPCSecurityManagerImpl(securityManagerConfig)) {
if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this, localShellUser()) else this if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this, User(INTERNAL_SHELL_USER, INTERNAL_SHELL_USER, setOf(Permissions.all()))) else this
} }
if (!configuration.messagingServerExternal) { if (!configuration.messagingServerExternal) {
@ -175,7 +180,8 @@ open class Node(configuration: NodeConfiguration,
messageBroker = ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize) messageBroker = ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize)
} }
val serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port) val serverAddress = configuration.messagingServerAddress
?: NetworkHostAndPort("localhost", configuration.p2pAddress.port)
val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) { val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) {
BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress)
} else { } else {
@ -186,7 +192,7 @@ open class Node(configuration: NodeConfiguration,
printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString()) printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
rpcServerAddresses?.let { rpcServerAddresses?.let {
rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, it.admin, MAX_RPC_MESSAGE_SIZE) internalRpcMessagingClient = InternalRPCMessagingClient(configuration, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.loadSslKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal))
printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC connection address", it.primary.toString())
printBasicNodeInfo("RPC admin connection address", it.admin.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString())
} }
@ -212,18 +218,17 @@ open class Node(configuration: NodeConfiguration,
} }
private fun startLocalRpcBroker(): BrokerAddresses? { private fun startLocalRpcBroker(): BrokerAddresses? {
with(configuration) { return with(configuration) {
return rpcOptions.address?.let { rpcOptions.address.let {
require(rpcOptions.address != null) { "RPC address needs to be specified for local RPC broker." }
val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc"
with(rpcOptions) { with(rpcOptions) {
rpcBroker = if (useSsl) { rpcBroker = if (useSsl) {
ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory) ArtemisRpcBroker.withSsl(configuration, this.address, adminAddress, sslConfig, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
} else { } else {
ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory) ArtemisRpcBroker.withoutSsl(configuration, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell())
} }
} }
return rpcBroker!!.addresses rpcBroker!!.addresses
} }
} }
} }
@ -286,12 +291,11 @@ open class Node(configuration: NodeConfiguration,
start() start()
} }
// Start up the MQ clients. // Start up the MQ clients.
rpcMessagingClient?.run { internalRpcMessagingClient?.run {
runOnStop += this::close runOnStop += this::close
when (rpcOps) { when (rpcOps) {
// not sure what this RPCOps base interface is for is SecureCordaRPCOps -> init(RpcExceptionHandlingProxy(rpcOps), securityManager)
is SecureCordaRPCOps -> start(RpcExceptionHandlingProxy(rpcOps), securityManager) else -> init(rpcOps, securityManager)
else -> start(rpcOps, securityManager)
} }
} }
verifierMessagingClient?.run { verifierMessagingClient?.run {
@ -351,20 +355,15 @@ open class Node(configuration: NodeConfiguration,
// Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia:
// //
// https://jolokia.org/agent/jvm.html // https://jolokia.org/agent/jvm.html
JmxReporter. JmxReporter.forRegistry(started.services.monitoringService.metrics).inDomain("net.corda").createsObjectNamesWith { _, domain, name ->
forRegistry(started.services.monitoringService.metrics). // Make the JMX hierarchy a bit better organised.
inDomain("net.corda"). val category = name.substringBefore('.')
createsObjectNamesWith { _, domain, name -> val subName = name.substringAfter('.', "")
// Make the JMX hierarchy a bit better organised. if (subName == "")
val category = name.substringBefore('.') ObjectName("$domain:name=$category")
val subName = name.substringAfter('.', "") else
if (subName == "") ObjectName("$domain:type=$category,name=$subName")
ObjectName("$domain:name=$category") }.build().start()
else
ObjectName("$domain:type=$category,name=$subName")
}.
build().
start()
_startupComplete.set(Unit) _startupComplete.set(Unit)
} }
@ -395,11 +394,11 @@ open class Node(configuration: NodeConfiguration,
rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node
} }
private var rpcMessagingClient: RPCMessagingClient? = null private var internalRpcMessagingClient: InternalRPCMessagingClient? = null
private var verifierMessagingClient: VerifierMessagingClient? = null private var verifierMessagingClient: VerifierMessagingClient? = null
/** Starts a blocking event loop for message dispatch. */ /** Starts a blocking event loop for message dispatch. */
fun run() { fun run() {
rpcMessagingClient?.start2(rpcBroker!!.serverControl) internalRpcMessagingClient?.start(rpcBroker!!.serverControl)
verifierMessagingClient?.start2() verifierMessagingClient?.start2()
(network as P2PMessagingClient).run() (network as P2PMessagingClient).run()
} }

View File

@ -0,0 +1,217 @@
package net.corda.node.internal.artemis
import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.rpc.LoginListener
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import java.io.IOException
import java.security.KeyStore
import java.security.Principal
import java.util.*
import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler
import javax.security.auth.callback.NameCallback
import javax.security.auth.callback.PasswordCallback
import javax.security.auth.callback.UnsupportedCallbackException
import javax.security.auth.login.FailedLoginException
import javax.security.auth.login.LoginException
import javax.security.auth.spi.LoginModule
import javax.security.cert.X509Certificate
/**
*
* The Participants in the system are "The current node", "Peer nodes", "The Artemis P2P broker", "RPC clients", "The Artemis RPC broker".
*
* These participants need to communicate and authenticate each other.
*
* Peer Nodes must use TLS when connecting to the current node's P2P broker.
*
* RPC Clients may use TLS, and need to provide a username/password.
*
* Note that the "login" method is called after the SSL handshake was successful.
* Based on the provided username, we execute extra authentication logic based on the presented client certificates or the username/password:
*
* * If someone connects with [PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
* the same as our root CA. If that's the case, the only access they're given is the ability to send to our P2P address.
* The messages these authenticated nodes send to us are tagged with their subject DN.
*
* * If someone connects with [NODE_P2P_USER] or [NODE_RPC_USER] then we confirm it's the current node by checking their TLS certificate
* is the same as our one in our key store. Then they're given full access to all valid queues.
*
* * Otherwise, if the username is neither of the above, we assume it's an RPC user and authenticate against our list of valid RPC users.
* RPC clients are given permission to perform RPC and nothing else.
*
*
* Basically, our security policy is to use some hardcoded usernames as discriminators to determine what checks to run on the presented client certificates,
* and after the checks pass, what roles to assign.
*
* The node starts one broker for RPC and one broker for P2P.
*
* The P2P broker has only 1 acceptor to which only clients that have a doorman signed certificate can connect.
* If a Peer attempts to connect as the current node, it would fail because it can't present the node's certificate.
*
* The RPC broker has 2 acceptors:
* * The Admin acceptor to which only the current node should connect. This port should be blocked to any outside connection.
* To connect as the current Node, you need to present the nodes ssl certificate.
* Theoretically, if the port is open, any Peer Node could connect to this endpoint, but to actually run commands it would have to know a valid rpc username/password.
* If the connection was made as [NODE_RPC_USER], the client would have to present the owner node's certificate.
* * The RPC client acceptor. This can be configured to use SSL. The authentication is based on username/password.
*
*/
class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() {
companion object {
const val PEER_ROLE = "SystemRoles/Peer"
const val NODE_P2P_ROLE = "SystemRoles/NodeP2P"
const val NODE_RPC_ROLE = "SystemRoles/NodeRPC"
const val RPC_ROLE = "SystemRoles/RPC"
internal val RPC_SECURITY_CONFIG = "RPC_SECURITY_CONFIG"
internal val P2P_SECURITY_CONFIG = "P2P_SECURITY_CONFIG"
internal val NODE_SECURITY_CONFIG = "NODE_SECURITY_CONFIG"
private val log = contextLogger()
}
private lateinit var nodeJaasConfig: NodeJaasConfig
private var rpcJaasConfig: RPCJaasConfig? = null
private var p2pJaasConfig: P2PJaasConfig? = null
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
super.initialize(subject, callbackHandler, sharedState, options)
nodeJaasConfig = uncheckedCast(options[NODE_SECURITY_CONFIG])
p2pJaasConfig = uncheckedCast(options[P2P_SECURITY_CONFIG])
rpcJaasConfig = uncheckedCast(options[RPC_SECURITY_CONFIG])
}
override fun login(): Boolean {
try {
val (username, password, certificates) = getUsernamePasswordAndCerts()
log.debug("Processing login for $username")
val userAndRoles = authenticateAndAuthorise(username, certificates, password)
principals += UserPrincipal(userAndRoles.first)
principals += userAndRoles.second
log.debug("Login for $username succeeded")
loginSucceeded = true
return true
} catch (e: Exception) {
log.error("Login failed: ${e.message}", e)
if (e is LoginException) {
throw e
} else {
throw FailedLoginException(e.message)
}
}
}
// The Main authentication logic, responsible for running all the configured checks for each user type
// and return the actual User and principals
private fun authenticateAndAuthorise(username: String, certificates: Array<X509Certificate>?, password: String): Pair<String, List<RolePrincipal>> {
fun requireTls(certificates: Array<X509Certificate>?) = requireNotNull(certificates) { "No client certificates presented." }
return when (username) {
ArtemisMessagingComponent.NODE_P2P_USER -> {
requireTls(certificates)
CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!)
Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(NODE_P2P_ROLE)))
}
ArtemisMessagingComponent.NODE_RPC_USER -> {
requireTls(certificates)
CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!)
Pair(ArtemisMessagingComponent.NODE_RPC_USER, listOf(RolePrincipal(NODE_RPC_ROLE)))
}
ArtemisMessagingComponent.PEER_USER -> {
requireNotNull(p2pJaasConfig) { "Attempted to connect as a peer to the rpc broker." }
requireTls(certificates)
// This check is redundant as it was performed already during the SSL handshake
CertificateChainCheckPolicy.RootMustMatch.createCheck(p2pJaasConfig!!.keyStore, p2pJaasConfig!!.trustStore).checkCertificateChain(certificates!!)
Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(PEER_ROLE)))
}
else -> {
requireNotNull(rpcJaasConfig) { "Attempted to connect as an rpc user to the P2P broker." }
rpcJaasConfig!!.run {
securityManager.authenticate(username, Password(password))
// This will assign the username the actual rights.
loginListener(username)
// This enables the RPC client to send requests and to receive responses.
Pair(username, listOf(RolePrincipal(RPC_ROLE), RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username")))
}
}
}
}
}
// Configs used for setting up the broker custom security module.
data class RPCJaasConfig(
val securityManager: RPCSecurityManager, //used to authenticate users - implemented with Shiro
val loginListener: LoginListener, //callback that dynamically assigns security roles to RPC users on their authentication
val useSslForRPC: Boolean)
data class P2PJaasConfig(val keyStore: KeyStore, val trustStore: KeyStore)
data class NodeJaasConfig(val keyStore: KeyStore, val trustStore: KeyStore)
// Boilerplate required for JAAS.
abstract class BaseBrokerJaasLoginModule : LoginModule {
protected var loginSucceeded: Boolean = false
protected lateinit var subject: Subject
protected lateinit var callbackHandler: CallbackHandler
protected val principals = ArrayList<Principal>()
protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<X509Certificate>?> {
val nameCallback = NameCallback("Username: ")
val passwordCallback = PasswordCallback("Password: ", false)
val certificateCallback = CertificateCallback()
try {
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
} catch (e: IOException) {
throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user")
}
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
val certificates = certificateCallback.certificates
return Triple(username, password, certificates)
}
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
}
override fun commit(): Boolean {
val result = loginSucceeded
if (result) {
subject.principals.addAll(principals)
}
clear()
return result
}
override fun abort(): Boolean {
clear()
return true
}
override fun logout(): Boolean {
subject.principals.removeAll(principals)
return true
}
private fun clear() {
loginSucceeded = false
}
}

View File

@ -10,6 +10,10 @@ import net.corda.core.utilities.seconds
import net.corda.node.internal.artemis.CertificateChainCheckPolicy import net.corda.node.internal.artemis.CertificateChainCheckPolicy
import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.nodeapi.internal.config.* import net.corda.nodeapi.internal.config.*
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -19,7 +23,6 @@ import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
val Int.MB: Long get() = this * 1024L * 1024L val Int.MB: Long get() = this * 1024L * 1024L
interface NodeConfiguration : NodeSSLConfiguration { interface NodeConfiguration : NodeSSLConfiguration {
@ -148,7 +151,8 @@ data class NodeConfigurationImpl(
override val messagingServerAddress: NetworkHostAndPort?, override val messagingServerAddress: NetworkHostAndPort?,
override val messagingServerExternal: Boolean = (messagingServerAddress != null), override val messagingServerExternal: Boolean = (messagingServerAddress != null),
override val notary: NotaryConfig?, override val notary: NotaryConfig?,
override val certificateChainCheckPolicies: List<CertChainPolicyConfig>, @Deprecated("Do not configure")
override val certificateChainCheckPolicies: List<CertChainPolicyConfig> = emptyList(),
override val devMode: Boolean = false, override val devMode: Boolean = false,
override val noLocalShell: Boolean = false, override val noLocalShell: Boolean = false,
override val devModeOptions: DevModeOptions? = null, override val devModeOptions: DevModeOptions? = null,
@ -171,9 +175,9 @@ data class NodeConfigurationImpl(
private val logger = loggerFor<NodeConfigurationImpl>() private val logger = loggerFor<NodeConfigurationImpl>()
} }
override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, crlCheckSoftFail)) override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, BrokerRpcSslOptions(baseDirectory / "certificates" / "nodekeystore.jks", keyStorePassword))
private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: SSLConfiguration): NodeRpcOptions { private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: BrokerRpcSslOptions): NodeRpcOptions {
return when { return when {
explicitAddress != null -> { explicitAddress != null -> {
require(settings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." } require(settings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." }
@ -243,17 +247,22 @@ data class NodeConfigurationImpl(
require(security == null || rpcUsers.isEmpty()) { require(security == null || rpcUsers.isEmpty()) {
"Cannot specify both 'rpcUsers' and 'security' in configuration" "Cannot specify both 'rpcUsers' and 'security' in configuration"
} }
if(certificateChainCheckPolicies.isNotEmpty()) {
logger.warn("""You are configuring certificateChainCheckPolicies. This is a setting that is not used, and will be removed in a future version.
|Please contact the R3 team on the public slack to discuss your use case.
""".trimMargin())
}
} }
} }
data class NodeRpcSettings( data class NodeRpcSettings(
val address: NetworkHostAndPort?, val address: NetworkHostAndPort,
val adminAddress: NetworkHostAndPort?, val adminAddress: NetworkHostAndPort,
val standAloneBroker: Boolean = false, val standAloneBroker: Boolean = false,
val useSsl: Boolean = false, val useSsl: Boolean = false,
val ssl: SslOptions? val ssl: BrokerRpcSslOptions?
) { ) {
fun asOptions(fallbackSslOptions: SSLConfiguration): NodeRpcOptions { fun asOptions(fallbackSslOptions: BrokerRpcSslOptions): NodeRpcOptions {
return object : NodeRpcOptions { return object : NodeRpcOptions {
override val address = this@NodeRpcSettings.address override val address = this@NodeRpcSettings.address
override val adminAddress = this@NodeRpcSettings.adminAddress override val adminAddress = this@NodeRpcSettings.adminAddress
@ -281,6 +290,7 @@ enum class CertChainPolicyType {
UsernameMustMatch UsernameMustMatch
} }
@Deprecated("Do not use")
data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set<String>) { data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set<String>) {
val certificateChainCheckPolicy: CertificateChainCheckPolicy val certificateChainCheckPolicy: CertificateChainCheckPolicy
get() { get() {

View File

@ -1,23 +0,0 @@
package net.corda.node.services.config
import net.corda.nodeapi.internal.config.SSLConfiguration
import java.nio.file.Path
import java.nio.file.Paths
// TODO: we use both SSL and Ssl for names. We should pick one of them, or even better change to TLS
data class SslOptions(override val certificatesDirectory: Path,
override val keyStorePassword: String,
override val trustStorePassword: String,
override val crlCheckSoftFail: Boolean) : SSLConfiguration {
fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(),
keyStorePassword: String = this.keyStorePassword,
trustStorePassword: String = this.trustStorePassword,
crlCheckSoftFail: Boolean = this.crlCheckSoftFail): SslOptions = copy(
certificatesDirectory = certificatesDirectory.toAbsolutePath(),
keyStorePassword = keyStorePassword,
trustStorePassword = trustStorePassword,
crlCheckSoftFail = crlCheckSoftFail)
}
private fun String.toAbsolutePath() = Paths.get(this).toAbsolutePath()

View File

@ -1,12 +1,12 @@
package net.corda.node.services.config.rpc package net.corda.node.services.config.rpc
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.BrokerRpcSslOptions
interface NodeRpcOptions { interface NodeRpcOptions {
val address: NetworkHostAndPort? val address: NetworkHostAndPort
val adminAddress: NetworkHostAndPort? val adminAddress: NetworkHostAndPort
val standAloneBroker: Boolean val standAloneBroker: Boolean
val useSsl: Boolean val useSsl: Boolean
val sslConfig: SSLConfiguration val sslConfig: BrokerRpcSslOptions
} }

View File

@ -1,42 +1,21 @@
package net.corda.node.services.config.shell package net.corda.node.services.config.shell
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.Permissions
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
import net.corda.tools.shell.ShellConfiguration import net.corda.tools.shell.ShellConfiguration
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.SSH_PORT
import net.corda.tools.shell.ShellSslOptions
//re-packs data to Shell specific classes //re-packs data to Shell specific classes
fun NodeConfiguration.toShellConfig(): ShellConfiguration { fun NodeConfiguration.toShellConfig() = ShellConfiguration(
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
val sslConfiguration = if (this.rpcOptions.useSsl) { cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
with(this.rpcOptions.sslConfig) { user = INTERNAL_SHELL_USER,
ShellSslOptions(sslKeystore, password = INTERNAL_SHELL_USER,
keyStorePassword, hostAndPort = this.rpcOptions.adminAddress,
trustStoreFile, nodeSslConfig = this,
trustStorePassword, sshdPort = this.sshd?.port,
crlCheckSoftFail) sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
} noLocalShell = this.noLocalShell)
} else {
null
}
val localShellUser: User = localShellUser()
return ShellConfiguration(
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
user = localShellUser.username,
password = localShellUser.password,
hostAndPort = this.rpcOptions.address ?: NetworkHostAndPort("localhost", SSH_PORT),
ssl = sslConfiguration,
sshdPort = this.sshd?.port,
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
noLocalShell = this.noLocalShell)
}
fun localShellUser() = User("shell", "shell", setOf(Permissions.all()))

View File

@ -2,59 +2,36 @@ package net.corda.node.services.messaging
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.noneOrSingle
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.*
import net.corda.node.internal.artemis.BrokerAddresses import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_P2P_ROLE
import net.corda.node.internal.artemis.CertificateChainCheckPolicy import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.PEER_ROLE
import net.corda.node.internal.artemis.SecureArtemisConfiguration
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.requireOnDefaultFileSystem import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.Configuration
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import java.io.IOException import java.io.IOException
import java.security.KeyStoreException import java.security.KeyStoreException
import java.security.Principal
import java.util.*
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler
import javax.security.auth.callback.NameCallback
import javax.security.auth.callback.UnsupportedCallbackException
import javax.security.auth.login.AppConfigurationEntry import javax.security.auth.login.AppConfigurationEntry
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED
import javax.security.auth.login.FailedLoginException
import javax.security.auth.login.LoginException
import javax.security.auth.spi.LoginModule
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman. // TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later) // TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
@ -133,11 +110,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
bindingsDirectory = (artemisDir / "bindings").toString() bindingsDirectory = (artemisDir / "bindings").toString()
journalDirectory = (artemisDir / "journal").toString() journalDirectory = (artemisDir / "journal").toString()
largeMessagesDirectory = (artemisDir / "large-messages").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString()
val connectionDirection = ConnectionDirection.Inbound( acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config))
acceptorFactoryClassName = NettyAcceptorFactory::class.java.name
)
val acceptors = mutableSetOf(createTcpTransport(connectionDirection, messagingServerAddress.host, messagingServerAddress.port))
acceptorConfigurations = acceptors
// Enable built in message deduplication. Note we still have to do our own as the delayed commits // Enable built in message deduplication. Note we still have to do our own as the delayed commits
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates. // and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
@ -164,11 +137,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
* 4. Verifiers. These are given read access to the verification request queue and write access to the response queue. * 4. Verifiers. These are given read access to the verification request queue and write access to the response queue.
*/ */
private fun ConfigurationImpl.configureAddressSecurity(): Configuration { private fun ConfigurationImpl.configureAddressSecurity(): Configuration {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true, true, true) val nodeInternalRole = Role(NODE_P2P_ROLE, true, true, true, true, true, true, true, true, true, true)
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true)) securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
securityRoles[VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(VERIFIER_ROLE, consume = true))
securityRoles["${VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX}.#"] = setOf(nodeInternalRole, restrictedRole(VERIFIER_ROLE, send = true))
return this return this
} }
@ -184,158 +155,16 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
val keyStore = config.loadSslKeyStore().internal val keyStore = config.loadSslKeyStore().internal
val trustStore = config.loadTrustStore().internal val trustStore = config.loadTrustStore().internal
val defaultCertPolicies = mapOf(
PEER_ROLE to CertificateChainCheckPolicy.RootMustMatch,
NODE_ROLE to CertificateChainCheckPolicy.LeafMustMatch,
VERIFIER_ROLE to CertificateChainCheckPolicy.RootMustMatch
)
val certChecks = defaultCertPolicies.mapValues { (role, defaultPolicy) ->
val configPolicy = config.certificateChainCheckPolicies.noneOrSingle { it.role == role }?.certificateChainCheckPolicy
(configPolicy ?: defaultPolicy).createCheck(keyStore, trustStore)
}
val securityConfig = object : SecurityConfiguration() { val securityConfig = object : SecurityConfiguration() {
// Override to make it work with our login module // Override to make it work with our login module
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf(NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks) val options = mapOf(
BrokerJaasLoginModule.P2P_SECURITY_CONFIG to P2PJaasConfig(keyStore, trustStore),
BrokerJaasLoginModule.NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options)) return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
} }
} }
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig) return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig)
} }
private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) =
ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL)
} }
/**
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
* the CN within that is their legal name.
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
*/
class NodeLoginModule : LoginModule {
companion object {
// Include forbidden username character to prevent name clash with any RPC usernames
const val PEER_ROLE = "SystemRoles/Peer"
const val NODE_ROLE = "SystemRoles/Node"
const val VERIFIER_ROLE = "SystemRoles/Verifier"
const val CERT_CHAIN_CHECKS_OPTION_NAME = "CertChainChecks"
private val log = contextLogger()
}
private var loginSucceeded: Boolean = false
private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler
private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
private lateinit var verifierCertCheck: CertificateChainCheckPolicy.Check
private val principals = ArrayList<Principal>()
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME])
peerCertCheck = certChainChecks[PEER_ROLE]!!
nodeCertCheck = certChainChecks[NODE_ROLE]!!
verifierCertCheck = certChainChecks[VERIFIER_ROLE]!!
}
override fun login(): Boolean {
val nameCallback = NameCallback("Username: ")
val certificateCallback = CertificateCallback()
try {
callbackHandler.handle(arrayOf(nameCallback, certificateCallback))
} catch (e: IOException) {
throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user")
}
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val certificates = certificateCallback.certificates
log.debug { "Processing login for $username" }
try {
val validatedUser = when (determineUserRole(certificates, username)) {
PEER_ROLE -> authenticatePeer(certificates)
NODE_ROLE -> authenticateNode(certificates)
VERIFIER_ROLE -> authenticateVerifier(certificates)
else -> throw FailedLoginException("Peer does not belong on our network")
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
} catch (exception: FailedLoginException) {
log.warn("$exception")
throw exception
}
}
private fun authenticateNode(certificates: Array<javax.security.cert.X509Certificate>): String {
nodeCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(NODE_ROLE)
return certificates.first().subjectDN.name
}
private fun authenticateVerifier(certificates: Array<javax.security.cert.X509Certificate>): String {
verifierCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(VERIFIER_ROLE)
return certificates.first().subjectDN.name
}
private fun authenticatePeer(certificates: Array<javax.security.cert.X509Certificate>): String {
peerCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(PEER_ROLE)
return certificates.first().subjectDN.name
}
private fun determineUserRole(certificates: Array<javax.security.cert.X509Certificate>?, username: String): String? {
fun requireTls() = require(certificates != null) { "No TLS?" }
return when (username) {
PEER_USER -> {
requireTls()
PEER_ROLE
}
NODE_USER -> {
requireTls()
NODE_ROLE
}
VerifierApi.VERIFIER_USERNAME -> {
requireTls()
VERIFIER_ROLE
}
else -> null
}
}
override fun commit(): Boolean {
val result = loginSucceeded
if (result) {
subject.principals.addAll(principals)
}
clear()
return result
}
override fun abort(): Boolean {
clear()
return true
}
override fun logout(): Boolean {
subject.principals.removeAll(principals)
return true
}
private fun clear() {
loginSucceeded = false
}
}

View File

@ -0,0 +1,48 @@
package net.corda.node.services.messaging
import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER
import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ServerLocator
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
/**
* Used by the Node to communicate with the RPC broker.
*/
class InternalRPCMessagingClient(val sslConfig: SSLConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name) : SingletonSerializeAsToken(), AutoCloseable {
private var locator: ServerLocator? = null
private var rpcServer: RPCServer? = null
fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {
val tcpTransport = ArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig)
locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted.
connectionTTL = -1
clientFailureCheckPeriod = -1
minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null
}
rpcServer = RPCServer(rpcOps, NODE_RPC_USER, NODE_RPC_USER, locator!!, securityManager, nodeName)
}
fun start(serverControl: ActiveMQServerControl) = synchronized(this) {
rpcServer!!.start(serverControl)
}
fun stop(): Unit = synchronized(this) {
rpcServer?.close()
locator?.close()
}
override fun close() = stop()
}

View File

@ -29,8 +29,7 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.services.statemachine.DeduplicationId
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.PersistentMap import net.corda.node.utilities.PersistentMap
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
@ -200,7 +199,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
started = true started = true
log.info("Connecting to message broker: $serverAddress") log.info("Connecting to message broker: $serverAddress")
// TODO Add broker CN to config for host verification in case the embedded broker isn't used // TODO Add broker CN to config for host verification in case the embedded broker isn't used
val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, config) val tcpTransport = p2pConnectorTcpTransport(serverAddress, config)
locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted. // would be the default and the two lines below can be deleted.
@ -214,7 +213,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
// using our TLS certificate. // using our TLS certificate.
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer // Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
// size of 1MB is acknowledged. // size of 1MB is acknowledged.
val createNewSession = { sessionFactory!!.createSession(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER, false, true, true, locator!!.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) } val createNewSession = { sessionFactory!!.createSession(ArtemisMessagingComponent.NODE_P2P_USER, ArtemisMessagingComponent.NODE_P2P_USER, false, true, true, locator!!.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) }
producerSession = createNewSession() producerSession = createNewSession()
bridgeSession = createNewSession() bridgeSession = createNewSession()

View File

@ -1,34 +0,0 @@
package net.corda.node.services.messaging
import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort, maxMessageSize: Int) : SingletonSerializeAsToken(), AutoCloseable {
private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize)
private var rpcServer: RPCServer? = null
fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {
val locator = artemis.start().sessionFactory.serverLocator
val myCert = config.loadSslKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_TLS)
rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, securityManager, CordaX500Name.build(myCert.subjectX500Principal))
}
fun start2(serverControl: ActiveMQServerControl) = synchronized(this) {
rpcServer!!.start(serverControl)
}
fun stop() = synchronized(this) {
rpcServer?.close()
artemis.stop()
}
override fun close() = stop()
}

View File

@ -1,47 +1,44 @@
package net.corda.node.services.rpc package net.corda.node.services.rpc
import net.corda.core.internal.noneOrSingle
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.*
import net.corda.node.internal.artemis.BrokerAddresses import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECURITY_CONFIG
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.config.CertChainPolicyConfig import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.loadKeyStore
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import rx.Observable
import java.io.IOException import java.io.IOException
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyStoreException import java.security.KeyStoreException
import java.util.concurrent.CompletableFuture
import javax.security.auth.login.AppConfigurationEntry import javax.security.auth.login.AppConfigurationEntry
internal class ArtemisRpcBroker internal constructor( internal class ArtemisRpcBroker internal constructor(
address: NetworkHostAndPort, address: NetworkHostAndPort,
private val adminAddressOptional: NetworkHostAndPort?, private val adminAddressOptional: NetworkHostAndPort?,
private val sslOptions: SSLConfiguration, private val sslOptions: BrokerRpcSslOptions?,
private val useSsl: Boolean, private val useSsl: Boolean,
private val securityManager: RPCSecurityManager, private val securityManager: RPCSecurityManager,
private val certificateChainCheckPolicies: List<CertChainPolicyConfig>,
private val maxMessageSize: Int, private val maxMessageSize: Int,
private val jmxEnabled: Boolean = false, private val jmxEnabled: Boolean = false,
private val baseDirectory: Path) : ArtemisBroker { private val baseDirectory: Path,
private val nodeConfiguration: SSLConfiguration,
private val shouldStartLocalShell: Boolean) : ArtemisBroker {
companion object { companion object {
private val logger = loggerFor<ArtemisRpcBroker>() private val logger = loggerFor<ArtemisRpcBroker>()
fun withSsl(address: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker { fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
return ArtemisRpcBroker(address, null, sslOptions, true, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory) return ArtemisRpcBroker(address, adminAddress, sslOptions, true, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
} }
fun withoutSsl(address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker { fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
return ArtemisRpcBroker(address, adminAddress, sslOptions, false, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory) return ArtemisRpcBroker(address, adminAddress, null, false, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
} }
} }
@ -66,8 +63,8 @@ internal class ArtemisRpcBroker internal constructor(
private val server = initialiseServer() private val server = initialiseServer()
private fun initialiseServer(): ActiveMQServer { private fun initialiseServer(): ActiveMQServer {
val serverConfiguration = RpcBrokerConfiguration(baseDirectory, maxMessageSize, jmxEnabled, addresses.primary, adminAddressOptional, sslOptions, useSsl) val serverConfiguration = RpcBrokerConfiguration(baseDirectory, maxMessageSize, jmxEnabled, addresses.primary, adminAddressOptional, sslOptions, useSsl, nodeConfiguration, shouldStartLocalShell)
val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener, sslOptions) val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener)
return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply { return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply {
registerActivationFailureListener { exception -> throw exception } registerActivationFailureListener { exception -> throw exception }
@ -76,33 +73,21 @@ internal class ArtemisRpcBroker internal constructor(
} }
@Throws(IOException::class, KeyStoreException::class) @Throws(IOException::class, KeyStoreException::class)
private fun createArtemisSecurityManager(loginListener: LoginListener, sslOptions: SSLConfiguration): ActiveMQJAASSecurityManager { private fun createArtemisSecurityManager(loginListener: LoginListener): ActiveMQJAASSecurityManager {
val keyStore = loadKeyStore(sslOptions.sslKeystore, sslOptions.keyStorePassword) val keyStore = nodeConfiguration.loadSslKeyStore().internal
val trustStore = loadKeyStore(sslOptions.trustStoreFile, sslOptions.trustStorePassword) val trustStore = nodeConfiguration.loadTrustStore().internal
val defaultCertPolicies = mapOf(
NodeLoginModule.NODE_ROLE to CertificateChainCheckPolicy.LeafMustMatch,
NodeLoginModule.RPC_ROLE to CertificateChainCheckPolicy.Any
)
val certChecks = defaultCertPolicies.mapValues { (role, defaultPolicy) ->
val policy = certificateChainCheckPolicies.noneOrSingle { it.role == role }?.certificateChainCheckPolicy ?: defaultPolicy
policy.createCheck(keyStore, trustStore)
}
val securityConfig = object : SecurityConfiguration() { val securityConfig = object : SecurityConfiguration() {
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf( val options = mapOf(
NodeLoginModule.LOGIN_LISTENER_ARG to loginListener, RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl),
NodeLoginModule.SECURITY_MANAGER_ARG to securityManager, NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
NodeLoginModule.USE_SSL_ARG to useSsl, )
NodeLoginModule.CERT_CHAIN_CHECKS_ARG to certChecks)
return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)) return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options))
} }
} }
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig) return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig)
} }
} }
typealias LoginListener = (String) -> Unit typealias LoginListener = (String) -> Unit
private fun <RESULT> CompletableFuture<RESULT>.toObservable() = Observable.from(this)

View File

@ -1,169 +0,0 @@
package net.corda.node.services.rpc
import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.loggerFor
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import java.io.IOException
import java.security.Principal
import java.util.*
import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler
import javax.security.auth.callback.NameCallback
import javax.security.auth.callback.PasswordCallback
import javax.security.auth.callback.UnsupportedCallbackException
import javax.security.auth.login.FailedLoginException
import javax.security.auth.login.LoginException
import javax.security.auth.spi.LoginModule
import javax.security.cert.X509Certificate
/**
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
* the CN within that is their legal name.
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
*/
internal class NodeLoginModule : LoginModule {
companion object {
internal const val NODE_ROLE = "SystemRoles/Node"
internal const val RPC_ROLE = "SystemRoles/RPC"
internal const val CERT_CHAIN_CHECKS_ARG = "CertChainChecks"
internal const val USE_SSL_ARG = "useSsl"
internal const val SECURITY_MANAGER_ARG = "RpcSecurityManager"
internal const val LOGIN_LISTENER_ARG = "LoginListener"
private val log = loggerFor<NodeLoginModule>()
}
private var loginSucceeded: Boolean = false
private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler
private lateinit var securityManager: RPCSecurityManager
private lateinit var loginListener: LoginListener
private var useSsl: Boolean? = null
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
private lateinit var rpcCertCheck: CertificateChainCheckPolicy.Check
private val principals = ArrayList<Principal>()
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
securityManager = uncheckedCast(options[SECURITY_MANAGER_ARG])
loginListener = uncheckedCast(options[LOGIN_LISTENER_ARG])
useSsl = options[USE_SSL_ARG] as Boolean
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_ARG])
nodeCertCheck = certChainChecks[NODE_ROLE]!!
rpcCertCheck = certChainChecks[RPC_ROLE]!!
}
override fun login(): Boolean {
val nameCallback = NameCallback("Username: ")
val passwordCallback = PasswordCallback("Password: ", false)
val certificateCallback = CertificateCallback()
try {
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
} catch (e: IOException) {
throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user")
}
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
val certificates = certificateCallback.certificates ?: emptyArray()
if (rpcCertCheck is CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck) {
(rpcCertCheck as CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck).username = username
}
log.debug("Logging user in")
try {
val role = determineUserRole(certificates, username, useSsl!!)
val validatedUser = when (role) {
NodeLoginModule.NODE_ROLE -> {
authenticateNode(certificates)
NODE_USER
}
RPC_ROLE -> {
authenticateRpcUser(username, Password(password), certificates, useSsl!!)
username
}
else -> throw FailedLoginException("Peer does not belong on our network")
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
} catch (exception: FailedLoginException) {
log.warn("$exception")
throw exception
}
}
private fun authenticateNode(certificates: Array<X509Certificate>) {
nodeCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(NodeLoginModule.NODE_ROLE)
}
private fun authenticateRpcUser(username: String, password: Password, certificates: Array<X509Certificate>, useSsl: Boolean) {
if (useSsl) {
rpcCertCheck.checkCertificateChain(certificates)
// no point in matching username with CN because companies wouldn't want to provide a certificate for each user
}
securityManager.authenticate(username, password)
loginListener(username)
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses
}
private fun determineUserRole(certificates: Array<X509Certificate>, username: String, useSsl: Boolean): String? {
return when (username) {
ArtemisMessagingComponent.NODE_USER -> {
require(certificates.isNotEmpty()) { "No TLS?" }
NodeLoginModule.NODE_ROLE
}
else -> {
if (useSsl) {
require(certificates.isNotEmpty()) { "No TLS?" }
}
return RPC_ROLE
}
}
}
override fun commit(): Boolean {
val result = loginSucceeded
if (result) {
subject.principals.addAll(principals)
}
clear()
return result
}
override fun abort(): Boolean {
clear()
return true
}
override fun logout(): Boolean {
subject.principals.removeAll(principals)
return true
}
private fun clear() {
loginSucceeded = false
}
}

View File

@ -8,10 +8,10 @@ import org.apache.activemq.artemis.core.settings.HierarchicalRepository
* Helper class to dynamically assign security roles to RPC users * Helper class to dynamically assign security roles to RPC users
* on their authentication. This object is plugged into the server * on their authentication. This object is plugged into the server
* as [SecuritySettingPlugin]. It responds to authentication events * as [SecuritySettingPlugin]. It responds to authentication events
* from [NodeLoginModule] by adding the address -> roles association * from [BrokerJaasLoginModule] by adding the address -> roles association
* generated by the given [source], unless already done before. * generated by the given [source], unless already done before.
*/ */
internal class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>) : SecuritySettingPlugin { internal class RolesAdderOnLogin(val systemUsers: List<String> = emptyList(), val source: (String) -> Pair<String, Set<Role>>) : SecuritySettingPlugin {
private lateinit var repository: RolesRepository private lateinit var repository: RolesRepository
fun onLogin(username: String) { fun onLogin(username: String) {
@ -24,6 +24,8 @@ internal class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>
override fun setSecurityRepository(repository: RolesRepository) { override fun setSecurityRepository(repository: RolesRepository) {
this.repository = repository this.repository = repository
// This dynamically adds roles to the specified system users as soon as the repository is initialized.
systemUsers.forEach(::onLogin)
} }
override fun stop() = this override fun stop() = this

View File

@ -2,30 +2,32 @@ package net.corda.node.services.rpc
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.artemis.BrokerJaasLoginModule
import net.corda.node.internal.artemis.SecureArtemisConfiguration import net.corda.node.internal.artemis.SecureArtemisConfiguration
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
import org.apache.activemq.artemis.core.settings.impl.AddressSettings import org.apache.activemq.artemis.core.settings.impl.AddressSettings
import java.nio.file.Path import java.nio.file.Path
internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: SSLConfiguration?, useSsl: Boolean) : SecureArtemisConfiguration() { internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: SSLConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() {
val loginListener: (String) -> Unit val loginListener: (String) -> Unit
init { init {
setDirectories(baseDirectory) setDirectories(baseDirectory)
val acceptorConfigurationsSet = mutableSetOf(acceptorConfiguration(address, useSsl, sslOptions)) val acceptorConfigurationsSet = mutableSetOf(
rpcAcceptorTcpTransport(address, sslOptions, useSsl)
)
adminAddress?.let { adminAddress?.let {
acceptorConfigurationsSet += acceptorConfiguration(adminAddress, true, sslOptions) acceptorConfigurationsSet += rpcInternalAcceptorTcpTransport(it, nodeConfiguration)
} }
acceptorConfigurations = acceptorConfigurationsSet acceptorConfigurations = acceptorConfigurationsSet
@ -41,9 +43,10 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
initialiseSettings(maxMessageSize) initialiseSettings(maxMessageSize)
val nodeInternalRole = Role(NodeLoginModule.NODE_ROLE, true, true, true, true, true, true, true, true, true, true) val nodeInternalRole = Role(BrokerJaasLoginModule.NODE_RPC_ROLE, true, true, true, true, true, true, true, true, true, true)
val rolesAdderOnLogin = RolesAdderOnLogin { username -> val addRPCRoleToUsers = if (shouldStartLocalShell) listOf(ArtemisMessagingComponent.INTERNAL_SHELL_USER) else emptyList()
val rolesAdderOnLogin = RolesAdderOnLogin(addRPCRoleToUsers) { username ->
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole(
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username", "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username",
consume = true, consume = true,
@ -63,7 +66,7 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
private fun configureAddressSecurity(nodeInternalRole: Role, rolesAdderOnLogin: RolesAdderOnLogin) { private fun configureAddressSecurity(nodeInternalRole: Role, rolesAdderOnLogin: RolesAdderOnLogin) {
securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole) securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole)
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(NodeLoginModule.RPC_ROLE, send = true)) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(BrokerJaasLoginModule.RPC_ROLE, send = true))
securitySettingPlugins.add(rolesAdderOnLogin) securitySettingPlugins.add(rolesAdderOnLogin)
} }
@ -118,11 +121,6 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
return configuration return configuration
} }
private fun acceptorConfiguration(address: NetworkHostAndPort, enableSsl: Boolean, sslOptions: SSLConfiguration?): TransportConfiguration {
return tcpTransport(ConnectionDirection.Inbound(NettyAcceptorFactory::class.java.name), address, sslOptions, enableSsl)
}
private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false, private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false,
deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false, deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false,
deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role { deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role {

View File

@ -6,6 +6,7 @@ import net.corda.core.internal.div
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
@ -151,7 +152,7 @@ class NodeConfigurationImplTest {
adminAddress = NetworkHostAndPort("localhost", 2), adminAddress = NetworkHostAndPort("localhost", 2),
standAloneBroker = false, standAloneBroker = false,
useSsl = false, useSsl = false,
ssl = SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, true)) ssl = null)
return NodeConfigurationImpl( return NodeConfigurationImpl(
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
myLegalName = ALICE_NAME, myLegalName = ALICE_NAME,
@ -165,7 +166,6 @@ class NodeConfigurationImplTest {
messagingServerAddress = null, messagingServerAddress = null,
p2pMessagingRetry = P2PMessagingRetryConfiguration(5.seconds, 3, 1.0), p2pMessagingRetry = P2PMessagingRetryConfiguration(5.seconds, 3, 1.0),
notary = null, notary = null,
certificateChainCheckPolicies = emptyList(),
devMode = true, devMode = true,
noLocalShell = false, noLocalShell = false,
rpcSettings = rpcSettings, rpcSettings = rpcSettings,

View File

@ -3,23 +3,26 @@ package net.corda.node.services.rpc
import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClient
import net.corda.core.context.AuthServiceId import net.corda.core.context.AuthServiceId
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.ArtemisBroker
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.all
import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.messaging.InternalRPCMessagingClient
import net.corda.node.services.messaging.RPCMessagingClient import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.ConnectionDirection import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.common.internal.withCertificates
import net.corda.testing.common.internal.withKeyStores
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree
import net.corda.testing.internal.createKeyPairAndSelfSignedCertificate
import net.corda.testing.internal.createNodeSslConfig
import net.corda.testing.internal.saveToKeyStore
import net.corda.testing.internal.saveToTrustStore
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
@ -27,9 +30,8 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.nio.file.Files import org.junit.rules.TemporaryFolder
import java.nio.file.Path import java.nio.file.Path
import kotlin.reflect.KClass
class ArtemisRpcTests { class ArtemisRpcTests {
private val ports: PortAllocation = RandomFree private val ports: PortAllocation = RandomFree
@ -42,168 +44,86 @@ class ArtemisRpcTests {
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule(true) val testSerialization = SerializationEnvironmentRule(true)
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test @Test
fun rpc_with_ssl_enabled() { fun rpc_with_ssl_enabled() {
withCertificates { server, client, createSelfSigned, createSignedBy -> val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB")) val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate) val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert)
// truststore needs to contain root CA for how the driver works... val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
server.keyStore["cordaclienttls"] = rootCertificate testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
server.trustStore["cordaclienttls"] = rootCertificate
server.trustStore["mark"] = markCertificate
client.keyStore["mark"] = markCertificate
client.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
testSslCommunication(brokerSslOptions, true, clientSslOptions)
}
}
} }
@Test @Test
fun rpc_with_ssl_disabled() { fun rpc_with_ssl_disabled() {
withCertificates { server, client, createSelfSigned, _ -> testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), null, false, null)
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
// truststore needs to contain root CA for how the driver works...
server.keyStore["cordaclienttls"] = rootCertificate
server.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { brokerSslOptions, _ ->
// here server is told not to use SSL, and client sslOptions are null (as in, do not use SSL)
testSslCommunication(brokerSslOptions, false, null)
}
}
}
@Test
fun rpc_with_server_certificate_untrusted_to_client() {
withCertificates { server, client, createSelfSigned, createSignedBy ->
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works...
server.keyStore["cordaclienttls"] = rootCertificate
server.trustStore["cordaclienttls"] = rootCertificate
server.trustStore["mark"] = markCertificate
client.keyStore["mark"] = markCertificate
// here the server certificate is not trusted by the client
// client.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
}
}
}
@Test
fun rpc_with_no_client_certificate() {
withCertificates { server, client, createSelfSigned, createSignedBy ->
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works...
server.keyStore["cordaclienttls"] = rootCertificate
server.trustStore["cordaclienttls"] = rootCertificate
server.trustStore["mark"] = markCertificate
// here client keystore is empty
// client.keyStore["mark"] = markCertificate
client.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
}
}
} }
@Test @Test
fun rpc_with_no_ssl_on_client_side_and_ssl_on_server_side() { fun rpc_with_no_ssl_on_client_side_and_ssl_on_server_side() {
withCertificates { server, client, createSelfSigned, createSignedBy -> val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB")) val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate) val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
// here client sslOptions are passed null (as in, do not use SSL)
// truststore needs to contain root CA for how the driver works... assertThatThrownBy {
server.keyStore["cordaclienttls"] = rootCertificate testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, null)
server.trustStore["cordaclienttls"] = rootCertificate }.isInstanceOf(ActiveMQConnectionTimedOutException::class.java)
server.trustStore["mark"] = markCertificate
client.keyStore["mark"] = markCertificate
client.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { brokerSslOptions, _ ->
// here client sslOptions are passed null (as in, do not use SSL)
testSslCommunication(brokerSslOptions, true, null, clientConnectionSpy = expectExceptionOfType(ActiveMQConnectionTimedOutException::class))
}
}
} }
@Test @Test
fun rpc_client_certificate_untrusted_to_server() { fun rpc_client_certificate_untrusted_to_server() {
withCertificates { server, client, createSelfSigned, _ -> val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB")) val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
// here client's certificate is self-signed, otherwise Artemis allows the connection (the issuing certificate is in the truststore)
val markCertificate = createSelfSigned(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"))
// truststore needs to contain root CA for how the driver works... // create another keypair and certificate and add that to the client truststore
server.keyStore["cordaclienttls"] = rootCertificate // the ssl connection should not
server.trustStore["cordaclienttls"] = rootCertificate val (_, selfSignCert1) = createKeyPairAndSelfSignedCertificate()
// here the client certificate is not trusted by the server val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert1)
// server.trustStore["mark"] = markCertificate
client.keyStore["mark"] = markCertificate val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
client.trustStore["cordaclienttls"] = rootCertificate val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
withKeyStores(server, client) { brokerSslOptions, clientSslOptions -> assertThatThrownBy {
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class)) testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
} }.isInstanceOf(ActiveMQNotConnectedException::class.java)
}
} }
private fun testSslCommunication(brokerSslOptions: SSLConfiguration, useSslForBroker: Boolean, clientSslOptions: SSLConfiguration?, address: NetworkHostAndPort = ports.nextHostAndPort(), private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(),
adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = Files.createTempDirectory(null), clientConnectionSpy: (() -> Unit) -> Unit = {}) { adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath()) {
val maxMessageSize = 10000 val maxMessageSize = 10000
val jmxEnabled = false val jmxEnabled = false
val certificateChainCheckPolicies: List<CertChainPolicyConfig> = listOf()
val artemisBroker: ArtemisBroker = if (useSslForBroker) { val artemisBroker: ArtemisBroker = if (useSslForBroker) {
ArtemisRpcBroker.withSsl(address, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory) ArtemisRpcBroker.withSsl(nodeSSlconfig, address, adminAddress, brokerSslOptions!!, securityManager, maxMessageSize, jmxEnabled, baseDirectory, false)
} else { } else {
ArtemisRpcBroker.withoutSsl(address, adminAddress, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory) ArtemisRpcBroker.withoutSsl(nodeSSlconfig, address, adminAddress, securityManager, maxMessageSize, jmxEnabled, baseDirectory, false)
} }
artemisBroker.use { broker -> artemisBroker.use { broker ->
broker.start() broker.start()
RPCMessagingClient(brokerSslOptions, broker.addresses.admin, maxMessageSize).use { server -> InternalRPCMessagingClient(nodeSSlconfig, adminAddress, maxMessageSize, CordaX500Name("MegaCorp", "London", "GB")).use { server ->
server.start(TestRpcOpsImpl(), securityManager, broker.serverControl) server.start(TestRpcOpsImpl(), securityManager, broker.serverControl)
val client = RPCClient<TestRpcOps>(tcpTransport(ConnectionDirection.Outbound(), broker.addresses.primary, clientSslOptions)) val client = RPCClient<TestRpcOps>(rpcConnectorTcpTransport(broker.addresses.primary, clientSslOptions))
clientConnectionSpy { val greeting = client.start(TestRpcOps::class.java, user.username, user.password).use { connection ->
client.start(TestRpcOps::class.java, user.username, user.password).use { connection -> connection.proxy.greet("Frodo")
connection.proxy.apply {
val greeting = greet("Frodo")
assertThat(greeting).isEqualTo("Oh, hello Frodo!")
}
}
} }
assertThat(greeting).isEqualTo("Oh, hello Frodo!")
} }
} }
} }
private fun <OPS : RPCOps> RPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) { private fun <OPS : RPCOps> InternalRPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) {
apply { apply {
start(ops, securityManager) init(ops, securityManager)
start2(brokerControl) start(brokerControl)
} }
} }
private fun <EXCEPTION : Exception> expectExceptionOfType(exceptionType: KClass<EXCEPTION>): (() -> Unit) -> Unit {
return { action -> assertThatThrownBy { action.invoke() }.isInstanceOf(exceptionType.java) }
}
interface TestRpcOps : RPCOps { interface TestRpcOps : RPCOps {
fun greet(name: String): String fun greet(name: String): String
} }
@ -213,4 +133,7 @@ class ArtemisRpcTests {
override val protocolVersion: Int = 1 override val protocolVersion: Int = 1
} }
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
} }

View File

@ -50,6 +50,8 @@ interface NodeHandle : AutoCloseable {
val p2pAddress: NetworkHostAndPort val p2pAddress: NetworkHostAndPort
/** Get the rpc address for this node **/ /** Get the rpc address for this node **/
val rpcAddress: NetworkHostAndPort val rpcAddress: NetworkHostAndPort
/** Get the rpc admin address for this node **/
val rpcAdminAddress: NetworkHostAndPort
/** Get a [List] of [User]'s for this node **/ /** Get a [List] of [User]'s for this node **/
val rpcUsers: List<User> val rpcUsers: List<User>
/** The location of the node's base directory **/ /** The location of the node's base directory **/

View File

@ -24,7 +24,8 @@ interface NodeHandleInternal : NodeHandle {
val useHTTPS: Boolean val useHTTPS: Boolean
val webAddress: NetworkHostAndPort val webAddress: NetworkHostAndPort
override val p2pAddress: NetworkHostAndPort get() = configuration.p2pAddress override val p2pAddress: NetworkHostAndPort get() = configuration.p2pAddress
override val rpcAddress: NetworkHostAndPort get() = configuration.rpcOptions.address!! override val rpcAddress: NetworkHostAndPort get() = configuration.rpcOptions.address
override val rpcAdminAddress: NetworkHostAndPort get() = configuration.rpcOptions.adminAddress
override val baseDirectory: Path get() = configuration.baseDirectory override val baseDirectory: Path get() = configuration.baseDirectory
} }

View File

@ -6,8 +6,7 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
import net.corda.cordform.CordformContext import net.corda.cordform.CordformContext
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
@ -149,12 +148,8 @@ class DriverDSLImpl(
} }
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> { private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.corda.rpcOptions.address!! val rpcAddress = config.corda.rpcOptions.address
val client = if (config.corda.rpcOptions.useSsl) { val client = createCordaRPCClientWithInternalSslAndClassLoader(config.corda.rpcOptions.adminAddress, sslConfiguration = config.corda)
createCordaRPCClientWithSsl(rpcAddress, sslConfiguration = config.corda.rpcOptions.sslConfig)
} else {
CordaRPCClient(rpcAddress)
}
val connectionFuture = poll(executorService, "RPC connection") { val connectionFuture = poll(executorService, "RPC connection") {
try { try {
config.corda.rpcUsers[0].run { client.start(username, password) } config.corda.rpcUsers[0].run { client.start(username, password) }
@ -238,9 +233,13 @@ class DriverDSLImpl(
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
allowMissingConfig = true, allowMissingConfig = true,
configOverrides = configOf( configOverrides = configOf(
"p2pAddress" to "localhost:1222", // required argument, not really used "p2pAddress" to portAllocation.nextHostAndPort().toString(),
"compatibilityZoneURL" to compatibilityZoneURL.toString(), "compatibilityZoneURL" to compatibilityZoneURL.toString(),
"myLegalName" to providedName.toString(), "myLegalName" to providedName.toString(),
"rpcSettings" to mapOf(
"address" to portAllocation.nextHostAndPort().toString(),
"adminAddress" to portAllocation.nextHostAndPort().toString()
),
"devMode" to false) "devMode" to false)
)) ))
@ -341,17 +340,13 @@ class DriverDSLImpl(
val rpcAddress = if (cordform.rpcAddress == null) { val rpcAddress = if (cordform.rpcAddress == null) {
val overrides = mutableMapOf<String, Any>("rpcSettings.address" to portAllocation.nextHostAndPort().toString()) val overrides = mutableMapOf<String, Any>("rpcSettings.address" to portAllocation.nextHostAndPort().toString())
cordform.config.apply { cordform.config.apply {
if (!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) { overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
}
} }
overrides overrides
} else { } else {
val overrides = mutableMapOf<String, Any>() val overrides = mutableMapOf<String, Any>()
cordform.config.apply { cordform.config.apply {
if ((!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) && !hasPath("rpcSettings.adminAddress")) { overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
}
} }
overrides overrides
} }
@ -837,10 +832,10 @@ class DriverDSLImpl(
private val propertiesInScope = setOf("java.io.tmpdir", AbstractAMQPSerializationScheme.SCAN_SPEC_PROP_NAME) private val propertiesInScope = setOf("java.io.tmpdir", AbstractAMQPSerializationScheme.SCAN_SPEC_PROP_NAME)
private fun inheritFromParentProcess() : Iterable<Pair<String, String>> { private fun inheritFromParentProcess(): Iterable<Pair<String, String>> {
return propertiesInScope.flatMap { propName -> return propertiesInScope.flatMap { propName ->
val propValue : String? = System.getProperty(propName) val propValue: String? = System.getProperty(propName)
if(propValue == null) { if (propValue == null) {
emptySet() emptySet()
} else { } else {
setOf(Pair(propName, propValue)) setOf(Pair(propName, propValue))
@ -853,7 +848,7 @@ class DriverDSLImpl(
var config = ConfigFactory.empty() var config = ConfigFactory.empty()
config += "webAddress" to webAddress.toString() config += "webAddress" to webAddress.toString()
config += "myLegalName" to configuration.myLegalName.toString() config += "myLegalName" to configuration.myLegalName.toString()
config += "rpcAddress" to configuration.rpcOptions.address!!.toString() config += "rpcAddress" to configuration.rpcOptions.address.toString()
config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers") config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers")
config += "useHTTPS" to useHTTPS config += "useHTTPS" to useHTTPS
config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString() config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString()

View File

@ -467,7 +467,6 @@ private fun mockNodeConfiguration(): NodeConfiguration {
doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(true).whenever(it).devMode doReturn(true).whenever(it).devMode
doReturn(null).whenever(it).compatibilityZoneURL doReturn(null).whenever(it).compatibilityZoneURL
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(VerifierType.InMemory).whenever(it).verifierType doReturn(VerifierType.InMemory).whenever(it).verifierType
doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).p2pMessagingRetry doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).p2pMessagingRetry
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec

View File

@ -21,7 +21,6 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServer
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
@ -41,7 +40,6 @@ import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.security.CheckType import org.apache.activemq.artemis.core.security.CheckType
import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
@ -212,20 +210,19 @@ data class RPCDriverDSL(
} }
fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration { fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration {
val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name)
return ConfigurationImpl().apply { return ConfigurationImpl().apply {
val artemisDir = "$baseDirectory/artemis" val artemisDir = "$baseDirectory/artemis"
bindingsDirectory = "$artemisDir/bindings" bindingsDirectory = "$artemisDir/bindings"
journalDirectory = "$artemisDir/journal" journalDirectory = "$artemisDir/journal"
largeMessagesDirectory = "$artemisDir/large-messages" largeMessagesDirectory = "$artemisDir/large-messages"
acceptorConfigurations = setOf(ArtemisTcpTransport.tcpTransport(connectionDirection, hostAndPort, null)) acceptorConfigurations = setOf(ArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null))
configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) configureCommonSettings(maxFileSize, maxBufferedBytesPerClient)
} }
} }
val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name)
fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration {
return ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), hostAndPort, null) return ArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, null)
} }
} }
@ -336,7 +333,7 @@ data class RPCDriverDSL(
configuration: CordaRPCClientConfigurationImpl = CordaRPCClientConfigurationImpl.default configuration: CordaRPCClientConfigurationImpl = CordaRPCClientConfigurationImpl.default
): CordaFuture<I> { ): CordaFuture<I> {
return driverDSL.executorService.fork { return driverDSL.executorService.fork {
val client = RPCClient<I>(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration) val client = RPCClient<I>(ArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration)
val connection = client.start(rpcOpsClass, username, password, externalTrace) val connection = client.start(rpcOpsClass, username, password, externalTrace)
driverDSL.shutdownManager.registerShutdown { driverDSL.shutdownManager.registerShutdown {
connection.close() connection.close()

View File

@ -10,14 +10,16 @@ import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.serialization.internal.amqp.AMQP_ENABLED import net.corda.serialization.internal.amqp.AMQP_ENABLED
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@Suppress("unused") @Suppress("unused")
@ -92,13 +94,11 @@ fun createDevNodeCaCertPath(
return Triple(rootCa, intermediateCa, nodeCa) return Triple(rootCa, intermediateCa, nodeCa)
} }
fun SSLConfiguration.useSslRpcOverrides(): Map<String, Any> { fun BrokerRpcSslOptions.useSslRpcOverrides(): Map<String, String> {
return mapOf( return mapOf(
"rpcSettings.useSsl" to "true", "rpcSettings.useSsl" to "true",
"rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), "rpcSettings.ssl.keyStorePath" to keyStorePath.toAbsolutePath().toString(),
"rpcSettings.ssl.keyStorePassword" to keyStorePassword, "rpcSettings.ssl.keyStorePassword" to keyStorePassword
"rpcSettings.ssl.trustStorePassword" to trustStorePassword,
"rpcSettings.ssl.crlCheckSoftFail" to true
) )
} }
@ -125,3 +125,40 @@ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe
* TODO: Should be removed after multiple identities are introduced. * TODO: Should be removed after multiple identities are introduced.
*/ */
fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party
fun createNodeSslConfig(path: Path, name: CordaX500Name = CordaX500Name("MegaCorp", "London", "GB")): SSLConfiguration {
val sslConfig = object : SSLConfiguration {
override val crlCheckSoftFail = true
override val certificatesDirectory = path
override val keyStorePassword = "serverstorepass"
override val trustStorePassword = "trustpass"
}
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
sslConfig.createDevKeyStores(name, rootCa.certificate, intermediateCa)
val trustStore = loadOrCreateKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate)
trustStore.save(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
return sslConfig
}
fun createKeyPairAndSelfSignedCertificate(): Pair<KeyPair, X509Certificate> {
val rpcKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, rpcKeyPair)
return Pair(rpcKeyPair, selfSignCert)
}
fun saveToKeyStore(keyStorePath: Path, rpcKeyPair: KeyPair, selfSignCert: X509Certificate, password: String = "password"): Path {
val keyStore = loadOrCreateKeyStore(keyStorePath, password)
keyStore.addOrReplaceKey("Key", rpcKeyPair.private, password.toCharArray(), arrayOf(selfSignCert))
keyStore.save(keyStorePath, password)
return keyStorePath
}
fun saveToTrustStore(trustStorePath: Path, selfSignCert: X509Certificate, password: String = "password"): Path {
val trustStore = loadOrCreateKeyStore(trustStorePath, password)
trustStore.addOrReplaceCertificate("Key", selfSignCert)
trustStore.save(trustStorePath, password)
return trustStorePath
}

View File

@ -3,17 +3,22 @@ package net.corda.tools.shell
import com.google.common.io.Files import com.google.common.io.Files
import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch import com.jcraft.jsch.JSch
import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.all
import net.corda.testing.common.internal.withCertificates import net.corda.node.services.config.shell.toShellConfig
import net.corda.testing.common.internal.withKeyStores import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree
import net.corda.testing.internal.createKeyPairAndSelfSignedCertificate
import net.corda.testing.internal.saveToKeyStore
import net.corda.testing.internal.saveToTrustStore
import net.corda.testing.internal.useSslRpcOverrides import net.corda.testing.internal.useSslRpcOverrides
import net.corda.testing.node.User import net.corda.testing.node.User
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
@ -22,11 +27,17 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.util.io.Streams import org.bouncycastle.util.io.Streams
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertTrue import kotlin.test.assertTrue
class InteractiveShellIntegrationTest { class InteractiveShellIntegrationTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test @Test
fun `shell should not log in with invalid credentials`() { fun `shell should not log in with invalid credentials`() {
val user = User("u", "p", setOf()) val user = User("u", "p", setOf())
@ -44,7 +55,7 @@ class InteractiveShellIntegrationTest {
} }
@Test @Test
fun `shell should log in with valid crentials`() { fun `shell should log in with valid credentials`() {
val user = User("u", "p", setOf()) val user = User("u", "p", setOf())
driver { driver {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
@ -62,72 +73,65 @@ class InteractiveShellIntegrationTest {
@Test @Test
fun `shell should log in with ssl`() { fun `shell should log in with ssl`() {
val user = User("mark", "dadada", setOf(all())) val user = User("mark", "dadada", setOf(all()))
withCertificates { server, client, createSelfSigned, createSignedBy -> var successful = false
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works... val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
server.keyStore["cordaclienttls"] = rootCertificate val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
server.trustStore["cordaclienttls"] = rootCertificate val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
server.trustStore["shell"] = markCertificate
client.keyStore["shell"] = markCertificate val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
client.trustStore["cordaclienttls"] = rootCertificate val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
var successful = false startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) user = user.username, password = user.password,
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), hostAndPort = node.rpcAddress,
user = user.username, password = user.password, ssl = clientSslOptions)
hostAndPort = node.rpcAddress,
ssl = sslConfiguration)
InteractiveShell.startShell(conf) InteractiveShell.startShell(conf)
InteractiveShell.nodeInfo() InteractiveShell.nodeInfo()
successful = true successful = true
} }
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
}
@Test
fun `shell shoud not log in with invalid truststore`() {
val user = User("mark", "dadada", setOf("ALL"))
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
val (_, cert1) = createKeyPairAndSelfSignedCertificate()
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1)
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress,
ssl = clientSslOptions)
InteractiveShell.startShell(conf)
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java)
} }
} }
} }
@Test @Test
fun `shell shoud not log in without ssl keystore`() { fun `internal shell user should not be able to connect if node started with devMode=false`() {
val user = User("mark", "dadada", setOf("ALL")) driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
withCertificates { server, client, createSelfSigned, createSignedBy -> startNode().getOrThrow().use { node ->
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB")) val conf = (node as NodeHandleInternal).configuration.toShellConfig()
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate) InteractiveShell.startShellInternal(conf)
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
// truststore needs to contain root CA for how the driver works...
server.keyStore["cordaclienttls"] = rootCertificate
server.trustStore["cordaclienttls"] = rootCertificate
server.trustStore["shell"] = markCertificate
//client key store doesn't have "mark" certificate
client.trustStore["cordaclienttls"] = rootCertificate
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail)
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress,
ssl = sslConfiguration)
InteractiveShell.startShell(conf)
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java)
}
}
} }
} }
} }
@ -181,62 +185,54 @@ class InteractiveShellIntegrationTest {
val user = User("mark", "dadada", setOf(Permissions.startFlow<SSHServerTest.FlowICanRun>(), val user = User("mark", "dadada", setOf(Permissions.startFlow<SSHServerTest.FlowICanRun>(),
Permissions.invokeRpc(CordaRPCOps::registeredFlows), Permissions.invokeRpc(CordaRPCOps::registeredFlows),
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/)) Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/))
withCertificates { server, client, createSelfSigned, createSignedBy ->
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works... val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
server.keyStore["cordaclienttls"] = rootCertificate val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
server.trustStore["cordaclienttls"] = rootCertificate val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
server.trustStore["shell"] = markCertificate val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
client.keyStore["shell"] = markCertificate var successful = false
client.trustStore["cordaclienttls"] = rootCertificate driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
var successful = false user = user.username, password = user.password,
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { hostAndPort = node.rpcAddress,
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> ssl = clientSslOptions,
sshdPort = 2223)
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, InteractiveShell.startShell(conf)
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) InteractiveShell.nodeInfo()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress,
ssl = sslConfiguration,
sshdPort = 2223)
InteractiveShell.startShell(conf) val session = JSch().getSession("mark", "localhost", 2223)
InteractiveShell.nodeInfo() session.setConfig("StrictHostKeyChecking", "no")
session.setPassword("dadada")
session.connect()
val session = JSch().getSession("mark", "localhost", 2223) assertTrue(session.isConnected)
session.setConfig("StrictHostKeyChecking", "no")
session.setPassword("dadada")
session.connect()
assertTrue(session.isConnected) val channel = session.openChannel("exec") as ChannelExec
channel.setCommand("start FlowICanRun")
channel.connect(5000)
val channel = session.openChannel("exec") as ChannelExec assertTrue(channel.isConnected)
channel.setCommand("start FlowICanRun")
channel.connect(5000)
assertTrue(channel.isConnected) val response = String(Streams.readAll(channel.inputStream))
val response = String(Streams.readAll(channel.inputStream)) val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") } channel.disconnect()
session.disconnect() // TODO Simon make sure to close them
channel.disconnect() // There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
session.disconnect() // TODO Simon make sure to close them assertThat(linesWithDoneCount).hasSize(1)
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching. successful = true
assertThat(linesWithDoneCount).hasSize(1)
successful = true
}
}
assertThat(successful).isTrue()
} }
assertThat(successful).isTrue()
} }
} }
} }

View File

@ -9,6 +9,7 @@ import net.corda.client.jackson.StringToMethodCallParser
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.PermissionException import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader
import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
@ -80,7 +81,6 @@ object InteractiveShell {
* internals. * internals.
*/ */
fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
shellConfiguration = configuration
rpcOps = { username: String, credentials: String -> rpcOps = { username: String, credentials: String ->
val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort, val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort,
configuration = object : CordaRPCClientConfiguration { configuration = object : CordaRPCClientConfiguration {
@ -91,6 +91,29 @@ object InteractiveShell {
this.connection = client.start(username, credentials) this.connection = client.start(username, credentials)
connection.proxy connection.proxy
} }
_startShell(configuration, classLoader)
}
/**
* Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
* internals.
*/
fun startShellInternal(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
rpcOps = { username: String, credentials: String ->
val client = createCordaRPCClientWithInternalSslAndClassLoader(hostAndPort = configuration.hostAndPort,
configuration = object : CordaRPCClientConfiguration {
override val maxReconnectAttempts = 1
},
sslConfiguration = configuration.nodeSslConfig,
classLoader = classLoader)
this.connection = client.start(username, credentials)
connection.proxy
}
_startShell(configuration, classLoader)
}
private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
shellConfiguration = configuration
InteractiveShell.classLoader = classLoader InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshdPort != null val runSshDaemon = configuration.sshdPort != null

View File

@ -1,9 +1,9 @@
package net.corda.tools.shell package net.corda.tools.shell
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
data class ShellConfiguration( data class ShellConfiguration(
val commandsDirectory: Path, val commandsDirectory: Path,
@ -11,7 +11,8 @@ data class ShellConfiguration(
var user: String = "", var user: String = "",
var password: String = "", var password: String = "",
val hostAndPort: NetworkHostAndPort, val hostAndPort: NetworkHostAndPort,
val ssl: ShellSslOptions? = null, val ssl: ClientRpcSslOptions? = null,
val nodeSslConfig: SSLConfiguration? = null,
val sshdPort: Int? = null, val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null, val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) { val noLocalShell: Boolean = false) {
@ -22,13 +23,3 @@ data class ShellConfiguration(
const val SSHD_HOSTKEY_DIR = "ssh" const val SSHD_HOSTKEY_DIR = "ssh"
} }
} }
//TODO: sslKeystore -> it's a path not the keystore itself.
//TODO: trustStoreFile -> it's a path not the file itself.
data class ShellSslOptions(override val sslKeystore: Path,
override val keyStorePassword: String,
override val trustStoreFile: Path,
override val trustStorePassword: String,
override val crlCheckSoftFail: Boolean) : SSLConfiguration {
override val certificatesDirectory: Path get() = Paths.get("")
}

View File

@ -6,6 +6,7 @@ import joptsimple.OptionParser
import joptsimple.util.EnumConverter import joptsimple.util.EnumConverter
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import org.slf4j.event.Level import org.slf4j.event.Level
@ -52,15 +53,6 @@ class CommandLineOptionParser {
private val helpArg = optionParser private val helpArg = optionParser
.accepts("help") .accepts("help")
.forHelp() .forHelp()
private val keyStorePasswordArg = optionParser
.accepts("keystore-password", "The password to unlock the KeyStore file.")
.withOptionalArg()
private val keyStoreDirArg = optionParser
.accepts("keystore-file", "The path to the KeyStore file.")
.withOptionalArg()
private val keyStoreTypeArg = optionParser
.accepts("keystore-type", "The type of the KeyStore (e.g. JKS).")
.withOptionalArg()
private val trustStorePasswordArg = optionParser private val trustStorePasswordArg = optionParser
.accepts("truststore-password", "The password to unlock the TrustStore file.") .accepts("truststore-password", "The password to unlock the TrustStore file.")
.withOptionalArg() .withOptionalArg()
@ -85,11 +77,8 @@ class CommandLineOptionParser {
loggingLevel = optionSet.valueOf(loggerLevel), loggingLevel = optionSet.valueOf(loggerLevel),
sshdPort = optionSet.valueOf(sshdPortArg), sshdPort = optionSet.valueOf(sshdPortArg),
sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
keyStorePassword = optionSet.valueOf(keyStorePasswordArg),
trustStorePassword = optionSet.valueOf(trustStorePasswordArg), trustStorePassword = optionSet.valueOf(trustStorePasswordArg),
keyStoreFile = (optionSet.valueOf(keyStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
keyStoreType = optionSet.valueOf(keyStoreTypeArg),
trustStoreType = optionSet.valueOf(trustStoreTypeArg)) trustStoreType = optionSet.valueOf(trustStoreTypeArg))
} }
@ -107,11 +96,8 @@ data class CommandLineOptions(val configFile: String?,
val loggingLevel: Level, val loggingLevel: Level,
val sshdPort: String?, val sshdPort: String?,
val sshdHostKeyDirectory: Path?, val sshdHostKeyDirectory: Path?,
val keyStorePassword: String?,
val trustStorePassword: String?, val trustStorePassword: String?,
val keyStoreFile: Path?,
val trustStoreFile: Path?, val trustStoreFile: Path?,
val keyStoreType: String?,
val trustStoreType: String?) { val trustStoreType: String?) {
private fun toConfigFile(): Config { private fun toConfigFile(): Config {
@ -123,9 +109,6 @@ data class CommandLineOptions(val configFile: String?,
password?.apply { cmdOpts["node.password"] = this } password?.apply { cmdOpts["node.password"] = this }
host?.apply { cmdOpts["node.addresses.rpc.host"] = this } host?.apply { cmdOpts["node.addresses.rpc.host"] = this }
port?.apply { cmdOpts["node.addresses.rpc.port"] = this } port?.apply { cmdOpts["node.addresses.rpc.port"] = this }
keyStoreFile?.apply { cmdOpts["ssl.keystore.path"] = this.toString() }
keyStorePassword?.apply { cmdOpts["ssl.keystore.password"] = this }
keyStoreType?.apply { cmdOpts["ssl.keystore.type"] = this }
trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() }
trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this }
trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this }
@ -187,12 +170,11 @@ private class ShellConfigurationFile {
data class KeyStore( data class KeyStore(
val path: String, val path: String,
val type: String, val type: String = "JKS",
val password: String val password: String
) )
data class Ssl( data class Ssl(
val keystore: KeyStore,
val truststore: KeyStore val truststore: KeyStore
) )
@ -205,12 +187,9 @@ private class ShellConfigurationFile {
val sslOptions = val sslOptions =
ssl?.let { ssl?.let {
ShellSslOptions( ClientRpcSslOptions(
sslKeystore = Paths.get(it.keystore.path), trustStorePath = Paths.get(it.truststore.path),
keyStorePassword = it.keystore.password, trustStorePassword = it.truststore.password)
trustStoreFile = Paths.get(it.truststore.path),
trustStorePassword = it.truststore.password,
crlCheckSoftFail = true)
} }
return ShellConfiguration( return ShellConfiguration(

View File

@ -2,6 +2,7 @@ package net.corda.tools.shell
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import org.slf4j.event.Level import org.slf4j.event.Level
@ -24,12 +25,9 @@ class StandaloneShellArgsParserTest {
"--sshd-port", "2223", "--sshd-port", "2223",
"--sshd-hostkey-directory", "/x/y/ssh", "--sshd-hostkey-directory", "/x/y/ssh",
"--help", "--help",
"--keystore-password", "pass1",
"--truststore-password", "pass2", "--truststore-password", "pass2",
"--keystore-file", "/x/y/keystore.jks",
"--truststore-file", "/x/y/truststore.jks", "--truststore-file", "/x/y/truststore.jks",
"--truststore-type", "dummy", "--truststore-type", "dummy")
"--keystore-type", "JKS")
val expectedOptions = CommandLineOptions( val expectedOptions = CommandLineOptions(
configFile = "/x/y/z/config.conf", configFile = "/x/y/z/config.conf",
@ -43,12 +41,9 @@ class StandaloneShellArgsParserTest {
loggingLevel = Level.DEBUG, loggingLevel = Level.DEBUG,
sshdPort = "2223", sshdPort = "2223",
sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(), sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(),
keyStorePassword = "pass1",
trustStorePassword = "pass2", trustStorePassword = "pass2",
keyStoreFile = Paths.get("/x/y/keystore.jks").normalize().toAbsolutePath(),
trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(), trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(),
trustStoreType = "dummy", trustStoreType = "dummy")
keyStoreType = "JKS")
val options = CommandLineOptionParser().parse(*args) val options = CommandLineOptionParser().parse(*args)
@ -70,12 +65,9 @@ class StandaloneShellArgsParserTest {
loggingLevel = Level.INFO, loggingLevel = Level.INFO,
sshdPort = null, sshdPort = null,
sshdHostKeyDirectory = null, sshdHostKeyDirectory = null,
keyStorePassword = null,
trustStorePassword = null, trustStorePassword = null,
keyStoreFile = null,
trustStoreFile = null, trustStoreFile = null,
trustStoreType = null, trustStoreType = null)
keyStoreType = null)
val options = CommandLineOptionParser().parse(*args) val options = CommandLineOptionParser().parse(*args)
@ -96,19 +88,14 @@ class StandaloneShellArgsParserTest {
loggingLevel = Level.DEBUG, loggingLevel = Level.DEBUG,
sshdPort = "2223", sshdPort = "2223",
sshdHostKeyDirectory = Paths.get("/x/y/ssh"), sshdHostKeyDirectory = Paths.get("/x/y/ssh"),
keyStorePassword = "pass1",
trustStorePassword = "pass2", trustStorePassword = "pass2",
keyStoreFile = Paths.get("/x/y/keystore.jks"),
trustStoreFile = Paths.get("/x/y/truststore.jks"), trustStoreFile = Paths.get("/x/y/truststore.jks"),
keyStoreType = "dummy",
trustStoreType = "dummy" trustStoreType = "dummy"
) )
val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"), val expectedSsl = ClientRpcSslOptions(
keyStorePassword = "pass1", trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStoreFile = Paths.get("/x/y/truststore.jks"), trustStorePassword = "pass2")
trustStorePassword = "pass2",
crlCheckSoftFail = true)
val expectedConfig = ShellConfiguration( val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"), commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"), cordappsDirectory = Paths.get("/x/y/cordapps"),
@ -139,25 +126,19 @@ class StandaloneShellArgsParserTest {
loggingLevel = Level.DEBUG, loggingLevel = Level.DEBUG,
sshdPort = null, sshdPort = null,
sshdHostKeyDirectory = null, sshdHostKeyDirectory = null,
keyStorePassword = null,
trustStorePassword = null, trustStorePassword = null,
keyStoreFile = null,
trustStoreFile = null, trustStoreFile = null,
keyStoreType = null,
trustStoreType = null) trustStoreType = null)
val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"),
keyStorePassword = "pass1",
trustStoreFile = Paths.get("/x/y/truststore.jks"),
trustStorePassword = "pass2",
crlCheckSoftFail = true)
val expectedConfig = ShellConfiguration( val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"), commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"), cordappsDirectory = Paths.get("/x/y/cordapps"),
user = "demo", user = "demo",
password = "abcd1234", password = "abcd1234",
hostAndPort = NetworkHostAndPort("alocalhost", 1234), hostAndPort = NetworkHostAndPort("alocalhost", 1234),
ssl = expectedSsl, ssl = ClientRpcSslOptions(
trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStorePassword = "pass2"),
sshdPort = 2223) sshdPort = 2223)
val config = options.toConfig() val config = options.toConfig()
@ -179,18 +160,13 @@ class StandaloneShellArgsParserTest {
loggingLevel = Level.DEBUG, loggingLevel = Level.DEBUG,
sshdPort = null, sshdPort = null,
sshdHostKeyDirectory = null, sshdHostKeyDirectory = null,
keyStorePassword = null,
trustStorePassword = null, trustStorePassword = null,
keyStoreFile = Paths.get("/x/y/cmd.jks"),
trustStoreFile = null, trustStoreFile = null,
keyStoreType = null,
trustStoreType = null) trustStoreType = null)
val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/cmd.jks"), val expectedSsl = ClientRpcSslOptions(
keyStorePassword = "pass1", trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStoreFile = Paths.get("/x/y/truststore.jks"), trustStorePassword = "pass2")
trustStorePassword = "pass2",
crlCheckSoftFail = true)
val expectedConfig = ShellConfiguration( val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"), commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"), cordappsDirectory = Paths.get("/x/y/cordapps"),

View File

@ -21,11 +21,6 @@ extensions {
} }
} }
ssl { ssl {
keystore {
path : "/x/y/keystore.jks"
type : "JKS"
password : "pass1"
}
truststore { truststore {
path : "/x/y/truststore.jks" path : "/x/y/truststore.jks"
type : "JKS" type : "JKS"