diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b088f61edb..2f9f9859ab 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -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 (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 (java.util.List) + public (java.util.List, net.corda.client.rpc.CordaRPCClientConfiguration) public (net.corda.core.utilities.NetworkHostAndPort) public (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.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() diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index d991c4e7cf..c896a70aa5 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -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 "^$" < + + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a27727f797..989c9f7ba6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,7 +17,6 @@ see changes to this list. * Andrius Dagys (R3) * Andrzej Cichocki (R3) * Andrzej Grzesik (R3) -* Anthony Coates (Deutsche Bank) * Anthony Keenan (R3) * Anthony Woolley (Société Générale) * Anton Semenov (Commerzbank) @@ -172,6 +171,7 @@ see changes to this list. * Thomas Schroeter (R3) * Tim Swanson (R3) * Timothy Smith +* Tittu Varghese (Servntire Global) * Tom Menner (R3) * tomconte * Tommy Lillehagen (R3) @@ -184,5 +184,4 @@ see changes to this list. * Vipin Bharathan * Wawrzek Niewodniczanski (R3) * Wei Wu Zhang (Commonwealth Bank of Australia) -* Zabrina Smith (Northern Trust) -* zorenmith (Northern Trust) +* Zabrina Smith (Northern Trust) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 7dbd56c50b..cafbb95371 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -18,8 +18,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 @@ -108,7 +108,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. @@ -116,9 +116,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 = emptyList() + private val haAddressPool: List = emptyList(), + private val internalConnection: Boolean = false ) { @JvmOverloads constructor(hostAndPort: NetworkHostAndPort, @@ -132,17 +134,25 @@ class CordaRPCClient private constructor( * @param configuration An optional configuration used to tweak client behaviour. */ @JvmOverloads - constructor(haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(haAddressPool.first(), configuration, null, null, haAddressPool) + constructor(haAddressPool: List, 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, + sslConfiguration: ClientRpcSslOptions, + configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default() + ): CordaRPCClient { + return CordaRPCClient(haAddressPool.first(), configuration, sslConfiguration, haAddressPool = haAddressPool) + } + internal fun createWithSsl( haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), @@ -154,16 +164,25 @@ class CordaRPCClient private constructor( 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) } internal fun createWithSslAndClassLoader( haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), - sslConfiguration: SSLConfiguration? = null, + sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null ): CordaRPCClient { return CordaRPCClient(haAddressPool.first(), configuration, sslConfiguration, classLoader, haAddressPool) @@ -182,17 +201,22 @@ class CordaRPCClient private constructor( } } - private fun getRpcClient() : RPCClient { - return if (haAddressPool.isEmpty()) { - RPCClient( - tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration), - configuration, - if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT) - } else { - RPCClient(haAddressPool, - sslConfiguration, + private fun getRpcClient(): RPCClient { + 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 -> { + RPCClient(haAddressPool, + sslConfiguration, + configuration, + if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT) + } } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt index c5984172ae..a7b8006071 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt @@ -15,33 +15,29 @@ 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 createCordaRPCClientWithSsl( - haAddressPool: List, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), - sslConfiguration: SSLConfiguration? = null -) = CordaRPCClient.createWithSsl(haAddressPool, configuration, sslConfiguration) - -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 createCordaRPCClientWithSslAndClassLoader( haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(), - sslConfiguration: SSLConfiguration? = null, + sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null ) = CordaRPCClient.createWithSslAndClassLoader(haAddressPool, configuration, sslConfiguration, classLoader) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index ff13da4c9b..37673b7e5e 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -23,10 +23,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 @@ -68,6 +69,9 @@ data class CordaRPCClientConfigurationImpl( } } +/** + * This runs on the client JVM + */ class RPCClient( val transport: TransportConfiguration, val rpcConfiguration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default, @@ -76,18 +80,25 @@ class RPCClient( ) { 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, - 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() diff --git a/core/src/main/kotlin/net/corda/core/messaging/ClientRpcSslOptions.kt b/core/src/main/kotlin/net/corda/core/messaging/ClientRpcSslOptions.kt new file mode 100644 index 0000000000..5b2b9776b1 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/messaging/ClientRpcSslOptions.kt @@ -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") \ No newline at end of file diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico new file mode 100644 index 0000000000..46f71069c7 Binary files /dev/null and b/docs/source/_static/favicon.ico differ diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 5e2e6f1d2f..20dd48c187 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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 diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index f413676450..1c0999ad8c 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -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 diff --git a/docs/source/conf.py b/docs/source/conf.py index 5202714c97..5763aeb512 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -144,7 +144,7 @@ html_add_permalinks = True # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +html_favicon = "_static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index e4973b7662..eab81cdb1d 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -334,4 +334,14 @@ path to the node's base directory. :permissions: A list of permissions for starting flows via RPC. To give the user the permission to start the flow ``foo.bar.FlowClass``, add the string ``StartFlow.foo.bar.FlowClass`` to the list. If the list contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator - users and for development. \ No newline at end of file + users and for development. + +Fields Override +--------------- +JVM options or environmental variables prefixed ``corda.`` can override ``node.conf`` fields. +Provided system properties also can set value for absent fields in ``node.conf``. +Example adding/overriding keyStore password when starting Corda node: + +.. sourcecode:: shell + + java -Dcorda.rpcSettings.ssl.keyStorePassword=mypassword -jar node.jar \ No newline at end of file diff --git a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf deleted file mode 100644 index 88fde57379..0000000000 --- a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf +++ /dev/null @@ -1,3 +0,0 @@ -myLegalName : "O=Bank A,L=London,C=GB" -p2pAddress : "my-corda-node:10002" -verifierType: "OutOfProcess" diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt index 216f00b20b..59c6e0889c 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt @@ -38,8 +38,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( diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index 9dff82ebb8..300b62540a 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -45,6 +45,10 @@ in the `Kotlin CorDapp Template () { - companion object { - const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.CommodityContract" - } - - /** A state representing a commodity claim against some party */ - data class State( - override val amount: Amount>, - - /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: AbstractParty - ) : FungibleAsset { - constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) - : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - - override val exitKeys: Set = Collections.singleton(owner.owningKey) - override val participants = listOf(owner) - - override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset - = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) - - override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by $owner)" - - override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) - } - - // Just for grouping - @CordaSerializable - interface Commands : CommandData { - /** - * A command stating that money has been moved, optionally to fulfil another contract. - * - * @param contract the contract this move is for the attention of. Only that contract's verify function - * should take the moved states into account when considering whether it is valid. Typically this will be - * null. - */ - data class Move(override val contract: Class? = null) : MoveCommand - - /** - * Allows new commodity states to be issued into existence. - */ - class Issue : TypeOnlyCommandData() - - /** - * A command stating that money has been withdrawn from the shared ledger and is now accounted for - * in some other way. - */ - data class Exit(val amount: Amount>) : CommandData - } - - override fun verify(tx: LedgerTransaction) { - // Each group is a set of input/output states with distinct (reference, commodity) attributes. These types - // of commodity are not fungible and must be kept separated for bookkeeping purposes. - val groups = tx.groupStates { it: CommodityContract.State -> it.amount.token } - - for ((inputs, outputs, key) in groups) { - // Either inputs or outputs could be empty. - val issuer = key.issuer - val commodity = key.product - val party = issuer.party - - requireThat { - "there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L }) - } - - val issueCommand = tx.commands.select().firstOrNull() - if (issueCommand != null) { - verifyIssueCommand(inputs, outputs, tx, issueCommand, commodity, issuer) - } else { - val inputAmount = inputs.sumCommoditiesOrNull() ?: throw IllegalArgumentException("there is at least one commodity input for this group") - val outputAmount = outputs.sumCommoditiesOrZero(Issued(issuer, commodity)) - - // If we want to remove commodity from the ledger, that must be signed for by the issuer. - // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. - val exitCommand = tx.commands.select(party = party).singleOrNull() - val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, commodity)) - - requireThat { - "there are no zero sized inputs" using (inputs.none { it.amount.quantity == 0L }) - "for reference ${issuer.reference} at issuer ${party.nameOrNull()} the amounts balance" using - (inputAmount == outputAmount + amountExitingLedger) - } - - verifyMoveCommand(inputs, tx.commands) - } - } - } - - private fun verifyIssueCommand(inputs: List, - outputs: List, - tx: LedgerTransaction, - issueCommand: CommandWithParties, - commodity: Commodity, - issuer: PartyAndReference) { - // If we have an issue command, perform special processing: the group is allowed to have no inputs, - // and the output states must have a deposit reference owned by the signer. - // - // Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must - // sum to more than the inputs. An issuance of zero size is not allowed. - // - // Note that this means literally anyone with access to the network can issue cash claims of arbitrary - // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some - // as-yet-unwritten identity service. See ADP-22 for discussion. - - // The grouping ensures that all outputs have the same deposit reference and currency. - val inputAmount = inputs.sumCommoditiesOrZero(Issued(issuer, commodity)) - val outputAmount = outputs.sumCommodities() - val commodityCommands = tx.commands.select() - requireThat { - "output deposits are ownedBy a command signer" using (issuer.party in issueCommand.signingParties) - "output values sum to more than the inputs" using (outputAmount > inputAmount) - "there is only a single issue command" using (commodityCommands.count() == 1) - } - } - - override fun extractCommands(commands: Collection>): List> - = commands.select() - - /** - * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. - */ - fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: AbstractParty, notary: Party) - = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) - - /** - * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. - */ - fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) - = generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue()) - - - override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) - = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) - - override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) - override fun generateMoveCommand() = Commands.Move() -} diff --git a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt index 8feffb9f7e..30aa0b5f30 100644 --- a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt +++ b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt @@ -20,9 +20,7 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.FungibleAsset import net.corda.core.contracts.Issued import net.corda.core.identity.AbstractParty -import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CommodityContract import net.corda.finance.contracts.asset.Obligation import java.util.* @@ -55,19 +53,6 @@ fun Iterable.sumFungibleOrNull() = filterIsInstance Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) -/** - * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies). - */ -fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() - -/** Sums the cash states in the list, returning null if there are none. */ -@Suppress("unused") -fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() - -/** Sums the cash states in the list, returning zero of the given currency if there are none. */ -fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero(currency) - /** * Sums the obligation states in the list, throwing an exception if there are none. All state objects in the * list are presumed to be nettable. diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 0149760736..054bcc99c6 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -35,6 +35,7 @@ import net.corda.testing.core.* import net.corda.testing.dsl.* import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.vault.CommodityState import net.corda.testing.node.MockServices import net.corda.testing.node.ledger import net.corda.testing.node.transaction @@ -584,15 +585,15 @@ class ObligationTests { unverifiedTransaction { attachments(Obligation.PROGRAM_ID) output(Obligation.PROGRAM_ID, "Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB)) - output(Obligation.PROGRAM_ID, "Alice's 1 FCOJ", CommodityContract.State(oneUnitFcoj, ALICE)) + output(Obligation.PROGRAM_ID, "Alice's 1 FCOJ", CommodityState(oneUnitFcoj, ALICE)) } transaction("Settlement") { attachments(Obligation.PROGRAM_ID) input("Alice's 1 FCOJ obligation to Bob") input("Alice's 1 FCOJ") - output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ", CommodityContract.State(oneUnitFcoj, BOB)) + output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ", CommodityState(oneUnitFcoj, BOB)) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token))) - command(ALICE_PUBKEY, CommodityContract.Commands.Move(Obligation::class.java)) + command(ALICE_PUBKEY, Obligation.Commands.Move(Obligation::class.java)) attachment(attachment(commodityContractBytes.inputStream())) verifies() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 98dd29e455..5a2921b768 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -10,7 +10,7 @@ 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 @@ -18,20 +18,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 = 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. *

    @@ -57,69 +48,114 @@ 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( - // Basic TCP target details. - TransportConstants.HOST_PROP_NAME to hostAndPort.host, - TransportConstants.PORT_PROP_NAME to hostAndPort.port, + private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf( + // Basic TCP target details. + TransportConstants.HOST_PROP_NAME to hostAndPort.host, + TransportConstants.PORT_PROP_NAME to hostAndPort.port, - // Turn on AMQP support, which needs the protocol jar on the classpath. - // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop. - // It does not use AMQP messages for its own messages e.g. topology and heartbeats. - // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. - TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", - TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), - 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 - ) + // Turn on AMQP support, which needs the protocol jar on the classpath. + // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop. + // It does not use AMQP messages for its own messages e.g. topology and heartbeats. + // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. + TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", + TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), + 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) + + 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) + } + + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + + if (config != null && enableSSL) { + config.sslKeystore.requireOnDefaultFileSystem() + config.trustStoreFile.requireOnDefaultFileSystem() + options.putAll(defaultSSLOptions) + options.putAll(config.toTransportOptions()) } - return TransportConfiguration(factoryName, options) + return TransportConfiguration(connectorFactoryClassName, options) + } + + /** [TransportConfiguration] for RPC TCP communication - server side. */ + fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + + if (config != null && enableSSL) { + config.keyStorePath.requireOnDefaultFileSystem() + options.putAll(config.toTransportOptions()) + options.putAll(defaultSSLOptions) + } + return TransportConfiguration(acceptorFactoryClassName, options) + } + + /** [TransportConfiguration] for RPC TCP communication + * This is the Transport that connects the client JVM to the broker. */ + fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + + if (config != null && enableSSL) { + config.trustStorePath.requireOnDefaultFileSystem() + options.putAll(config.toTransportOptions()) + options.putAll(defaultSSLOptions) + } + return TransportConfiguration(connectorFactoryClassName, options) } /** Create as list of [TransportConfiguration]. **/ - fun tcpTransportsFromList( - direction: ConnectionDirection, - hostAndPortList: List, - config: SSLConfiguration?, - enableSSL: Boolean = true): List { - val tcpTransports = ArrayList(hostAndPortList.size) - hostAndPortList.forEach { - tcpTransports.add(tcpTransport(direction, it, config, enableSSL)) - } + fun rpcConnectorTcpTransportsFromList(hostAndPortList: List, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List = 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) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index f61ed121dd..07e60c673d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -14,8 +14,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.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -47,7 +46,7 @@ class ArtemisMessagingClient( 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. @@ -63,7 +62,7 @@ class ArtemisMessagingClient( // 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, autoCommitSends, autoCommitAcks, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE) + val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, autoCommitSends, autoCommitAcks, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE) session.start() // Create a general purpose producer. val producer = session.createProducer() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index e9adeb50e3..15701f4816 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -32,8 +32,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." diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 80a4190f9c..365b5d150b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -19,7 +19,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 @@ -127,7 +127,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConf 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 diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index d66d136a12..3b6c8d1bfd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -253,7 +253,6 @@ class AMQPBridgeTest { doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort - doReturn(emptyList()).whenever(it).certificateChainCheckPolicies doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index ca7dab2cf7..e15d3c4c6c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -389,7 +389,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()).whenever(it).certificateChainCheckPolicies doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration doReturn(true).whenever(it).crlCheckSoftFail } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt index 059ce2e00c..8c5a282811 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt @@ -11,27 +11,32 @@ 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.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -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.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName +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.ClassRule +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder class RpcSslTest : IntegrationTest() { companion object { @@ -41,54 +46,120 @@ class RpcSslTest : IntegrationTest() { .map { it.toDatabaseSchemaName() }.toTypedArray()) } + @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 connection = client.start(user.username, user.password) - connection.proxy.apply { - nodeInfo() - successful = true - } + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { + 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.close() - } - assertThat(successful).isTrue() + 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_not_using_ssl() { + fun `RPC client using ssl will fail if connecting to a node that cannot present a matching certificate`() { + val user = User("mark", "dadada", setOf(all())) + var successful = false + + val (keyPair, cert) = createKeyPairAndSelfSignedCertificate() + val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert) + val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") + + val (_, cert1) = createKeyPairAndSelfSignedCertificate() + val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1) + val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") + + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { + val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() + Assertions.assertThatThrownBy { + val connection = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password) + connection.proxy.apply { + nodeInfo() + successful = true + } + connection.close() + }.isInstanceOf(ActiveMQNotConnectedException::class.java) + + } + + assertThat(successful).isFalse() + } + + @Test + fun `RPC client not using ssl can run commands`() { val user = User("mark", "dadada", setOf(all())) 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) + } + } + } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index aeea7ba228..9e802d4342 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -17,7 +17,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 @@ -53,10 +53,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) } } @@ -80,7 +80,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) } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt index 7eafa6bb97..62a4d6a0f7 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt @@ -14,7 +14,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.* @@ -33,7 +32,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 diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2707f9faa2..93a4702857 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -396,10 +396,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) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 86fe731b29..bd6509eeb0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -13,6 +13,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 @@ -38,19 +39,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 @@ -177,10 +181,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) { @@ -188,7 +193,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 { @@ -207,8 +213,7 @@ open class Node(configuration: NodeConfiguration, rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize ) rpcServerAddresses?.let { - rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, it.admin, MAX_RPC_MESSAGE_SIZE, rpcServerConfiguration) - + 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()) } @@ -236,18 +241,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 } } } @@ -314,12 +318,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 { @@ -382,20 +385,15 @@ 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 -> - // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') - val subName = name.substringAfter('.', "") - if (subName == "") - ObjectName("$domain:name=$category") - else - ObjectName("$domain:type=$category,name=$subName") - }. - build(). - start() + 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('.', "") + if (subName == "") + ObjectName("$domain:name=$category") + else + ObjectName("$domain:type=$category,name=$subName") + }.build().start() _startupComplete.set(Unit) } @@ -426,12 +424,12 @@ 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() } diff --git a/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt b/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt new file mode 100644 index 0000000000..2d81f3f425 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/artemis/BrokerJaasLoginModule.kt @@ -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, options: Map) { + 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?, password: String): Pair> { + fun requireTls(certificates: Array?) = 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() + + protected fun getUsernamePasswordAndCerts(): Triple?> { + 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, options: Map) { + 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 + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index f9803a2820..02914512a9 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -34,7 +34,7 @@ operator fun Config.plus(overrides: Map): Config = ConfigFactory.p object ConfigHelper { - const val CORDA_PROPERTY_PREFIX = "corda." + private const val CORDA_PROPERTY_PREFIX = "corda." private val log = LoggerFactory.getLogger(javaClass) fun loadConfig(baseDirectory: Path, diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index a6310e578a..2ac067df96 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -25,6 +25,11 @@ import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag +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.slf4j.Logger @@ -130,7 +135,7 @@ data class MySQLConfiguration( */ val connectionRetries: Int = 2, // Default value for a 3 server cluster. /** - * Time increment between re-connection attempts. + * Time increment between re-connection attempts. * * The total back-off duration is calculated as: backOffIncrement * backOffBase ^ currentRetryCount */ @@ -209,7 +214,8 @@ data class NodeConfigurationImpl( override val messagingServerExternal: Boolean = (messagingServerAddress != null), override val enterpriseConfiguration: EnterpriseConfiguration, override val notary: NotaryConfig?, - override val certificateChainCheckPolicies: List, + @Deprecated("Do not configure") + override val certificateChainCheckPolicies: List = emptyList(), override val devMode: Boolean = false, override val noLocalShell: Boolean = false, override val devModeOptions: DevModeOptions? = null, @@ -233,9 +239,9 @@ data class NodeConfigurationImpl( private val logger = loggerFor() } - 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)." } @@ -328,17 +334,24 @@ data class NodeConfigurationImpl( } else { require(maxConnectionPoolSize.toInt() > flowThreadPoolSize) } + + // Check for usage of deprecated config + 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 @@ -366,6 +379,7 @@ enum class CertChainPolicyType { UsernameMustMatch } +@Deprecated("Do not use") data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set) { val certificateChainCheckPolicy: CertificateChainCheckPolicy get() { diff --git a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt b/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt deleted file mode 100644 index 84c6060338..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - -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() \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/rpc/NodeRpcOptions.kt b/node/src/main/kotlin/net/corda/node/services/config/rpc/NodeRpcOptions.kt index ea20bcdd7d..a992c4b6e5 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/rpc/NodeRpcOptions.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/rpc/NodeRpcOptions.kt @@ -11,12 +11,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 } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt index 1ff4ad66df..5a0eaa4ac9 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt @@ -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( - commandsDirectory = this.baseDirectory / COMMANDS_DIR, - cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, - user = localShellUser.username, - password = localShellUser.password, - hostAndPort = this.rpcOptions.address ?: NetworkHostAndPort("localhost", SSH_PORT), - ssl = sslConfiguration, - sshdPort = this.sshd?.port, - sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR, - noLocalShell = this.noLocalShell) -} - -fun localShellUser() = User("shell", "shell", setOf(Permissions.all())) +fun NodeConfiguration.toShellConfig() = ShellConfiguration( + commandsDirectory = this.baseDirectory / COMMANDS_DIR, + cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, + 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) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 0445f524a3..f5aa822805 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -12,59 +12,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) @@ -143,11 +120,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 @@ -174,11 +147,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 } @@ -194,158 +165,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 { - 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) + return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig) } - - private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) = - ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL) } - -/** - * Clients must connect to us with a username and password and must use TLS. If a someone connects with - * [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate - * is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with - * [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is - * the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address. - * In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume - * the CN within that is their legal name. - * Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of - * valid RPC users. RPC clients are given permission to perform RPC and nothing else. - */ -class NodeLoginModule : LoginModule { - companion object { - // Include forbidden username character to prevent name clash with any RPC usernames - const val PEER_ROLE = "SystemRoles/Peer" - const val NODE_ROLE = "SystemRoles/Node" - const val VERIFIER_ROLE = "SystemRoles/Verifier" - - const val CERT_CHAIN_CHECKS_OPTION_NAME = "CertChainChecks" - private val log = contextLogger() - } - - private var loginSucceeded: Boolean = false - private lateinit var subject: Subject - private lateinit var callbackHandler: CallbackHandler - private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check - private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check - private lateinit var verifierCertCheck: CertificateChainCheckPolicy.Check - private val principals = ArrayList() - - override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map, options: Map) { - this.subject = subject - this.callbackHandler = callbackHandler - val certChainChecks: Map = 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): String { - nodeCertCheck.checkCertificateChain(certificates) - principals += RolePrincipal(NODE_ROLE) - return certificates.first().subjectDN.name - } - - private fun authenticateVerifier(certificates: Array): String { - verifierCertCheck.checkCertificateChain(certificates) - principals += RolePrincipal(VERIFIER_ROLE) - return certificates.first().subjectDN.name - } - - private fun authenticatePeer(certificates: Array): String { - peerCertCheck.checkCertificateChain(certificates) - principals += RolePrincipal(PEER_ROLE) - return certificates.first().subjectDN.name - } - - private fun determineUserRole(certificates: Array?, 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 - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt new file mode 100644 index 0000000000..03b24f8194 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt @@ -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() +} diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index e55f5e2f99..d20eda2263 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -40,8 +40,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 @@ -214,7 +213,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. @@ -229,7 +228,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() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt deleted file mode 100644 index 990589d06a..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - -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, - private val rpcServerConfiguration: RPCServerConfiguration = RPCServerConfiguration.default -) : 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), rpcServerConfiguration) - } - - fun start2(serverControl: ActiveMQServerControl) = synchronized(this) { - rpcServer!!.start(serverControl) - } - - fun stop() = synchronized(this) { - rpcServer?.close() - artemis.stop() - } - - override fun close() = stop() -} diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt index 004f1d8202..28c329b434 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt @@ -10,48 +10,45 @@ 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, 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() - fun withSsl(address: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List, 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, 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) } } @@ -76,8 +73,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 } @@ -86,33 +83,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 { 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 CompletableFuture.toObservable() = Observable.from(this) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/NodeLoginModule.kt b/node/src/main/kotlin/net/corda/node/services/rpc/NodeLoginModule.kt deleted file mode 100644 index f4b774f6a0..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/rpc/NodeLoginModule.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - -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() - } - - 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() - - override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map, options: Map) { - 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 = 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) { - nodeCertCheck.checkCertificateChain(certificates) - principals += RolePrincipal(NodeLoginModule.NODE_ROLE) - } - - private fun authenticateRpcUser(username: String, password: Password, certificates: Array, 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, 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 - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RolesAdderOnLogin.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RolesAdderOnLogin.kt index efbe2df195..6b94700806 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RolesAdderOnLogin.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RolesAdderOnLogin.kt @@ -18,10 +18,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>) : SecuritySettingPlugin { +internal class RolesAdderOnLogin(val systemUsers: List = emptyList(), val source: (String) -> Pair>) : SecuritySettingPlugin { private lateinit var repository: RolesRepository fun onLogin(username: String) { @@ -34,6 +34,8 @@ internal class RolesAdderOnLogin(val source: (String) -> Pair> 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 diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index 925952217b..8c03af7a05 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -12,30 +12,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 @@ -51,9 +53,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, @@ -73,7 +76,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) } @@ -128,11 +131,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 { diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 2685f9f463..d9b1a35f17 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -18,6 +18,7 @@ import net.corda.core.internal.toPath import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag 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 @@ -196,7 +197,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, @@ -210,7 +211,6 @@ class NodeConfigurationImplTest { messagingServerAddress = null, p2pMessagingRetry = P2PMessagingRetryConfiguration(5.seconds, 3, 1.0), notary = null, - certificateChainCheckPolicies = emptyList(), devMode = true, noLocalShell = false, rpcSettings = rpcSettings, diff --git a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index 8d6ae40791..610d3d2fa1 100644 --- a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -13,23 +13,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 @@ -37,9 +40,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 @@ -52,168 +54,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, _ -> - // here client sslOptions are passed null (as in, do not use SSL) - testSslCommunication(brokerSslOptions, true, null, clientConnectionSpy = expectExceptionOfType(ActiveMQConnectionTimedOutException::class)) - } - } + 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) + 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 = 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(tcpTransport(ConnectionDirection.Outbound(), broker.addresses.primary, clientSslOptions)) + val client = RPCClient(rpcConnectorTcpTransport(broker.addresses.primary, clientSslOptions)) - clientConnectionSpy { - client.start(TestRpcOps::class.java, user.username, user.password).use { connection -> - connection.proxy.apply { - val greeting = greet("Frodo") - assertThat(greeting).isEqualTo("Oh, hello 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 RPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) { + private fun InternalRPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) { apply { - start(ops, securityManager) - start2(brokerControl) + init(ops, securityManager) + start(brokerControl) } } - private fun expectExceptionOfType(exceptionType: KClass): (() -> Unit) -> Unit { - return { action -> assertThatThrownBy { action.invoke() }.isInstanceOf(exceptionType.java) } - } - interface TestRpcOps : RPCOps { fun greet(name: String): String } @@ -223,4 +143,7 @@ class ArtemisRpcTests { override val protocolVersion: Int = 1 } + + private fun tempFile(name: String): Path = tempFolder.root.toPath() / name + } \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 423ea3b5ef..8afd6cb7f0 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -60,6 +60,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 /** The location of the node's base directory **/ diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index 05e4f7de78..e060181dcc 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -34,7 +34,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 } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index bf2e0aa6ba..7f7839d8c5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -16,8 +16,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 @@ -159,12 +158,8 @@ class DriverDSLImpl( } private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { - 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) } @@ -248,9 +243,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) )) @@ -351,17 +350,13 @@ class DriverDSLImpl( val rpcAddress = if (cordform.rpcAddress == null) { val overrides = mutableMapOf("rpcSettings.address" to portAllocation.nextHostAndPort().toString()) cordform.config.apply { - if (!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) { - overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString() - } + overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString() } overrides } else { val overrides = mutableMapOf() cordform.config.apply { - if ((!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) && !hasPath("rpcSettings.adminAddress")) { - overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString() - } + overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString() } overrides } @@ -843,10 +838,10 @@ class DriverDSLImpl( private val propertiesInScope = setOf("java.io.tmpdir", AbstractAMQPSerializationScheme.SCAN_SPEC_PROP_NAME) - private fun inheritFromParentProcess() : Iterable> { + private fun inheritFromParentProcess(): Iterable> { return propertiesInScope.flatMap { propName -> - val propValue : String? = System.getProperty(propName) - if(propValue == null) { + val propValue: String? = System.getProperty(propName) + if (propValue == null) { emptySet() } else { setOf(Pair(propName, propValue)) @@ -859,7 +854,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() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 82b2bf4f77..47918879a1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -477,7 +477,6 @@ private fun mockNodeConfiguration(): NodeConfiguration { doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).devMode doReturn(null).whenever(it).compatibilityZoneURL - doReturn(emptyList()).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 diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 73f54876b7..fde79971ab 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -31,7 +31,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 @@ -51,7 +50,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 @@ -222,20 +220,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) } } @@ -346,7 +343,7 @@ data class RPCDriverDSL( configuration: CordaRPCClientConfigurationImpl = CordaRPCClientConfigurationImpl.default ): CordaFuture { return driverDSL.executorService.fork { - val client = RPCClient(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration) + val client = RPCClient(ArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration) val connection = client.start(rpcOpsClass, username, password, externalTrace) driverDSL.shutdownManager.registerShutdown { connection.close() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 6e68af7d01..26da293652 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -19,14 +19,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") @@ -101,13 +103,11 @@ fun createDevNodeCaCertPath( return Triple(rootCa, intermediateCa, nodeCa) } -fun SSLConfiguration.useSslRpcOverrides(): Map { +fun BrokerRpcSslOptions.useSslRpcOverrides(): Map { 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 ) } @@ -134,3 +134,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 { + 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 +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index 410e7a7ae3..a469e7f314 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -25,7 +25,8 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.DealState import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CommodityContract +import net.corda.finance.contracts.asset.Obligation +import net.corda.finance.contracts.asset.OnLedgerAsset import net.corda.testing.core.* import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentityAndCert @@ -176,19 +177,26 @@ class VaultFiller @JvmOverloads constructor( return Vault(states) } + + /** + * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. + */ + fun generateCommoditiesIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) + = OnLedgerAsset.generateIssue(tx, TransactionState(CommodityState(amount, owner), Obligation.PROGRAM_ID, notary), Obligation.Commands.Issue()) + + /** * * @param issuerServices service hub of the issuer node, which will be used to sign the transaction. * @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!). */ // TODO: need to make all FungibleAsset commands (issue, move, exit) generic - fun fillWithSomeTestCommodity(amount: Amount, issuerServices: ServiceHub, issuedBy: PartyAndReference): Vault { + fun fillWithSomeTestCommodity(amount: Amount, issuerServices: ServiceHub, issuedBy: PartyAndReference): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) - val commodity = CommodityContract() val issuance = TransactionBuilder(null as Party?) - commodity.generateIssue(issuance, Amount(amount.quantity, Issued(issuedBy, amount.token)), me, altNotary) + generateCommoditiesIssue(issuance, Amount(amount.quantity, Issued(issuedBy, amount.token)), me, altNotary) val transaction = issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey) services.recordTransactions(transaction) return Vault(setOf(transaction.tx.outRef(0))) @@ -251,3 +259,27 @@ class VaultFiller @JvmOverloads constructor( return update.getOrThrow(Duration.ofSeconds(3)) } } + + + +/** A state representing a commodity claim against some party */ +data class CommodityState( + override val amount: Amount>, + + /** There must be a MoveCommand signed by this key to claim the amount */ + override val owner: AbstractParty +) : FungibleAsset { + constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + + override val exitKeys: Set = Collections.singleton(owner.owningKey) + override val participants = listOf(owner) + + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset + = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) + + override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by $owner)" + + override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Obligation.Commands.Move(), copy(owner = newOwner)) +} + diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt index a83b557aa3..bdf1b0f537 100644 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -13,23 +13,28 @@ 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.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_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.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName +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 @@ -39,7 +44,9 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.util.io.Streams import org.junit.ClassRule import org.junit.Ignore +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import kotlin.test.assertTrue class InteractiveShellIntegrationTest : IntegrationTest() { @@ -50,6 +57,10 @@ class InteractiveShellIntegrationTest : IntegrationTest() { .map { it.toDatabaseSchemaName() }.toTypedArray()) } + @Rule + @JvmField + val tempFolder = TemporaryFolder() + @Test fun `shell should not log in with invalid credentials`() { val user = User("u", "p", setOf()) @@ -67,7 +78,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { } @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) @@ -85,72 +96,65 @@ class InteractiveShellIntegrationTest : IntegrationTest() { @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) + var successful = false - // 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") - client.keyStore["shell"] = 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)) { - startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { + 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) + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress, + ssl = clientSslOptions) - InteractiveShell.startShell(conf) + InteractiveShell.startShell(conf) - InteractiveShell.nodeInfo() - successful = true - } - } - assertThat(successful).isTrue() + InteractiveShell.nodeInfo() + successful = true + } + } + assertThat(successful).isTrue() + } + + @Test + fun `shell shoud not log in with invalid truststore`() { + val user = User("mark", "dadada", setOf("ALL")) + val (keyPair, cert) = createKeyPairAndSelfSignedCertificate() + val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert) + val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") + + val (_, cert1) = createKeyPairAndSelfSignedCertificate() + val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1) + val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") + + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { + startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> + + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress, + ssl = clientSslOptions) + + InteractiveShell.startShell(conf) + + assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java) } } } @Test - fun `shell shoud not log in without ssl keystore`() { - 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 key store doesn't have "mark" certificate - client.trustStore["cordaclienttls"] = rootCertificate - - withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> - driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { - startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> - - val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, - clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) - val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), - user = user.username, password = user.password, - hostAndPort = node.rpcAddress, - ssl = sslConfiguration) - - InteractiveShell.startShell(conf) - - assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java) - } - } + 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) } } } @@ -204,62 +208,54 @@ class InteractiveShellIntegrationTest : IntegrationTest() { val user = User("mark", "dadada", setOf(Permissions.startFlow(), 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 + var successful = false + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) { + startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> - 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 conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress, + ssl = clientSslOptions, + sshdPort = 2223) - 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, - sshdPort = 2223) + InteractiveShell.startShell(conf) + InteractiveShell.nodeInfo() - InteractiveShell.startShell(conf) - InteractiveShell.nodeInfo() + val session = JSch().getSession("mark", "localhost", 2223) + session.setConfig("StrictHostKeyChecking", "no") + session.setPassword("dadada") + session.connect() - val session = JSch().getSession("mark", "localhost", 2223) - session.setConfig("StrictHostKeyChecking", "no") - session.setPassword("dadada") - session.connect() + assertTrue(session.isConnected) - assertTrue(session.isConnected) + val channel = session.openChannel("exec") as ChannelExec + channel.setCommand("start FlowICanRun") + channel.connect(5000) - val channel = session.openChannel("exec") as ChannelExec - channel.setCommand("start FlowICanRun") - channel.connect(5000) + assertTrue(channel.isConnected) - assertTrue(channel.isConnected) + val response = String(Streams.readAll(channel.inputStream)) - val response = String(Streams.readAll(channel.inputStream)) + val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") } - val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") } + channel.disconnect() + session.disconnect() // TODO Simon make sure to close them - channel.disconnect() - session.disconnect() // TODO Simon make sure to close them + // There are ANSI control characters involved, so we want to avoid direct byte to byte matching. + assertThat(linesWithDoneCount).hasSize(1) - // There are ANSI control characters involved, so we want to avoid direct byte to byte matching. - assertThat(linesWithDoneCount).hasSize(1) - - successful = true - } - } - assertThat(successful).isTrue() + successful = true } + + assertThat(successful).isTrue() + } } } \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 646b1c5e31..3f86919db2 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -19,6 +19,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 @@ -90,7 +91,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 { @@ -101,6 +101,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 diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt index 28b773b085..90d5f8407c 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt @@ -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("") -} \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt index a003db498c..bc0b455a54 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt @@ -16,6 +16,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 @@ -62,15 +63,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() @@ -95,11 +87,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)) } @@ -117,11 +106,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 { @@ -133,9 +119,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 } @@ -197,12 +180,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 ) @@ -215,12 +197,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( diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt index 04743c53f5..50ee47cdc2 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -12,6 +12,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 @@ -34,12 +35,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", @@ -53,12 +51,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) @@ -80,12 +75,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) @@ -106,19 +98,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"), @@ -149,25 +136,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() @@ -189,18 +170,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"), diff --git a/tools/shell/src/test/resources/config.conf b/tools/shell/src/test/resources/config.conf index 9a964cec18..e4446f1ec9 100644 --- a/tools/shell/src/test/resources/config.conf +++ b/tools/shell/src/test/resources/config.conf @@ -21,11 +21,6 @@ extensions { } } ssl { - keystore { - path : "/x/y/keystore.jks" - type : "JKS" - password : "pass1" - } truststore { path : "/x/y/truststore.jks" type : "JKS"