mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
fc88cefbc8
commit
455221629b
@ -2504,6 +2504,26 @@ public final class net.corda.core.identity.PartyAndCertificate extends java.lang
|
||||
@CordaSerializable
|
||||
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
|
||||
public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.messaging.RPCOps
|
||||
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 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, net.corda.client.rpc.CordaRPCClientConfiguration)
|
||||
@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 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 abstract int getCacheConcurrencyLevel()
|
||||
|
@ -48,7 +48,7 @@ EOF
|
||||
#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
|
||||
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
|
||||
$newInternalExposures
|
||||
|
@ -8,8 +8,8 @@ import net.corda.core.context.Trace
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.internal.effectiveSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
|
||||
import java.time.Duration
|
||||
@ -98,7 +98,7 @@ interface CordaRPCClientConfiguration {
|
||||
*
|
||||
* @param hostAndPort The network address to connect to.
|
||||
* @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.
|
||||
* 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.
|
||||
@ -106,9 +106,11 @@ interface CordaRPCClientConfiguration {
|
||||
class CordaRPCClient private constructor(
|
||||
private val hostAndPort: NetworkHostAndPort,
|
||||
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 haAddressPool: List<NetworkHostAndPort> = emptyList()
|
||||
private val haAddressPool: List<NetworkHostAndPort> = emptyList(),
|
||||
private val internalConnection: Boolean = false
|
||||
) {
|
||||
@JvmOverloads
|
||||
constructor(hostAndPort: NetworkHostAndPort,
|
||||
@ -122,24 +124,41 @@ class CordaRPCClient private constructor(
|
||||
* @param configuration An optional configuration used to tweak client behaviour.
|
||||
*/
|
||||
@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 {
|
||||
internal fun createWithSsl(
|
||||
fun createWithSsl(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
|
||||
sslConfiguration: SSLConfiguration? = null
|
||||
sslConfiguration: ClientRpcSslOptions,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()
|
||||
): CordaRPCClient {
|
||||
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(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
classLoader: ClassLoader? = null
|
||||
): 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,18 +175,23 @@ class CordaRPCClient private constructor(
|
||||
}
|
||||
|
||||
private fun getRpcClient(): RPCClient<CordaRPCOps> {
|
||||
return if (haAddressPool.isEmpty()) {
|
||||
RPCClient(
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration),
|
||||
return when {
|
||||
// Node->RPC broker, mutually authenticated SSL. This is used when connecting the integrated shell
|
||||
internalConnection == true -> RPCClient(hostAndPort, nodeSslConfiguration!!)
|
||||
|
||||
// Client->RPC broker
|
||||
haAddressPool.isEmpty() -> RPCClient(
|
||||
rpcConnectorTcpTransport(hostAndPort, config = sslConfiguration),
|
||||
configuration,
|
||||
if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
|
||||
} else {
|
||||
else -> {
|
||||
RPCClient(haAddressPool,
|
||||
sslConfiguration,
|
||||
configuration,
|
||||
if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable]
|
||||
|
@ -5,22 +5,24 @@ import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.pendingFlowsCount
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import rx.Observable
|
||||
|
||||
/** Utility which exposes the internal Corda RPC constructor to other internal Corda components */
|
||||
fun createCordaRPCClientWithSsl(
|
||||
fun createCordaRPCClientWithSslAndClassLoader(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
|
||||
sslConfiguration: SSLConfiguration? = null
|
||||
) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration)
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
classLoader: ClassLoader? = null
|
||||
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
|
||||
|
||||
fun createCordaRPCClientWithSslAndClassLoader(
|
||||
fun createCordaRPCClientWithInternalSslAndClassLoader(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
classLoader: ClassLoader? = null
|
||||
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
|
||||
) = CordaRPCClient.createWithInternalSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
|
||||
|
||||
fun CordaRPCOps.drainAndShutdown(): Observable<Unit> {
|
||||
|
||||
|
@ -13,10 +13,11 @@ import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransportsFromList
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
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>(
|
||||
val transport: TransportConfiguration,
|
||||
val rpcConfiguration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
|
||||
@ -66,18 +70,25 @@ class RPCClient<I : RPCOps>(
|
||||
) {
|
||||
constructor(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
|
||||
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(
|
||||
haAddressPool: List<NetworkHostAndPort>,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
|
||||
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
|
||||
) : this(tcpTransport(ConnectionDirection.Outbound(), haAddressPool.first(), sslConfiguration),
|
||||
configuration, serializationContext, tcpTransportsFromList(ConnectionDirection.Outbound(), haAddressPool, sslConfiguration))
|
||||
) : this(rpcConnectorTcpTransport(haAddressPool.first(), sslConfiguration),
|
||||
configuration, serializationContext, rpcConnectorTcpTransportsFromList(haAddressPool, sslConfiguration))
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
@ -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")
|
@ -49,7 +49,11 @@ Unreleased
|
||||
* 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`.
|
||||
|
||||
* 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
|
||||
|
||||
|
@ -307,9 +307,17 @@ The client RPC wire protocol is defined and documented in ``net/corda/client/rpc
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
----------------------------------------
|
||||
CorDapps must whitelist any classes used over RPC with Corda's serialization framework, unless they are whitelisted by
|
||||
|
@ -1,3 +0,0 @@
|
||||
myLegalName : "O=Bank A,L=London,C=GB"
|
||||
p2pAddress : "my-corda-node:10002"
|
||||
verifierType: "OutOfProcess"
|
@ -28,8 +28,7 @@ class ExampleConfigTest {
|
||||
@Test
|
||||
fun `example node_confs parses fine`() {
|
||||
readAndCheckConfigurations(
|
||||
"example-node.conf",
|
||||
"example-out-of-process-verifier-node.conf"
|
||||
"example-node.conf"
|
||||
) {
|
||||
val baseDirectory = Paths.get("some-example-base-dir")
|
||||
ConfigHelper.loadConfig(
|
||||
|
@ -79,7 +79,7 @@ Security
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
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.utilities.NetworkHostAndPort
|
||||
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.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
|
||||
|
||||
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()
|
||||
}
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Class to set Artemis TCP configuration options. */
|
||||
class ArtemisTcpTransport {
|
||||
companion object {
|
||||
const val VERIFY_PEER_LEGAL_NAME = "corda.verifyPeerCommonName"
|
||||
|
||||
/**
|
||||
* Corda supported TLS schemes.
|
||||
* <p><ul>
|
||||
@ -47,14 +38,7 @@ class ArtemisTcpTransport {
|
||||
/** Supported TLS versions, currently TLSv1.2 only. */
|
||||
val TLS_VERSIONS = listOf("TLSv1.2")
|
||||
|
||||
/** Specify [TransportConfiguration] for TCP communication. */
|
||||
fun tcpTransport(
|
||||
direction: ConnectionDirection,
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
config: SSLConfiguration?,
|
||||
enableSSL: Boolean = true
|
||||
): TransportConfiguration {
|
||||
val options = mutableMapOf<String, Any?>(
|
||||
private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf(
|
||||
// Basic TCP target details.
|
||||
TransportConstants.HOST_PROP_NAME to hostAndPort.host,
|
||||
TransportConstants.PORT_PROP_NAME to hostAndPort.port,
|
||||
@ -68,48 +52,100 @@ class ArtemisTcpTransport {
|
||||
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
|
||||
//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) {
|
||||
config.sslKeystore.requireOnDefaultFileSystem()
|
||||
config.trustStoreFile.requireOnDefaultFileSystem()
|
||||
val tlsOptions = mapOf(
|
||||
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
|
||||
// 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)
|
||||
options.putAll(defaultSSLOptions)
|
||||
options.putAll(config.toTransportOptions())
|
||||
}
|
||||
val factoryName = when (direction) {
|
||||
is ConnectionDirection.Inbound -> direction.acceptorFactoryClassName
|
||||
is ConnectionDirection.Outbound -> direction.connectorFactoryClassName
|
||||
return TransportConfiguration(acceptorFactoryClassName, options)
|
||||
}
|
||||
return TransportConfiguration(factoryName, options)
|
||||
|
||||
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(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]. **/
|
||||
fun tcpTransportsFromList(
|
||||
direction: ConnectionDirection,
|
||||
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))
|
||||
fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map {
|
||||
rpcConnectorTcpTransport(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)
|
||||
|
@ -4,8 +4,7 @@ import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_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.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
@ -35,7 +34,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration,
|
||||
check(started == null) { "start can't be called twice" }
|
||||
log.info("Connecting to message broker: $serverAddress")
|
||||
// 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 {
|
||||
// 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.
|
||||
@ -50,7 +49,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration,
|
||||
// using our TLS certificate.
|
||||
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
|
||||
// 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()
|
||||
// Create a general purpose producer.
|
||||
val producer = session.createProducer()
|
||||
|
@ -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
|
||||
// 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"
|
||||
// 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 PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
|
||||
const val P2P_PREFIX = "p2p.inbound."
|
||||
|
@ -9,7 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
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.PEER_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
|
||||
@ -112,7 +112,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val maxMessageSize
|
||||
if (connected) {
|
||||
log.info("Bridge Connected")
|
||||
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
|
||||
val consumer = session.createConsumer(queueName)
|
||||
this.consumer = consumer
|
||||
|
@ -175,7 +175,6 @@ class AMQPBridgeTest {
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(artemisAddress).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), MAX_MESSAGE_SIZE)
|
||||
|
@ -365,7 +365,6 @@ class ProtonWrapperTests {
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
|
@ -1,69 +1,144 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testing.common.internal.withCertificates
|
||||
import net.corda.testing.common.internal.withKeyStores
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
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.driver
|
||||
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.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.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
class RpcSslTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun rpc_client_using_ssl() {
|
||||
fun `RPC client using ssl is able to run a command`() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
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)
|
||||
var successfulLogin = false
|
||||
var failedLogin = false
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
|
||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
val node = startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow()
|
||||
val client = createCordaRPCClientWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions)
|
||||
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
|
||||
val client = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions)
|
||||
val connection = client.start(user.username, user.password)
|
||||
|
||||
connection.proxy.apply {
|
||||
val nodeInfo = nodeInfo()
|
||||
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
|
||||
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).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(successful).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_client_not_using_ssl() {
|
||||
fun `RPC client not using ssl can run commands`() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
var successful = false
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
val node = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val client = CordaRPCClient(node.rpcAddress)
|
||||
val connection = client.start(user.username, user.password)
|
||||
val connection = CordaRPCClient(node.rpcAddress).start(user.username, user.password)
|
||||
connection.proxy.apply {
|
||||
nodeInfo()
|
||||
successful = true
|
||||
}
|
||||
|
||||
connection.close()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.x500Name
|
||||
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.DEV_INTERMEDIATE_CA
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
@ -43,10 +43,10 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() {
|
||||
}
|
||||
|
||||
@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)
|
||||
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`() {
|
||||
val attacker = clientTo(alice.internals.configuration.rpcOptions.address!!, sslConfiguration = null)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start(NODE_USER, NODE_USER, enableSSL = false)
|
||||
attacker.start(NODE_P2P_USER, NODE_P2P_USER, enableSSL = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.testing.internal.configureTestSSL
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
@ -23,7 +22,7 @@ class SimpleMQClient(val target: NetworkHostAndPort,
|
||||
lateinit var producer: ClientProducer
|
||||
|
||||
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 {
|
||||
isBlockOnNonDurableSend = true
|
||||
threadPoolMaxSize = 1
|
||||
|
@ -313,10 +313,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
|
||||
open fun startShell() {
|
||||
if (configuration.shouldInitCrashShell()) {
|
||||
if (configuration.rpcOptions.address == null) {
|
||||
throw ConfigurationException("Cannot init CrashShell because node RPC address is not set (via 'rpcSettings' option).")
|
||||
}
|
||||
InteractiveShell.startShell(configuration.toShellConfig(), cordappLoader.appClassLoader)
|
||||
InteractiveShell.startShellInternal(configuration.toShellConfig(), cordappLoader.appClassLoader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package net.corda.node.internal
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
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.serialization.amqp.AMQPServerSerializationScheme
|
||||
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.SchemaService
|
||||
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.rpc.ArtemisRpcBroker
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
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.addShutdownHook
|
||||
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.serialization.internal.*
|
||||
import org.slf4j.Logger
|
||||
@ -164,10 +168,11 @@ open class Node(configuration: NodeConfiguration,
|
||||
networkParameters: NetworkParameters): MessagingService {
|
||||
// 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.
|
||||
val securityManagerConfig = configuration.security?.authService ?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
val securityManagerConfig = configuration.security?.authService
|
||||
?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
|
||||
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) {
|
||||
@ -175,7 +180,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
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) {
|
||||
BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress)
|
||||
} else {
|
||||
@ -186,7 +192,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
|
||||
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 admin connection address", it.admin.toString())
|
||||
}
|
||||
@ -212,18 +218,17 @@ open class Node(configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun startLocalRpcBroker(): BrokerAddresses? {
|
||||
with(configuration) {
|
||||
return rpcOptions.address?.let {
|
||||
require(rpcOptions.address != null) { "RPC address needs to be specified for local RPC broker." }
|
||||
return with(configuration) {
|
||||
rpcOptions.address.let {
|
||||
val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc"
|
||||
with(rpcOptions) {
|
||||
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 {
|
||||
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 up the MQ clients.
|
||||
rpcMessagingClient?.run {
|
||||
internalRpcMessagingClient?.run {
|
||||
runOnStop += this::close
|
||||
when (rpcOps) {
|
||||
// not sure what this RPCOps base interface is for
|
||||
is SecureCordaRPCOps -> start(RpcExceptionHandlingProxy(rpcOps), securityManager)
|
||||
else -> start(rpcOps, securityManager)
|
||||
is SecureCordaRPCOps -> init(RpcExceptionHandlingProxy(rpcOps), securityManager)
|
||||
else -> init(rpcOps, securityManager)
|
||||
}
|
||||
}
|
||||
verifierMessagingClient?.run {
|
||||
@ -351,10 +355,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
// Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia:
|
||||
//
|
||||
// https://jolokia.org/agent/jvm.html
|
||||
JmxReporter.
|
||||
forRegistry(started.services.monitoringService.metrics).
|
||||
inDomain("net.corda").
|
||||
createsObjectNamesWith { _, domain, name ->
|
||||
JmxReporter.forRegistry(started.services.monitoringService.metrics).inDomain("net.corda").createsObjectNamesWith { _, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
@ -362,9 +363,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
}.build().start()
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
private var rpcMessagingClient: RPCMessagingClient? = null
|
||||
private var internalRpcMessagingClient: InternalRPCMessagingClient? = null
|
||||
private var verifierMessagingClient: VerifierMessagingClient? = null
|
||||
/** Starts a blocking event loop for message dispatch. */
|
||||
fun run() {
|
||||
rpcMessagingClient?.start2(rpcBroker!!.serverControl)
|
||||
internalRpcMessagingClient?.start(rpcBroker!!.serverControl)
|
||||
verifierMessagingClient?.start2()
|
||||
(network as P2PMessagingClient).run()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -10,6 +10,10 @@ import net.corda.core.utilities.seconds
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
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.tools.shell.SSHDConfiguration
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
@ -19,7 +23,6 @@ import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
|
||||
val Int.MB: Long get() = this * 1024L * 1024L
|
||||
|
||||
interface NodeConfiguration : NodeSSLConfiguration {
|
||||
@ -148,7 +151,8 @@ data class NodeConfigurationImpl(
|
||||
override val messagingServerAddress: NetworkHostAndPort?,
|
||||
override val messagingServerExternal: Boolean = (messagingServerAddress != null),
|
||||
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 noLocalShell: Boolean = false,
|
||||
override val devModeOptions: DevModeOptions? = null,
|
||||
@ -171,9 +175,9 @@ data class 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 {
|
||||
explicitAddress != null -> {
|
||||
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()) {
|
||||
"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(
|
||||
val address: NetworkHostAndPort?,
|
||||
val adminAddress: NetworkHostAndPort?,
|
||||
val address: NetworkHostAndPort,
|
||||
val adminAddress: NetworkHostAndPort,
|
||||
val standAloneBroker: 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 {
|
||||
override val address = this@NodeRpcSettings.address
|
||||
override val adminAddress = this@NodeRpcSettings.adminAddress
|
||||
@ -281,6 +290,7 @@ enum class CertChainPolicyType {
|
||||
UsernameMustMatch
|
||||
}
|
||||
|
||||
@Deprecated("Do not use")
|
||||
data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set<String>) {
|
||||
val certificateChainCheckPolicy: CertificateChainCheckPolicy
|
||||
get() {
|
||||
|
@ -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()
|
@ -1,12 +1,12 @@
|
||||
package net.corda.node.services.config.rpc
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
|
||||
interface NodeRpcOptions {
|
||||
val address: NetworkHostAndPort?
|
||||
val adminAddress: NetworkHostAndPort?
|
||||
val address: NetworkHostAndPort
|
||||
val adminAddress: NetworkHostAndPort
|
||||
val standAloneBroker: Boolean
|
||||
val useSsl: Boolean
|
||||
val sslConfig: SSLConfiguration
|
||||
val sslConfig: BrokerRpcSslOptions
|
||||
}
|
@ -1,42 +1,21 @@
|
||||
package net.corda.node.services.config.shell
|
||||
|
||||
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.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.Companion.COMMANDS_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.SSH_PORT
|
||||
import net.corda.tools.shell.ShellSslOptions
|
||||
|
||||
//re-packs data to Shell specific classes
|
||||
fun NodeConfiguration.toShellConfig(): ShellConfiguration {
|
||||
|
||||
val sslConfiguration = if (this.rpcOptions.useSsl) {
|
||||
with(this.rpcOptions.sslConfig) {
|
||||
ShellSslOptions(sslKeystore,
|
||||
keyStorePassword,
|
||||
trustStoreFile,
|
||||
trustStorePassword,
|
||||
crlCheckSoftFail)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val localShellUser: User = localShellUser()
|
||||
return ShellConfiguration(
|
||||
fun NodeConfiguration.toShellConfig() = 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,
|
||||
user = INTERNAL_SHELL_USER,
|
||||
password = INTERNAL_SHELL_USER,
|
||||
hostAndPort = this.rpcOptions.adminAddress,
|
||||
nodeSslConfig = this,
|
||||
sshdPort = this.sshd?.port,
|
||||
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
|
||||
noLocalShell = this.noLocalShell)
|
||||
}
|
||||
|
||||
fun localShellUser() = User("shell", "shell", setOf(Permissions.all()))
|
||||
|
@ -2,59 +2,36 @@ package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.core.internal.ThreadBox
|
||||
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.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.node.internal.artemis.SecureArtemisConfiguration
|
||||
import net.corda.node.internal.artemis.*
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_P2P_ROLE
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||
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.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor
|
||||
import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor
|
||||
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.NODE_USER
|
||||
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.PEER_USER
|
||||
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
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.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.server.ActiveMQServer
|
||||
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.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.KeyStoreException
|
||||
import java.security.Principal
|
||||
import java.util.*
|
||||
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.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: 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()
|
||||
journalDirectory = (artemisDir / "journal").toString()
|
||||
largeMessagesDirectory = (artemisDir / "large-messages").toString()
|
||||
val connectionDirection = ConnectionDirection.Inbound(
|
||||
acceptorFactoryClassName = NettyAcceptorFactory::class.java.name
|
||||
)
|
||||
val acceptors = mutableSetOf(createTcpTransport(connectionDirection, messagingServerAddress.host, messagingServerAddress.port))
|
||||
acceptorConfigurations = acceptors
|
||||
acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config))
|
||||
// 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.
|
||||
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.
|
||||
*/
|
||||
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["$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
|
||||
}
|
||||
|
||||
@ -184,158 +155,16 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
val keyStore = config.loadSslKeyStore().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() {
|
||||
// Override to make it work with our login module
|
||||
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 ActiveMQJAASSecurityManager(NodeLoginModule::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
|
||||
return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -29,8 +29,7 @@ import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.statemachine.DeduplicationId
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.PersistentMap
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
|
||||
@ -200,7 +199,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
started = true
|
||||
log.info("Connecting to message broker: $serverAddress")
|
||||
// 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 {
|
||||
// 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.
|
||||
@ -214,7 +213,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
|
||||
// using our TLS certificate.
|
||||
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
|
||||
// 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()
|
||||
bridgeSession = createNewSession()
|
||||
|
@ -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()
|
||||
}
|
@ -1,47 +1,44 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.artemis.*
|
||||
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.services.config.CertChainPolicyConfig
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
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.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import rx.Observable
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStoreException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.security.auth.login.AppConfigurationEntry
|
||||
|
||||
internal class ArtemisRpcBroker internal constructor(
|
||||
address: NetworkHostAndPort,
|
||||
private val adminAddressOptional: NetworkHostAndPort?,
|
||||
private val sslOptions: SSLConfiguration,
|
||||
private val sslOptions: BrokerRpcSslOptions?,
|
||||
private val useSsl: Boolean,
|
||||
private val securityManager: RPCSecurityManager,
|
||||
private val certificateChainCheckPolicies: List<CertChainPolicyConfig>,
|
||||
private val maxMessageSize: Int,
|
||||
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 {
|
||||
private val logger = loggerFor<ArtemisRpcBroker>()
|
||||
|
||||
fun withSsl(address: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker {
|
||||
return ArtemisRpcBroker(address, null, sslOptions, true, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
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 {
|
||||
return ArtemisRpcBroker(address, adminAddress, sslOptions, false, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
|
||||
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 fun initialiseServer(): ActiveMQServer {
|
||||
val serverConfiguration = RpcBrokerConfiguration(baseDirectory, maxMessageSize, jmxEnabled, addresses.primary, adminAddressOptional, sslOptions, useSsl)
|
||||
val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener, sslOptions)
|
||||
val serverConfiguration = RpcBrokerConfiguration(baseDirectory, maxMessageSize, jmxEnabled, addresses.primary, adminAddressOptional, sslOptions, useSsl, nodeConfiguration, shouldStartLocalShell)
|
||||
val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener)
|
||||
|
||||
return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply {
|
||||
registerActivationFailureListener { exception -> throw exception }
|
||||
@ -76,33 +73,21 @@ internal class ArtemisRpcBroker internal constructor(
|
||||
}
|
||||
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
private fun createArtemisSecurityManager(loginListener: LoginListener, sslOptions: SSLConfiguration): ActiveMQJAASSecurityManager {
|
||||
val keyStore = loadKeyStore(sslOptions.sslKeystore, sslOptions.keyStorePassword)
|
||||
val trustStore = loadKeyStore(sslOptions.trustStoreFile, sslOptions.trustStorePassword)
|
||||
|
||||
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)
|
||||
}
|
||||
private fun createArtemisSecurityManager(loginListener: LoginListener): ActiveMQJAASSecurityManager {
|
||||
val keyStore = nodeConfiguration.loadSslKeyStore().internal
|
||||
val trustStore = nodeConfiguration.loadTrustStore().internal
|
||||
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
NodeLoginModule.LOGIN_LISTENER_ARG to loginListener,
|
||||
NodeLoginModule.SECURITY_MANAGER_ARG to securityManager,
|
||||
NodeLoginModule.USE_SSL_ARG to useSsl,
|
||||
NodeLoginModule.CERT_CHAIN_CHECKS_ARG to certChecks)
|
||||
RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl),
|
||||
NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
|
||||
)
|
||||
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
|
||||
|
||||
private fun <RESULT> CompletableFuture<RESULT>.toObservable() = Observable.from(this)
|
@ -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
|
||||
}
|
||||
}
|
@ -8,10 +8,10 @@ import org.apache.activemq.artemis.core.settings.HierarchicalRepository
|
||||
* Helper class to dynamically assign security roles to RPC users
|
||||
* on their authentication. This object is plugged into the server
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
|
||||
fun onLogin(username: String) {
|
||||
@ -24,6 +24,8 @@ internal class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>
|
||||
|
||||
override fun setSecurityRepository(repository: RolesRepository) {
|
||||
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
|
||||
|
@ -2,30 +2,32 @@ package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.artemis.BrokerJaasLoginModule
|
||||
import net.corda.node.internal.artemis.SecureArtemisConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
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.remoting.impl.netty.NettyAcceptorFactory
|
||||
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.AddressSettings
|
||||
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
|
||||
|
||||
init {
|
||||
setDirectories(baseDirectory)
|
||||
|
||||
val acceptorConfigurationsSet = mutableSetOf(acceptorConfiguration(address, useSsl, sslOptions))
|
||||
val acceptorConfigurationsSet = mutableSetOf(
|
||||
rpcAcceptorTcpTransport(address, sslOptions, useSsl)
|
||||
)
|
||||
adminAddress?.let {
|
||||
acceptorConfigurationsSet += acceptorConfiguration(adminAddress, true, sslOptions)
|
||||
acceptorConfigurationsSet += rpcInternalAcceptorTcpTransport(it, nodeConfiguration)
|
||||
}
|
||||
acceptorConfigurations = acceptorConfigurationsSet
|
||||
|
||||
@ -41,9 +43,10 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
|
||||
|
||||
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",
|
||||
consume = true,
|
||||
@ -63,7 +66,7 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
|
||||
|
||||
private fun configureAddressSecurity(nodeInternalRole: Role, rolesAdderOnLogin: RolesAdderOnLogin) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -118,11 +121,6 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
|
||||
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,
|
||||
deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false,
|
||||
deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role {
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
@ -151,7 +152,7 @@ class NodeConfigurationImplTest {
|
||||
adminAddress = NetworkHostAndPort("localhost", 2),
|
||||
standAloneBroker = false,
|
||||
useSsl = false,
|
||||
ssl = SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, true))
|
||||
ssl = null)
|
||||
return NodeConfigurationImpl(
|
||||
baseDirectory = baseDirectory,
|
||||
myLegalName = ALICE_NAME,
|
||||
@ -165,7 +166,6 @@ class NodeConfigurationImplTest {
|
||||
messagingServerAddress = null,
|
||||
p2pMessagingRetry = P2PMessagingRetryConfiguration(5.seconds, 3, 1.0),
|
||||
notary = null,
|
||||
certificateChainCheckPolicies = emptyList(),
|
||||
devMode = true,
|
||||
noLocalShell = false,
|
||||
rpcSettings = rpcSettings,
|
||||
|
@ -3,23 +3,26 @@ package net.corda.node.services.rpc
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.node.services.config.CertChainPolicyConfig
|
||||
import net.corda.node.services.messaging.RPCMessagingClient
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.node.services.messaging.InternalRPCMessagingClient
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
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.driver.PortAllocation
|
||||
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.ActiveMQNotConnectedException
|
||||
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.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ArtemisRpcTests {
|
||||
private val ports: PortAllocation = RandomFree
|
||||
@ -42,168 +44,86 @@ class ArtemisRpcTests {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun rpc_with_ssl_enabled() {
|
||||
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
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions)
|
||||
}
|
||||
}
|
||||
val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
|
||||
val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert)
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_ssl_disabled() {
|
||||
withCertificates { server, client, createSelfSigned, _ ->
|
||||
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))
|
||||
}
|
||||
}
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), null, false, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_no_ssl_on_client_side_and_ssl_on_server_side() {
|
||||
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
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, _ ->
|
||||
val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
|
||||
val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
// here client sslOptions are passed null (as in, do not use SSL)
|
||||
testSslCommunication(brokerSslOptions, true, null, clientConnectionSpy = expectExceptionOfType(ActiveMQConnectionTimedOutException::class))
|
||||
}
|
||||
}
|
||||
assertThatThrownBy {
|
||||
testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, null)
|
||||
}.isInstanceOf(ActiveMQConnectionTimedOutException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_client_certificate_untrusted_to_server() {
|
||||
withCertificates { server, client, createSelfSigned, _ ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
// 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"))
|
||||
val (rpcKeyPair, selfSignCert) = createKeyPairAndSelfSignedCertificate()
|
||||
val keyStorePath = saveToKeyStore(tempFile("rpcKeystore.jks"), rpcKeyPair, selfSignCert)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
// here the client certificate is not trusted by the server
|
||||
// server.trustStore["mark"] = markCertificate
|
||||
// create another keypair and certificate and add that to the client truststore
|
||||
// the ssl connection should not
|
||||
val (_, selfSignCert1) = createKeyPairAndSelfSignedCertificate()
|
||||
val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert1)
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
|
||||
}
|
||||
}
|
||||
assertThatThrownBy {
|
||||
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(),
|
||||
adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = Files.createTempDirectory(null), clientConnectionSpy: (() -> Unit) -> Unit = {}) {
|
||||
private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(),
|
||||
adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath()) {
|
||||
val maxMessageSize = 10000
|
||||
val jmxEnabled = false
|
||||
val certificateChainCheckPolicies: List<CertChainPolicyConfig> = listOf()
|
||||
|
||||
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 {
|
||||
ArtemisRpcBroker.withoutSsl(address, adminAddress, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
ArtemisRpcBroker.withoutSsl(nodeSSlconfig, address, adminAddress, securityManager, maxMessageSize, jmxEnabled, baseDirectory, false)
|
||||
}
|
||||
artemisBroker.use { broker ->
|
||||
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)
|
||||
|
||||
val client = RPCClient<TestRpcOps>(tcpTransport(ConnectionDirection.Outbound(), broker.addresses.primary, clientSslOptions))
|
||||
val client = RPCClient<TestRpcOps>(rpcConnectorTcpTransport(broker.addresses.primary, clientSslOptions))
|
||||
|
||||
clientConnectionSpy {
|
||||
client.start(TestRpcOps::class.java, user.username, user.password).use { connection ->
|
||||
connection.proxy.apply {
|
||||
val greeting = greet("Frodo")
|
||||
val greeting = client.start(TestRpcOps::class.java, user.username, user.password).use { connection ->
|
||||
connection.proxy.greet("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 {
|
||||
start(ops, securityManager)
|
||||
start2(brokerControl)
|
||||
init(ops, securityManager)
|
||||
start(brokerControl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <EXCEPTION : Exception> expectExceptionOfType(exceptionType: KClass<EXCEPTION>): (() -> Unit) -> Unit {
|
||||
return { action -> assertThatThrownBy { action.invoke() }.isInstanceOf(exceptionType.java) }
|
||||
}
|
||||
|
||||
interface TestRpcOps : RPCOps {
|
||||
fun greet(name: String): String
|
||||
}
|
||||
@ -213,4 +133,7 @@ class ArtemisRpcTests {
|
||||
|
||||
override val protocolVersion: Int = 1
|
||||
}
|
||||
|
||||
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
|
||||
|
||||
}
|
@ -50,6 +50,8 @@ interface NodeHandle : AutoCloseable {
|
||||
val p2pAddress: NetworkHostAndPort
|
||||
/** Get the rpc address for this node **/
|
||||
val rpcAddress: NetworkHostAndPort
|
||||
/** Get the rpc admin address for this node **/
|
||||
val rpcAdminAddress: NetworkHostAndPort
|
||||
/** Get a [List] of [User]'s for this node **/
|
||||
val rpcUsers: List<User>
|
||||
/** The location of the node's base directory **/
|
||||
|
@ -24,7 +24,8 @@ interface NodeHandleInternal : NodeHandle {
|
||||
val useHTTPS: Boolean
|
||||
val webAddress: NetworkHostAndPort
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,7 @@ import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader
|
||||
import net.corda.cordform.CordformContext
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
@ -149,12 +148,8 @@ class DriverDSLImpl(
|
||||
}
|
||||
|
||||
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
|
||||
val rpcAddress = config.corda.rpcOptions.address!!
|
||||
val client = if (config.corda.rpcOptions.useSsl) {
|
||||
createCordaRPCClientWithSsl(rpcAddress, sslConfiguration = config.corda.rpcOptions.sslConfig)
|
||||
} else {
|
||||
CordaRPCClient(rpcAddress)
|
||||
}
|
||||
val rpcAddress = config.corda.rpcOptions.address
|
||||
val client = createCordaRPCClientWithInternalSslAndClassLoader(config.corda.rpcOptions.adminAddress, sslConfiguration = config.corda)
|
||||
val connectionFuture = poll(executorService, "RPC connection") {
|
||||
try {
|
||||
config.corda.rpcUsers[0].run { client.start(username, password) }
|
||||
@ -238,9 +233,13 @@ class DriverDSLImpl(
|
||||
baseDirectory = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOf(
|
||||
"p2pAddress" to "localhost:1222", // required argument, not really used
|
||||
"p2pAddress" to portAllocation.nextHostAndPort().toString(),
|
||||
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
|
||||
"myLegalName" to providedName.toString(),
|
||||
"rpcSettings" to mapOf(
|
||||
"address" to portAllocation.nextHostAndPort().toString(),
|
||||
"adminAddress" to portAllocation.nextHostAndPort().toString()
|
||||
),
|
||||
"devMode" to false)
|
||||
))
|
||||
|
||||
@ -341,18 +340,14 @@ class DriverDSLImpl(
|
||||
val rpcAddress = if (cordform.rpcAddress == null) {
|
||||
val overrides = mutableMapOf<String, Any>("rpcSettings.address" to portAllocation.nextHostAndPort().toString())
|
||||
cordform.config.apply {
|
||||
if (!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) {
|
||||
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
|
||||
}
|
||||
}
|
||||
overrides
|
||||
} else {
|
||||
val overrides = mutableMapOf<String, Any>()
|
||||
cordform.config.apply {
|
||||
if ((!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) && !hasPath("rpcSettings.adminAddress")) {
|
||||
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
|
||||
}
|
||||
}
|
||||
overrides
|
||||
}
|
||||
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
|
||||
@ -853,7 +848,7 @@ class DriverDSLImpl(
|
||||
var config = ConfigFactory.empty()
|
||||
config += "webAddress" to webAddress.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 += "useHTTPS" to useHTTPS
|
||||
config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString()
|
||||
|
@ -467,7 +467,6 @@ private fun mockNodeConfiguration(): NodeConfiguration {
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(true).whenever(it).devMode
|
||||
doReturn(null).whenever(it).compatibilityZoneURL
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(VerifierType.InMemory).whenever(it).verifierType
|
||||
doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).p2pMessagingRetry
|
||||
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec
|
||||
|
@ -21,7 +21,6 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.services.messaging.RPCServer
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
|
||||
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.remoting.impl.invm.InVMAcceptorFactory
|
||||
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.Role
|
||||
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 {
|
||||
val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name)
|
||||
return ConfigurationImpl().apply {
|
||||
val artemisDir = "$baseDirectory/artemis"
|
||||
bindingsDirectory = "$artemisDir/bindings"
|
||||
journalDirectory = "$artemisDir/journal"
|
||||
largeMessagesDirectory = "$artemisDir/large-messages"
|
||||
acceptorConfigurations = setOf(ArtemisTcpTransport.tcpTransport(connectionDirection, hostAndPort, null))
|
||||
acceptorConfigurations = setOf(ArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null))
|
||||
configureCommonSettings(maxFileSize, maxBufferedBytesPerClient)
|
||||
}
|
||||
}
|
||||
|
||||
val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name)
|
||||
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
|
||||
): CordaFuture<I> {
|
||||
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)
|
||||
driverDSL.shutdownManager.registerShutdown {
|
||||
connection.close()
|
||||
|
@ -10,14 +10,16 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.createDevKeyStores
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.serialization.internal.amqp.AMQP_ENABLED
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
@Suppress("unused")
|
||||
@ -92,13 +94,11 @@ fun createDevNodeCaCertPath(
|
||||
return Triple(rootCa, intermediateCa, nodeCa)
|
||||
}
|
||||
|
||||
fun SSLConfiguration.useSslRpcOverrides(): Map<String, Any> {
|
||||
fun BrokerRpcSslOptions.useSslRpcOverrides(): Map<String, String> {
|
||||
return mapOf(
|
||||
"rpcSettings.useSsl" to "true",
|
||||
"rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(),
|
||||
"rpcSettings.ssl.keyStorePassword" to keyStorePassword,
|
||||
"rpcSettings.ssl.trustStorePassword" to trustStorePassword,
|
||||
"rpcSettings.ssl.crlCheckSoftFail" to true
|
||||
"rpcSettings.ssl.keyStorePath" to keyStorePath.toAbsolutePath().toString(),
|
||||
"rpcSettings.ssl.keyStorePassword" to keyStorePassword
|
||||
)
|
||||
}
|
||||
|
||||
@ -125,3 +125,40 @@ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe
|
||||
* TODO: Should be removed after multiple identities are introduced.
|
||||
*/
|
||||
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
|
||||
}
|
@ -3,17 +3,22 @@ package net.corda.tools.shell
|
||||
import com.google.common.io.Files
|
||||
import com.jcraft.jsch.ChannelExec
|
||||
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.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testing.common.internal.withCertificates
|
||||
import net.corda.testing.common.internal.withKeyStores
|
||||
import net.corda.node.services.config.shell.toShellConfig
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
||||
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.node.User
|
||||
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.bouncycastle.util.io.Streams
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InteractiveShellIntegrationTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun `shell should not log in with invalid credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
@ -44,7 +55,7 @@ class InteractiveShellIntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should log in with valid crentials`() {
|
||||
fun `shell should log in with valid credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
driver {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
@ -62,29 +73,22 @@ class InteractiveShellIntegrationTest {
|
||||
@Test
|
||||
fun `shell should log in with ssl`() {
|
||||
val user = User("mark", "dadada", setOf(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...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
|
||||
client.keyStore["shell"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
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 (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)) {
|
||||
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 = sslConfiguration)
|
||||
ssl = clientSslOptions)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
@ -94,34 +98,25 @@ class InteractiveShellIntegrationTest {
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell shoud not log in without ssl keystore`() {
|
||||
fun `shell shoud not log in with invalid truststore`() {
|
||||
val user = User("mark", "dadada", setOf("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)
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
|
||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
val (_, cert1) = createKeyPairAndSelfSignedCertificate()
|
||||
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1)
|
||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
||||
|
||||
//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 ->
|
||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.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)
|
||||
ssl = clientSslOptions)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
@ -129,6 +124,15 @@ class InteractiveShellIntegrationTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `internal shell user should not be able to connect if node started with devMode=false`() {
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
startNode().getOrThrow().use { node ->
|
||||
val conf = (node as NodeHandleInternal).configuration.toShellConfig()
|
||||
InteractiveShell.startShellInternal(conf)
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,29 +185,21 @@ class InteractiveShellIntegrationTest {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<SSHServerTest.FlowICanRun>(),
|
||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
||||
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...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
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")
|
||||
|
||||
client.keyStore["shell"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.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,
|
||||
ssl = clientSslOptions,
|
||||
sshdPort = 2223)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
@ -234,9 +230,9 @@ class InteractiveShellIntegrationTest {
|
||||
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import net.corda.client.jackson.StringToMethodCallParser
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.client.rpc.PermissionException
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
@ -80,7 +81,6 @@ object InteractiveShell {
|
||||
* internals.
|
||||
*/
|
||||
fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
|
||||
shellConfiguration = configuration
|
||||
rpcOps = { username: String, credentials: String ->
|
||||
val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort,
|
||||
configuration = object : CordaRPCClientConfiguration {
|
||||
@ -91,6 +91,29 @@ object InteractiveShell {
|
||||
this.connection = client.start(username, credentials)
|
||||
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
|
||||
val runSshDaemon = configuration.sshdPort != null
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.tools.shell
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
data class ShellConfiguration(
|
||||
val commandsDirectory: Path,
|
||||
@ -11,7 +11,8 @@ data class ShellConfiguration(
|
||||
var user: String = "",
|
||||
var password: String = "",
|
||||
val hostAndPort: NetworkHostAndPort,
|
||||
val ssl: ShellSslOptions? = null,
|
||||
val ssl: ClientRpcSslOptions? = null,
|
||||
val nodeSslConfig: SSLConfiguration? = null,
|
||||
val sshdPort: Int? = null,
|
||||
val sshHostKeyDirectory: Path? = null,
|
||||
val noLocalShell: Boolean = false) {
|
||||
@ -22,13 +23,3 @@ data class ShellConfiguration(
|
||||
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("")
|
||||
}
|
@ -6,6 +6,7 @@ import joptsimple.OptionParser
|
||||
import joptsimple.util.EnumConverter
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
|
||||
import org.slf4j.event.Level
|
||||
@ -52,15 +53,6 @@ class CommandLineOptionParser {
|
||||
private val helpArg = optionParser
|
||||
.accepts("help")
|
||||
.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
|
||||
.accepts("truststore-password", "The password to unlock the TrustStore file.")
|
||||
.withOptionalArg()
|
||||
@ -85,11 +77,8 @@ class CommandLineOptionParser {
|
||||
loggingLevel = optionSet.valueOf(loggerLevel),
|
||||
sshdPort = optionSet.valueOf(sshdPortArg),
|
||||
sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
|
||||
keyStorePassword = optionSet.valueOf(keyStorePasswordArg),
|
||||
trustStorePassword = optionSet.valueOf(trustStorePasswordArg),
|
||||
keyStoreFile = (optionSet.valueOf(keyStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
|
||||
trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
|
||||
keyStoreType = optionSet.valueOf(keyStoreTypeArg),
|
||||
trustStoreType = optionSet.valueOf(trustStoreTypeArg))
|
||||
}
|
||||
|
||||
@ -107,11 +96,8 @@ data class CommandLineOptions(val configFile: String?,
|
||||
val loggingLevel: Level,
|
||||
val sshdPort: String?,
|
||||
val sshdHostKeyDirectory: Path?,
|
||||
val keyStorePassword: String?,
|
||||
val trustStorePassword: String?,
|
||||
val keyStoreFile: Path?,
|
||||
val trustStoreFile: Path?,
|
||||
val keyStoreType: String?,
|
||||
val trustStoreType: String?) {
|
||||
|
||||
private fun toConfigFile(): Config {
|
||||
@ -123,9 +109,6 @@ data class CommandLineOptions(val configFile: String?,
|
||||
password?.apply { cmdOpts["node.password"] = this }
|
||||
host?.apply { cmdOpts["node.addresses.rpc.host"] = 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() }
|
||||
trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this }
|
||||
trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this }
|
||||
@ -187,12 +170,11 @@ private class ShellConfigurationFile {
|
||||
|
||||
data class KeyStore(
|
||||
val path: String,
|
||||
val type: String,
|
||||
val type: String = "JKS",
|
||||
val password: String
|
||||
)
|
||||
|
||||
data class Ssl(
|
||||
val keystore: KeyStore,
|
||||
val truststore: KeyStore
|
||||
)
|
||||
|
||||
@ -205,12 +187,9 @@ private class ShellConfigurationFile {
|
||||
|
||||
val sslOptions =
|
||||
ssl?.let {
|
||||
ShellSslOptions(
|
||||
sslKeystore = Paths.get(it.keystore.path),
|
||||
keyStorePassword = it.keystore.password,
|
||||
trustStoreFile = Paths.get(it.truststore.path),
|
||||
trustStorePassword = it.truststore.password,
|
||||
crlCheckSoftFail = true)
|
||||
ClientRpcSslOptions(
|
||||
trustStorePath = Paths.get(it.truststore.path),
|
||||
trustStorePassword = it.truststore.password)
|
||||
}
|
||||
|
||||
return ShellConfiguration(
|
||||
|
@ -2,6 +2,7 @@ package net.corda.tools.shell
|
||||
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import org.slf4j.event.Level
|
||||
@ -24,12 +25,9 @@ class StandaloneShellArgsParserTest {
|
||||
"--sshd-port", "2223",
|
||||
"--sshd-hostkey-directory", "/x/y/ssh",
|
||||
"--help",
|
||||
"--keystore-password", "pass1",
|
||||
"--truststore-password", "pass2",
|
||||
"--keystore-file", "/x/y/keystore.jks",
|
||||
"--truststore-file", "/x/y/truststore.jks",
|
||||
"--truststore-type", "dummy",
|
||||
"--keystore-type", "JKS")
|
||||
"--truststore-type", "dummy")
|
||||
|
||||
val expectedOptions = CommandLineOptions(
|
||||
configFile = "/x/y/z/config.conf",
|
||||
@ -43,12 +41,9 @@ class StandaloneShellArgsParserTest {
|
||||
loggingLevel = Level.DEBUG,
|
||||
sshdPort = "2223",
|
||||
sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(),
|
||||
keyStorePassword = "pass1",
|
||||
trustStorePassword = "pass2",
|
||||
keyStoreFile = Paths.get("/x/y/keystore.jks").normalize().toAbsolutePath(),
|
||||
trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(),
|
||||
trustStoreType = "dummy",
|
||||
keyStoreType = "JKS")
|
||||
trustStoreType = "dummy")
|
||||
|
||||
val options = CommandLineOptionParser().parse(*args)
|
||||
|
||||
@ -70,12 +65,9 @@ class StandaloneShellArgsParserTest {
|
||||
loggingLevel = Level.INFO,
|
||||
sshdPort = null,
|
||||
sshdHostKeyDirectory = null,
|
||||
keyStorePassword = null,
|
||||
trustStorePassword = null,
|
||||
keyStoreFile = null,
|
||||
trustStoreFile = null,
|
||||
trustStoreType = null,
|
||||
keyStoreType = null)
|
||||
trustStoreType = null)
|
||||
|
||||
val options = CommandLineOptionParser().parse(*args)
|
||||
|
||||
@ -96,19 +88,14 @@ class StandaloneShellArgsParserTest {
|
||||
loggingLevel = Level.DEBUG,
|
||||
sshdPort = "2223",
|
||||
sshdHostKeyDirectory = Paths.get("/x/y/ssh"),
|
||||
keyStorePassword = "pass1",
|
||||
trustStorePassword = "pass2",
|
||||
keyStoreFile = Paths.get("/x/y/keystore.jks"),
|
||||
trustStoreFile = Paths.get("/x/y/truststore.jks"),
|
||||
keyStoreType = "dummy",
|
||||
trustStoreType = "dummy"
|
||||
)
|
||||
|
||||
val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"),
|
||||
keyStorePassword = "pass1",
|
||||
trustStoreFile = Paths.get("/x/y/truststore.jks"),
|
||||
trustStorePassword = "pass2",
|
||||
crlCheckSoftFail = true)
|
||||
val expectedSsl = ClientRpcSslOptions(
|
||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
||||
trustStorePassword = "pass2")
|
||||
val expectedConfig = ShellConfiguration(
|
||||
commandsDirectory = Paths.get("/x/y/commands"),
|
||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
||||
@ -139,25 +126,19 @@ class StandaloneShellArgsParserTest {
|
||||
loggingLevel = Level.DEBUG,
|
||||
sshdPort = null,
|
||||
sshdHostKeyDirectory = null,
|
||||
keyStorePassword = null,
|
||||
trustStorePassword = null,
|
||||
keyStoreFile = null,
|
||||
trustStoreFile = null,
|
||||
keyStoreType = 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(
|
||||
commandsDirectory = Paths.get("/x/y/commands"),
|
||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
||||
user = "demo",
|
||||
password = "abcd1234",
|
||||
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
|
||||
ssl = expectedSsl,
|
||||
ssl = ClientRpcSslOptions(
|
||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
||||
trustStorePassword = "pass2"),
|
||||
sshdPort = 2223)
|
||||
|
||||
val config = options.toConfig()
|
||||
@ -179,18 +160,13 @@ class StandaloneShellArgsParserTest {
|
||||
loggingLevel = Level.DEBUG,
|
||||
sshdPort = null,
|
||||
sshdHostKeyDirectory = null,
|
||||
keyStorePassword = null,
|
||||
trustStorePassword = null,
|
||||
keyStoreFile = Paths.get("/x/y/cmd.jks"),
|
||||
trustStoreFile = null,
|
||||
keyStoreType = null,
|
||||
trustStoreType = null)
|
||||
|
||||
val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/cmd.jks"),
|
||||
keyStorePassword = "pass1",
|
||||
trustStoreFile = Paths.get("/x/y/truststore.jks"),
|
||||
trustStorePassword = "pass2",
|
||||
crlCheckSoftFail = true)
|
||||
val expectedSsl = ClientRpcSslOptions(
|
||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
||||
trustStorePassword = "pass2")
|
||||
val expectedConfig = ShellConfiguration(
|
||||
commandsDirectory = Paths.get("/x/y/commands"),
|
||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
||||
|
@ -21,11 +21,6 @@ extensions {
|
||||
}
|
||||
}
|
||||
ssl {
|
||||
keystore {
|
||||
path : "/x/y/keystore.jks"
|
||||
type : "JKS"
|
||||
password : "pass1"
|
||||
}
|
||||
truststore {
|
||||
path : "/x/y/truststore.jks"
|
||||
type : "JKS"
|
||||
|
Loading…
Reference in New Issue
Block a user