Merge remote-tracking branch 'open/master' into tudor-merge-21-05-18

# Conflicts:
#	.idea/compiler.xml
#	client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt
#	client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt
#	docs/source/generating-a-node.rst
#	finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt
#	node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt
#	node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
#	node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt
#	node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt
#	node/src/main/kotlin/net/corda/node/internal/Node.kt
#	node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt
#	node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
#	node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt
#	node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt
#	node/src/main/kotlin/net/corda/node/services/rpc/NodeLoginModule.kt
#	tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt
This commit is contained in:
tudor.malene@gmail.com 2018-05-21 13:49:59 +01:00
commit 2ca11d7996
67 changed files with 1383 additions and 1307 deletions

View File

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

View File

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

2
.idea/compiler.xml generated
View File

@ -162,6 +162,8 @@
<module name="node-driver_integrationTest" target="1.8" />
<module name="node-driver_main" target="1.8" />
<module name="node-driver_test" target="1.8" />
<module name="node-schemas_main" target="1.8" />
<module name="node-schemas_test" target="1.8" />
<module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" />
<module name="node_smokeTest" target="1.8" />

View File

@ -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)

View File

@ -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<NetworkHostAndPort> = emptyList()
private val haAddressPool: List<NetworkHostAndPort> = 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<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(haAddressPool.first(), configuration, null, null, haAddressPool)
constructor(haAddressPool: List<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool)
companion object {
internal fun createWithSsl(
fun createWithSsl(
hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration? = null
sslConfiguration: ClientRpcSslOptions,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()
): CordaRPCClient {
return CordaRPCClient(hostAndPort, configuration, sslConfiguration)
}
fun createWithSsl(
haAddressPool: List<NetworkHostAndPort>,
sslConfiguration: ClientRpcSslOptions,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()
): CordaRPCClient {
return CordaRPCClient(haAddressPool.first(), configuration, sslConfiguration, haAddressPool = haAddressPool)
}
internal fun createWithSsl(
haAddressPool: List<NetworkHostAndPort>,
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<NetworkHostAndPort>,
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<CordaRPCOps> {
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<CordaRPCOps> {
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)
}
}
}

View File

@ -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<NetworkHostAndPort>,
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<NetworkHostAndPort>,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default(),
sslConfiguration: SSLConfiguration? = null,
sslConfiguration: ClientRpcSslOptions? = null,
classLoader: ClassLoader? = null
) = CordaRPCClient.createWithSslAndClassLoader(haAddressPool, configuration, sslConfiguration, classLoader)

View File

@ -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<I : RPCOps>(
val transport: TransportConfiguration,
val rpcConfiguration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
@ -76,18 +80,25 @@ class RPCClient<I : RPCOps>(
) {
constructor(
hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration? = null,
sslConfiguration: ClientRpcSslOptions? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration, serializationContext)
) : this(rpcConnectorTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext)
constructor(
hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(rpcInternalClientTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext)
constructor(
haAddressPool: List<NetworkHostAndPort>,
sslConfiguration: SSLConfiguration? = null,
sslConfiguration: ClientRpcSslOptions? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfigurationImpl.default,
serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT
) : this(tcpTransport(ConnectionDirection.Outbound(), haAddressPool.first(), sslConfiguration),
configuration, serializationContext, tcpTransportsFromList(ConnectionDirection.Outbound(), haAddressPool, sslConfiguration))
) : this(rpcConnectorTcpTransport(haAddressPool.first(), sslConfiguration),
configuration, serializationContext, rpcConnectorTcpTransportsFromList(haAddressPool, sslConfiguration))
companion object {
private val log = contextLogger()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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.
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

View File

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

View File

@ -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(

View File

@ -45,6 +45,10 @@ in the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotli
h2Port 10004
// Includes the corda-finance CorDapp on our node.
cordapps = ["$corda_release_distribution:corda-finance:$corda_release_version"]
// Specify a JVM argument to be used when running the node (in this case, extra heap size).
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
}
node {
name "O=PartyA,L=London,C=GB"

View File

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

View File

@ -1,9 +1,3 @@
@misc{IT,
title = "\emph{{IT in banks: What does it cost?}}",
author = "{{Mai}}",
howpublished = "{\url{https://www.dbresearch.com/PROD/DBR_INTERNET_ENPROD/PROD0000000000299039.pdf}}",
year = 2012
}
@misc{DTCC,
title = "\emph{{Embracing Disruption}}",
@ -25,6 +19,20 @@
year = 2014
}
@misc{CordaIntro,
title = "\emph{{Corda: An Introduction}}",
author = "{{Brown, Carlyle, Grigg, \& Hearn}}",
howpublished = "{\url{https://docs.corda.net/_static/corda-introductory-whitepaper.pdf}}",
year = 2016
}
@misc{CordaTech,
title = "\emph{{Corda: A Distributed Ledger}}",
author = "{{Hearn}}",
howpublished = "{\url{https://docs.corda.net/_static/corda-technical-whitepaper.pdf}}",
year = 2016
}
@misc{EUC,
title = "\emph{{The European Commission's Settlement Finality Directive}}",
author = "{{European Commission}}",

View File

@ -0,0 +1,5 @@
#!/bin/sh
pdflatex corda-platform-whitepaper.tex ; bibtex corda-platform-whitepaper.aux ; pdflatex corda-platform-whitepaper.tex; pdflatex corda-platform-whitepaper.tex
open corda-platform-whitepaper.pdf &
rm corda-platform-whitepaper.toc corda-platform-whitepaper.out corda-platform-whitepaper.log corda-platform-whitepaper.blg corda-platform-whitepaper.bbl corda-platform-whitepaper.aux

Binary file not shown.

View File

@ -0,0 +1,295 @@
\documentclass{article}
\author{Richard Gendal Brown}
\date{May, 2018}
\title{The Corda Platform: An Introduction}
\usepackage{amsfonts}
\usepackage{listings}
\usepackage{color}
\usepackage{epigraph}
\usepackage{graphicx}
\graphicspath{ {images/} }
\usepackage[export]{adjustbox}
\usepackage{float}
\usepackage{hyperref}
\usepackage[super,comma,sort&compress]{natbib}
\usepackage[nottoc]{tocbibind}
\renewcommand{\thefootnote}{\alph{footnote}}
\setlength\epigraphwidth{4.5cm}
\setlength\epigraphrule{0pt}
\begin{document}
\maketitle
\begin{abstract}
A distributed ledger made up of mutually distrusting nodes would allow for a single global database that records the state of deals, obligations and other agreements between institutions and people. This would eliminate much of the manual, time consuming effort currently required to keep disparate ledgers synchronised with each other. It would also allow for greater levels of code sharing than is currently typical in the financial and other industries, reducing transactions costs for everyone. We present Corda, a software implementation which is designed to achieve these goals, and a network to which Corda nodes deployed by a broad range of operators can be connected to be assured of mutual interoperability. This paper provides a high level introduction intended for the general reader and supersedes its predecessor, `Corda: An Introduction'\cite{CordaIntro}. A companion technical white paper\cite{CordaTech} elaborates on the design and fundamental architectural decisions.
\end{abstract}
\newpage
\tableofcontents
\newpage
\section{Introduction}
We envision a future where legal agreements such as business contracts are recorded and automatically managed without error, where anybody can transact seamlessly for any contractual purpose without friction. We believe markets will move towards models where parties to contracts collaborate to maintain accurate, shared records rather than maintaining their own independent and inconsistent systems which require extensive reconciliation processes to ensure consistency. Duplicates, reconciliations, failed matches and breaks will be things of the past. Isolated pools of trapped assets will be no more.
This paper introduces the Corda platform. The Corda platform consists of an open source software project, Corda, and a set of standards, network parameters and associated governance processes, which together define the global Corda Network. Collectively, they enable any organisation or individual on this open network to transact directly with any other. Uniquely, this architecture is designed to model and automate real-world transactions in a legally enforceable manner, and do so across an open network on which multiple applications can execute and seamlessly interoperate. It does so whilst placing identity, transaction finality, privacy and open governance at its core.
The end state vision is one where real-world entities manage legally-enforceable contracts, and transfer value without technological constraints or loss of privacy. In contrast to `unpermissioned' blockchain platforms, the Corda platform is intended to manage real-world transactions between identifiable parties, with privacy and legal certainty. In contrast to other `permissioned' blockchain platforms, the Corda Platform is intended to allow multiple groups of participants (and associated applications) to co-exist and interoperate across the same open network. The network's governance model is explicitly designed to reflect the common interests of the diverse user-base of the platform.
To facilitate settlement of obligations arising from contracts managed on the platform, the Corda platform enables the issuance, transfer and redemption of cash-like liabilities denominated in real-world currencies where regulation allows. In addition, the architecture enables the issuance of native assets and tokens on the platform which can be used to incentivise adoption and participation, pay for services and can be either platform-wide or specific to particular Business Networks\footnote{See section \ref{businessnetworks} for a definition} operating on the broader Corda network.
This paper introduces the design features of the Corda platform which we believe make it an attractive choice for anybody seeking to record, manage and automate real-world agreements across the internet without friction.\footnote{The author can be reached via email: Richard Gendal Brown \href{mailto:richard@r3.com}{(richard@r3.com)}. This paper builds on its predecessor, which was authored by Richard Brown, James Carlyle, Ian Grigg and Mike Hearn. In addition, the author is grateful to Nick Arini, Dave Hudson, Nigel King, Todd McDonald and Mike Ward for input to and review of this paper.}.
\section{Context}
Corda has been open source software since 2016. The design of Corda has benefitted from the collaboration of a global and diverse alliance of organisations representing many industries, and regulatory engagement has been a key element of this design process\footnote{Many of the individuals and firms who contributed to the design and development of Corda are listed at \url{https://github.com/corda/corda/blob/master/CONTRIBUTORS.md}.}. The requirements of the financial industry formed the original basis of Corda's design but field experience has demonstrated that Corda has broad applicability, well beyond banking.
The motivating problem, which Corda seeks to solve, is the problem of managing contracts and other agreements between any combination of firms and individuals, especially when those parties trust each other enough to trade but not enough to have their counterparty maintain all the records.
Regardless of industry or geography, we see the same inefficient pattern in today's business environment: institutions invariably maintain their own ledgers, which record each firm's view of its agreements and positions with respect to its customer set and its trading partners. This duplication can lead to inconsistencies, and it drives a need for costly matching, reconciliation and fixing of errors by and among the various parties to a transaction. To the extent that differences remain between two firms' views of the same transaction, this is also a source of risk, some of it potentially systemic.
Until recently, this was unavoidable: except for centralised market infrastructures\footnote{Examples of this in the financial industry include the Depository Trust \& Clearing Corporation (DTCC) and Continuous Linked Settlement Group (CLS), and there are equivalents in many other industries}, there were few effective ways to consolidate technology across firms without also consolidating the firms themselves.
Centralised market infrastructure utilities have gone some way towards increasing the amount of data and business-logic sharing between firms. But this development creates unfavourable tradeoffs of its own: such as the possibility of rent-seeking and inertia. This has been evidenced by how the world of financial transactions still lags far behind that which has been achieved in the realm of consumer and web based software since the advent of the web.
We believe that the maturation of cryptographic techniques, exemplified in part by what is commonly referred to as ``blockchain technology", provides a new opportunity. This is the possibility of authoritative systems of record that are securely shared between firms, and which enable a large subset of each firm's transactions to be managed in a common way. Systems that provide a guarantee that ``I know that what I see is what you see".
This vision is in contrast to today, where a multitude of expensive, non-interoperable, isolated systems drive up cost, risk, duplication and error. In essence, we foresee a transition from IT infrastructures that are developed and optimised at the level of individual firms to IT infrastructures that are developed and optimised at the level of markets and industries.
\section{Vision}
In the long-term, one can envision a shared ``global logical ledger" with which all economic actors (companies, individuals, machines) will interact and which will allow any parties to record and manage agreements amongst themselves in a secure, consistent, reliable, private, auditable and authoritative manner.
A possible end-state is one in which we have moved from authoritative systems-of-record maintained \textit{within} firms, and which must then be expensively reconciled, to global authoritative systems-of-record shared \textit{between} all economic actors: optimisation at the level of markets, not at the level of firms.
This vision provides the opportunity to transform the economics of firms by implementing a new shared platform for the recording of commercial agreements and processing of business logic: one where a single global logical ledger is authoritative for all agreements between participants recorded on it. This architecture defines a new shared platform for all industries, upon which incumbents, new entrants and third parties can compete to deliver innovative new products and services.
Importantly, the vision is one in which a large number of firms, suppliers, customers and third parties manage a large and diverse range of agreements on the \textit{same} platform, through deployment of a large range of Corda Applications, or \textit{CorDapps}, across a common, shared, openly governed network. Corda is designed to allow information and assets gained through usage of one CorDapp to be used in a different context with different counterparties in a different CorDapp. This stands in contrast with other enterprise-focussed blockchain platforms, which are designed to be deployed as isolated instances for each supported application.
\begin{figure}[H]
\includegraphics[scale=.5, center]{sharedlogic}
\caption{In the diagram above, we show a progression from a world where parties to shared facts record and manage their own records, with associated discrepancies and duplications (\textit{``Bilateral - Reconciliation"}) or one where parties delegate control and responsibility over critical processing to centralised utilities (\textit{``Third Party / Market Infrastructure"}), to one where they collaborate to maintain a shared record, assured to be consistent between them, consuming the services of existing and new service providers and market infrastructure providers on an open and competitive basis (\textit{``Shared Ledger Vision"}).}
\end{figure}
We believe that the savings accruing from higher-quality data, fewer discrepancies and quicker agreement of details between firms will be significant. Moreover, deployment of this common architecture across firms will define a new platform on which existing and new providers can compete to serve the needs of clients. Going further, it is possible that such a platform will also find application within firms, where the problem of multiple systems recording details of the same trades is also a major driver of cost and complexity.
However, it is also important to ensure that the mistakes associated with new technology introduction in the past are not repeated. Isolated, non-interoperable, standalone deployments help nobody. The Corda vision is one where \textit{multiple} applications, products and services are deployed to an openly governed \textit{common shared network}, where assets gained in one context from one trading partner for one service can be immediately redeployed without friction or transfer costs for another purpose to pay a different trading partner utilising a different application. To achieve this, different solutions utilising Corda software need to share some common standards and agree on some common parameters. This common layer of standards, underpinned by open, transparent governance over their evolution, defines the global Corda \textit{network}.
\subsection{End-State Principles}
Business principles underpinning our end-state vision using the Corda platform include:
\begin{itemize}
\item Inclusion. Parties are able to discover each other freely, and transact directly, in a single, open network.
\item Assured identity. Parties will have assurance over the identity of participants in the network.
\item Privacy. The only parties who have access to the details of a transaction are those who participate in the transaction and those who need to assure themselves of transaction provenance.
\item Shared logic. The behavior of agreements managed by the system will be described in computer code that is shared to ensure consistency and validity of agreements.
\item Legal footing. Deals recorded by the ledger are, by contract, accepted as admissible evidence and legally binding by all parties in any dispute.\cite{Ricardian}
\item Authoritative. Facts recorded by the ledger are regarded as authoritative rather than ``shadows" of authoritative data held elsewhere.
\item Immutability. Facts recorded on the ledger are final and immutable; errors and unwinds must be processed through a subsequent transaction.
\item Open. The system is open: open source, participation, development, governance and standards to ensure the platform balances the needs of its diverse user-base in a transparent fashion.
\end{itemize}
\paragraph{}
Architectural choices underpinning the vision include:
\begin{itemize}
\item Scale. The network will scale to support billions of transactions daily across industries.
\item Longevity. Different versions of Corda will be able to coexist on the same network and applications will continue to run on later versions.
\item Secure. This system will operate under the assumption of an adversarial security environment.
\item Stable. The network will evolve carefully, with consensus-critical \textit{``network parameters"} maintained through a transparently governed process.
\item Interoperable. The platform is designed to allow multiple applications to coexist and interoperate on the same network; a standardised set of interfaces for contracts are included to maximise interoperability from a diverse range of providers; the Corda software is implemented on industry-standard tools to maximise deployability and integration with existing enterprise infrastructures.
\end{itemize}
We will measure successful delivery of this vision through the reduction of cost, risk and regulatory burden, through the introduction of innovative new products and services by third parties, and by the diversity and scale of the ecosystem this enables.
From our requirements analysis and assessment of existing distributed ledger platforms, we concluded that no existing platform could deliver these end-state principles. For example, the threat models underpinning the designs of traditional distributed databases were unsuitable for our use-case of bringing mutually distrusting legal entities into consensus across the internet; such systems assume a common, fully trusted, administrative domain. The architectures of existing blockchain systems such as Ethereum were unsuitable for our requirement of restricted and carefully specified data sharing at the level of individual legal agreements with legal enforceability.
As a result we present the Corda platform.
\section{Corda}
Corda is distributed ledger software for recording and processing shared data such as contracts, designed to implement the vision contained in this document.
Corda supports smart contracts, matching the definition of Clack, Bakshi, Braine\cite{SCT}; our smart contract is an agreement whose execution is both \textit{automatable} by computer code working with human input and control, and whose rights and obligations, as expressed in legal prose, are legally \textit{enforceable}.
\subsection{Principal Features}
Corda's design was initially driven by the needs of regulated financial institutions but turns out to be far more broadly applicable. It is heavily inspired by blockchain systems, but without the design choices that make traditional blockchains inappropriate for the execution of real-world business transactions.
Our fundamental building block is a \textit{``state object"}, representing a specific instance of a specific agreement, which may be thought of as representing a real-world contract or section of a contract. We use the terms `agreement' and `contract' interchangeably in this paper. This stands in contrast to systems where the data over which participants must reach consensus is the state of an entire ledger or the state of an entire virtual machine. Corda provides three main tools to achieve global distributed consensus:
\begin{itemize}
\item \textit{Smart contract} logic which specifies \textit{constraints} that ensure state transitions are valid according to pre-agreed \textit{rules}, described in \textit{contract code} as part of \textit{CorDapps}.
\item Uniqueness and timestamping services known as \textit{notary pools} to order transactions temporally and eliminate conflicts.
\item A unique component called the \textit{flow framework} which simplifies the process of writing complex multi-step protocols between multiple mutually distrusting parties across the internet.
\end{itemize}
\subsection{Concepts}
We begin with the idea of a global ledger: a reliable single source. However, in our model, it is not the case that transactions and ledger entries are globally visible. A transaction between a group of parties is visible only to them, and to those whose own view of the ledger in the future may depend on verifying the validity of this transaction. This contrasts with platforms based on global broadcast but also with platforms with an approach to privacy that depends on partitioning data into coarse-grained ``confidential contracts" or ``channels". Such designs logically cannot support a global network that supports multiple interoperable applications and assets.
The foundational object in our concept is a \textit{state object}, which is a digital document which records the existence, content and current state of an agreement between two or more parties. It is intended to be shared only with those who have a legitimate reason to see it. To ensure consistency in a global, shared system where not all data is visible to all participants, we rely heavily on secure cryptographic hashes to identify parties and data, and to link states to previous versions to provide chains of provenance. The ledger is defined as a set of immutable state objects.
We talk and think in terms of the `state' of `agreements'. That is: not just a contract between parties but a specific instance, version and current status of that contract. Our objective is to ensure that all parties to an agreement remain in consensus on these facts as they evolve and is intended to interface naturally with real-world legal systems where required. We believe this is the essence of the blockchain concept that is fundamental to its adoption by real-world businesses. Corda ensures that the data held by different actors is, and remains, consistent as operations are applied to update that data. This forms a foundation on which reliable transactions can be built: from simple monetary payments to sophisticated smart contracts.
\begin{figure}[H]
\includegraphics[scale = .4, center]{partiesto}
\caption{In the diagram above, we see a state object representing a deposit of \pounds100 at a commercial bank, owned by a fictional shipping company. The state object refers to the contract code that governs its transitions, which is likely to be written once and reused by huge numbers of states, and can refer by hash to its governing legal prose.}
\end{figure}
\subsection{Consensus}
In Corda, updates are applied using \textit{transactions}, which consume existing state objects and produce new state objects, thus creating chains of provenance. There are two aspects of consensus:
\begin{enumerate}
\item{Transaction validity: parties can reach certainty that a proposed update transaction defining output states is valid by checking that the associated contract code, which is required to be deterministic, verifies successfully and has all the required signatures; and that any transactions to which this transaction refers are also valid.}
\item{Transaction uniqueness: parties can reach certainty that the transaction in question is the unique consumer of all its input states. That is: there exists no other transaction, over which we have previously reached consensus, that consumes any of the same states.}
\end{enumerate}
Parties can agree on transaction validity by independently running the same contract code and validation logic. However, two valid transactions could conceivably exist at the same time and so participants need a way to determine which will be regarded as having come first. This requires a predetermined observer, which should be a group of mutually distrusting participants in a \textit{Notary pool}, of which, as a unique feature of Corda's design, there can be many on the same network providing different characteristics and tradeoffs.
\begin{figure}[H]
\includegraphics[scale = .5, center]{Consensus}
\caption{Consensus over transaction validity is performed only by parties to the transaction in question. Therefore, data is only shared with those parties which are required to see it. Other platforms generally reach consensus at the ledger level. Thus, any given actor in a Corda system sees only a subset of the overall data managed by the system as a whole. We allow arbitrary combinations of actors to participate in the consensus process for any given piece of data.}
\end{figure}
Corda has ``pluggable" uniqueness services. This is to improve privacy, scalability, geographic availability, legal-system compatibility\cite{EUC} and algorithmic agility. A single service may be composed of many mutually untrusting nodes coordinating via a byzantine fault tolerant algorithm, or could be very simple, like a single machine.
It is important to note that these uniqueness services are required only to attest as to whether the states consumed by a given transaction have previously been consumed; they are not required to attest as to the validity of the transaction itself, which is a matter for the parties to the transaction. This means that the uniqueness services are not required to (and, in the general case, will not) see the full contents of any transactions, significantly improving privacy and scalability of the system compared with alternative distributed ledger and blockchain designs. This design decision represents an important choice as to the acceptable tradeoffs in shared ledger architectures and is explored more fully in the technical whitepaper.
\subsection{Business Logic}
Corda enforces business logic through smart contract code. A smart contract in Corda is a pure function whose responsibility is either to accept or reject a proposed transaction and which can be composed from simpler, reusable functions. Each state object specifies the function that must be executed by any transaction which seeks to consume or create that type of state. As such, for any given proposed transaction, one of these functions is executed for each type of state in that transaction, and all must agree that the proposed transaction is permissible given the rules associated with each state. Thus a transaction is valid only if the contract code associated with all contained states agree. It is the responsibility of a transaction proposer to construct a transaction which complies with the constraints associated with each included state object. Contracts thus define the consensus-critical part of the business logic of the ledger, and they are mobile: nodes will download and run contracts inside a sandbox without any review in some deployments, although we envisage the use of signed code for deployments on the global Corda network. This model, whereby one party proposes a transaction and others verify it, is modelled on Bitcoin (albeit heavily extended and generalised) and is the key to Corda's privacy and scalability characteristics.
The virtual machine we have selected for contract execution and validation is the Java Virtual Machine\cite{JVM}, as it has a wealth of existing libraries and a large skill base, and reusing an industry standard makes it easier for banks to reuse their existing code inside contracts. However, we augment it with a custom sandbox that is radically more restrictive than the ordinary JVM sandbox, and it enforces not only security requirements but also deterministic execution. Like Ethereum\cite{Ethereum}, the choice of standardising a bytecode set rather than a language enables users to innovate in contract language design, or reuse well known languages, according to taste. It also makes it easy to directly use contract code from internal applications, once that contract has been reviewed, which should simplify application development considerably.
\subsection{Identity}
Corda enables a broad range of identities to participate in transactions, from institutions to individuals. An identity in Corda is represented by a certificate, signed by a suitable authority, representing a named real-world actor. Participants are expected to protect the private keys associated with their Corda identities, and transactions signed by a participant's key are intended to be legally binding on that participant.
Corda implements its core identity framework through the use of X.509 certificates. This enables Corda to associate a unique ``human readable" entity name (i.e. a legal name) with a public key and network address (IP address). Corda strictly requires uniqueness in this mapping. An entity ``distinguished name" (for example that of a company or legal person) can be associated with at most one public key. This is for non-repudiation purposes so a transaction signature can be legally and unambiguously associated with a legal entity, and to eliminate sources of ambiguity in user interfaces and elsewhere. Upon this foundation can be layered value added services from multiple providers.
The underlying Corda software assumes an identity infrastructure between the participants in the network but makes no assumption as to its sophistication or mode of operation; the global Corda network introduces a specific model, optimised for broad access whilst rigorously ensuring the uniqueness of issued certificates needed to enable formation of real-world legal contracts.
\subsection{Summary of the Corda Model}
The core concepts in our model are:
\begin{itemize}
\item \textit{State objects}, representing an agreement between two or more parties, governed by machine-readable \textit{Contract Code}. This code references, and is intended to implement, portions of human-readable \textit{Legal Prose}. \item \textit{Transactions}, which transition state objects through a lifecycle.
\item A \textit{Flow Framework}, which enables parties to coordinate actions without a central controller.
\end{itemize}
Determinism is achieved, and the amount of shared state required minimized by selectively and decisively restricting the universe of allowable programming techniques.
The combination of state objects (data), Contract Code (constraints on the evolution of that data), the Flow Framework (business logic choreography), any necessary APIs, wallet plugins, and UI components can be thought of a Shared Ledger application, or Corda Distributed Application (\textit{``CorDapp"}). This is the core set of components a contract developer on the platform should expect to build.
\begin{figure}[H]
\includegraphics[scale = .4, center]{cash}
\caption{In the diagram above, we see one of the simplest Corda transactions: a cash issuance transaction. We see the creation of a new Cash state, issued by a commercial bank to a fictional shipping company. The issuing transaction is signed by the issuing bank. From this simple model, significantly more complicated transactions, such as payments, delivery-versus-payment contracts and future-dated obligations can be constructed.}
\end{figure}
\section{The Global Corda Network}
The global Corda network consists of the set of nodes in the world that are configured with common settings such that they can locate each other, with assured identity, to transact directly. The network provides the standards needed to allow Corda nodes to transact flawlessly regardless of who operates them, which applications they run, in which trading groupings they participate, and regardless of the original reason for their deployment. The network enables the formation of an open ecosystem where services can be offered both to specific groups of nodes (``business networks", as distinct from the global Corda network itself), or to all nodes. The network provides an open environment for the deployment of Corda nodes with transaction legality, finality and privacy. The network is underpinned by a governance model intended to ensure it operates in the interest of its participants.
\subsection{Business Networks} \label{businessnetworks}
We anticipate that most deployments of Corda will consist of parties coming together to automate one or more common business processes, and these deployments will be underpinned by specific business models and membership criteria. We refer to such a collection of parties as a business network: a group of Corda network participants who collaborate for a specific business purpose whilst nevertheless being able to participate in other business networks or transact directly with other nodes at the same time and with the same infrastructure. The Corda network provides the capability to form business networks while allowing participants to remain unconstrained in their ability to move assets or form agreements freely.
Business networks define their own membership criteria, privacy requirements, governance, business logic and asset types. Participants can be members of many business networks as well as transacting directly with arbitrary peers.
The global Corda network is intended to enable a large number of overlapping, competing and collaborating Business networks to form, unified by just enough commonality to facilitate interoperability of nodes, and compatibility of assets and other contracts managed by those nodes.
Business networks will play a key role in the health of the Corda network ecosystem providing the mechanism through which CorDapps are rooted in the regulatory and legal aspects of various legal jurisdictions.
\begin{figure}[H]
\includegraphics[scale = .5, center]{corda-connect}
\caption{The global Corda network: seamless interoperability, and frictionless commerce, through shared standards and open governance.}
\end{figure}
\subsection{Principal Components}
The global Corda network:
\begin{itemize}
\item specifies \textit{network parameters} that define the consensus-critical settings nodes must agree on to ensure global compatibility and stability
\item provides \textit{an identity framework} so firms can enter into real-world contracts with confidence
\item recommends \textit{consensus pools} that provide consistent, transparent and resilient uniqueness consensus services
\item enables \textit{oracles} that provide information services across the network
\item facilitates \textit{fiat currencies} and \textit{native digital tokens} in a standard representation for seamless transfer of value
\item relies on \textit{open governance} that represents the stakeholders of the Corda ecosystem
\end{itemize}
\subsubsection{Governance}
Whilst the overwhelming majority of power lies with Corda users themselves, or the governors of their chosen business networks, some residual control unavoidably lies in the specification of the standards and operation of associated technical services upon which all participants rely. This means the success of the Corda network depends on the extent to which it is operated in the interests of its broad base of stakeholders, and is responsive to their needs.
For example, in Bitcoin, the size of the maximum permitted block is ultimately arbitrary within some boundaries. Yet it is an arbitrary figure upon which all participants in \textit{the} Bitcoin network must agree. In the absence of a well thought through and administered governance process to decide on settings such as this, operated by and for the participants in the network, gridlock or conflict can be the result. To avoid a similar outcome, the purpose of the Corda network's governance process is to ensure it is always clear how such disputes can be mediated, by whom and in whose interests.
However, in the end-state of this vision, a huge number of nodes and business networks will be transacting significant volumes of business and value across the global Corda network. So it is not sufficient merely to \textit{claim} that the Corda network will be operated for the benefit of its users. It must be the case that it \textit{will} be so operated. Channeling Google's ``don't be evil" mantra, our vision for the Corda network is that it be operated so that it \textit{can't be evil}.
To that end, an open governance model will be established in a series of stages and effected through a ``Corda network governing body", whose composition is intended to be representative of the users of the Corda network. The committee's responsibilities will include: to take over the definition, management and updating of network parameters; to agree rules for the issuance of identity certificates; and to set common standards for notary consensus pools.
\subsubsection{Network Parameters}
Corda users need to agree on certain technical parameters that must be the same for all nodes in order to communicate successfully. Examples include the maximum-allowable message size, how quickly members agree to upgrade their nodes when a new feature is introduced, and how to use privacy features like Intel SGX. Although these sound like unimportant technical points, they are configuration parameters of the Corda software precisely \textit{because} they are things upon which reasonable people could disagree, as happened with the Bitcoin block size debate. As such, the parameter-setting process could rapidly become politicised. It is, therefore, a priority to move responsibility for setting, maintaining and updating these parameters to the Corda network governing body once established.
\subsubsection{Identity}
Corda manages real contracts between real people and firms. So we need a way for users to know they really are transacting with who they think they are. This requires there to be a unique mapping from real-world identity to network identity (public key). As such, it is an unavoidable consequence that an important network parameter is which identity authority or authorities to rely on.
However, it is our vision that access to the Corda network be available to as broad a range of participants as possible; restrictions driven by business model, regulation or geography can and should be implemented at the level of individual business networks, not at the level of the overall platform. Indeed, it is important to emphasise that operators of business networks are able, and indeed are expected, to independently verify participants' identities before admitting them to their business networks. It is this extra layer of checking that allows the Corda network-level identity framework to be as light as possible whilst ensuring uniqueness of identity certificates.
To this end, the Corda network will launch with a simple identity service that will subsequently be upgraded to enable a broader range of approaches. One such approach is likely to be one where participants ``bring their own identity" in the form of signed attestations from a set of trusted identity issuers/attestors. We envisage a model whereby the automatic issuance of a Corda network certificate can be triggered for any party with the requisite attestations, subject only to the constraint that any resulting identity certificate issued on the global Corda network is unique, both for the distinguished name and the public keys or any other unique identifier that may be used in the future.
One of the responsibilities of the Corda network governing body will be to oversee this process, including setting standards for attestors. We expect this deliberately minimal identity framework to catalyse the formation of a rich ecosystem of identity value added services through third party specialist providers which can layer on top of this foundation to meet the varying needs of different applications.
\subsubsection{Consensus}
Corda, like all blockchain platforms, works on the principle of \textit{consensus}. Parties know a transaction has been confirmed when the appointed consensus service has processed it. Uniquely, Corda is architected to support volumes in excess of billions of daily transactions across a single network. To achieve this, Corda allows for a range of consensus services (notary pools) optimised for different purposes on the same network, including on the global Corda network described in this chapter.
Participants can verify the integrity of these choices through a variety of mechanisms: some notary pools will provide transparency (in a way analogous to public blockchains today). Others may remotely attest that they are running a particular algorithm in a secure enclave. To aid network participants in configuring their nodes and to maximise interoperability, a responsibility of the Corda network governing body is to establish and maintain a list of notary pools which are considered to be operated to sufficient standards of integrity. Participants are encouraged to configure their nodes to accept historical transactions that have been notarised by pools on this shared list to maximise compatibility of transactions across the network. Participants can, of course, choose any notary pool to notarise their own transactions; this shared list is to enable historic transactions to be easily combined with new transactions to enable free movement of data and assets around the network; the fundamental vision and unique characteristic of Corda.
\subsubsection {The Corda Network Economic Model}
The success of Corda and the global Corda network depends, in part, on there being a vibrant ecosystem of participants on the network: users; designers, governors and operators of business networks; application developers; providers of notary nodes; oracle services and infrastructure providers, many of whom will need to pay and be paid.
The global Corda network will support a digital representation of fiat money, the first broad-access blockchain network to do so. However, access to this facility will necessarily be subject to the thicket of regulation that attaches to the global payments system.
Therefore, the Corda network may also offer support for an asset which is native to the platform and which can be far more broadly held, subject to regulatory approval.
The Corda network can encourage the emergence of application-specific native assets or tokens, utilising a common standard for representation, storage and exchange of such tokens.
\section{Comparisons with Other Platforms}
Corda's design is inspired by previous work, including that introduced in the writings of Todd Boyle and Ian Grigg on triple entry accounting\cite{Triple}, and aspects of existing distributed ledger platforms such as Bitcoin\cite{Bitcoin} and Ethereum. We can compare our vision with two alternative models: the public blockchains and the enterprise blockchains.
\begin{figure}[H]
\includegraphics[scale = .5, center]{platform-comparisons}
\caption{Corda delivers the promise of interoperable applications in an enterprise context (right) without the disadvantages of the fully public Ethereum network (left) or the non-interoperable, expensive standalone networks being built by first-generation enterprise blockchains (middle).}
\end{figure}
\subsection{Comparisons to Public Blockchains: Bitcoin and Ethereum}
The first diagram above is the model enabled by Bitcoin and delivered by Ethereum: a single global network upon which large numbers of users and applications can transact and interoperate. It is a compelling vision and has rightly captured the imagination of many enthusiasts. Unfortunately, it suffers from severe drawbacks when applied to the enterprise domain: all data is shared with all parties, with obvious privacy and scalability implications, and no realistic prospect in sight of a truly workable solution. For this reason and others it is proving exceedingly difficult to adapt or adopt the public Ethereum architecture for enterprise use-cases. Specific comparisons to Bitcoin and Ethereum now follow:
\subsubsection{Comparisons to Bitcoin}
Corda has some significant similarities to Bitcoin:
\begin{itemize}
\item{Immutable states that are consumed and created by transactions is the same.}
\item{Transactions have multiple inputs and outputs. Bitcoin sometimes refers to the ledger as the unspent transaction output set (UTXO set) as a result. However, the Corda model is a significant generalisation of this concept, to support arbitrarily complex data models.}
\item{A contract is pure function; contracts do not have storage or the ability to interact with anything. Given the same transaction, a contract's ``verify" function always yields exactly the same result.}
\end{itemize}
However, a Bitcoin transaction has a single, rigid data format and can hold very little data apart from quantities of bitcoin and associated spending rules (script). Some people have been known to try and work around this limitation by embedding data in semi-standardized places in the contract code so the data can be extracted through pattern matching but this is a poor approach.
By contrast, our states can include arbitrary typed data. In addition, our transactions invoke not only input contracts but also the contracts of the outputs. A Bitcoin transaction's acceptance is controlled only by the contract code in the consumed input states. We use the term ``contract" to refer to a bundle of business logic that may handle various different tasks, beyond transaction verification. For instance, currently our contracts also include code for creating valid transactions (this is often called ``wallet code" in Bitcoin).
A Bitcoin script can only be given a fixed set of byte arrays as the input. This means there is no way for a contract to examine the structure of the entire transaction, which severely limits what contracts can do. Our contracts are Turing-complete and can be written in any ordinary programming language that targets the JVM.
Corda allows arbitrarily-precise time-bounds to be specified in transactions (which must be attested to by a trusted timestamper) rather than relying on the time at which a block happens to be mined. This is important given that many contract types we envisage supporting require precision in timing and because our primary consensus implementations use block-free conflict resolution algorithms. It should be noted that Corda does not utilise Proof of Work or have a concept of ``mining".
\subsubsection{Comparisons to Ethereum}
Like Ethereum, CorDapp code runs inside a relatively powerful virtual machine and can contain complex logic. Non-assembly based programming languages can be used for contract programming. They are both intended for the modelling of many different kinds of financial contracts.
However, the term ``contract" in Ethereum refers to an instantiation of a program that is replicated and maintained by every participating node. This instantiation is very much like an object in an Object Oriented program: it can receive and send messages, update local storage and so on. In contrast, our implementation of the smart contract in code refers to a set of functions, only one of which is a part of keeping the system synchronised (the \textit{verify} function). That function is pure and stateless (i.e., it may not interact with any other part of the system whilst executing). As contracts do not have any kind of mutable storage, there is no notion of a ``message". This model means it is possible to analyse a Corda transaction statically and not only determine whether it can be processed in parallel with other transactions but know what data must be available on the processing node, enabling both higher throughput and finer-grained data privacy.
\subsection{Comparisons to Enterprise Blockchains: Enterprise Ethereum and Hyperledger Fabric}
The second model, drawn in the centre of the diagram above is the inevitable result of efforts that have tried to apply the Ethereum architecture to enterprise use-cases. With Enterprise Ethereum platforms such as Quorum as well as clones of Ethereum such as Fabric, we see a wholesale retreat from the vision: each business solution is deployed as an independent, standalone network that is incompatible with all the others, completely undermining a key foundation of the value proposition. Assets earned on one of these networks will not be easily usable on another. Each silo will require its own identity, consensus and governance processes. The firms who deploy nodes to connect to these networks will be lumbered with expensively duplicated and incompatible infrastructure in their data-centres. And the industry will then have to spend millions of dollars inventing ways to make these isolated networks interoperate with each other. This is the complete opposite of what the enterprise blockchain revolution is intended to deliver.
\section{Conclusion}
In contrast to most existing distributed ledger and blockchain platforms today, Corda was built with the explicit purpose of recording and enforcing business agreements between trading partners. As such, it takes a unique approach to data distribution and transaction semantics while emphasising the features of distributed ledgers which are attractive to firms, namely reliable execution of contracts in an automatable and enforceable fashion.
The Corda vision, enabled by the global Corda network, enables multiple applications and services to run across a common layer of identity, consensus, business logic, data definitions, and governance. Firms can deploy one Corda node infrastructure which can then transact with multiple different groups of trading partners, for different purposes, all at the same time and whilst providing strong privacy. Multiple different competing notary consensus pools, comprised of a range of providers, will be available; data and other oracle services can be deployed once and used by multiple applications; and the entire ecosystem will be governed by and for its users through a transparent and open process. Multiple different applications, designed for completely different purposes, can be deployed today to solve specific problems and yet, in the future, contracts and other information managed by these applications could be combined in new ways for purposes unimaginable today.
\bibliographystyle{unsrt}
\bibliography{Ref}
\end{document}

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -26,7 +26,6 @@ import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.contracts.asset.CommodityContract
import java.math.BigDecimal
import java.time.DayOfWeek
import java.time.LocalDate

View File

@ -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.finance.contracts.asset
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.contracts.Commodity
import net.corda.finance.utils.sumCommodities
import net.corda.finance.utils.sumCommoditiesOrNull
import net.corda.finance.utils.sumCommoditiesOrZero
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Commodity
//
/**
* A commodity contract represents an amount of some commodity, tracked on a distributed ledger. The design of this
* contract is intentionally similar to the [Cash] contract, and the same commands (issue, move, exit) apply, the
* differences are in representation of the underlying commodity. Issuer in this context means the party who has the
* commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
*
* This is an early stage example contract used to illustrate non-cash fungible assets, and is likely to change significantly
* in future.
*/
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
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<Issued<Commodity>>,
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: AbstractParty
) : FungibleAsset<Commodity> {
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: AbstractParty)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val exitKeys: Set<PublicKey> = Collections.singleton(owner.owningKey)
override val participants = listOf(owner)
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Commodity>>, newOwner: AbstractParty): FungibleAsset<Commodity>
= 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<out Contract>? = 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<Issued<Commodity>>) : 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<Commands.Issue>().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<Commands.Exit>(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<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: CommandWithParties<Commands.Issue>,
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<CommodityContract.Commands>()
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<CommandWithParties<CommandData>>): List<CommandWithParties<Commands>>
= 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<Commodity>, 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<Issued<Commodity>>, owner: AbstractParty, notary: Party)
= generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue())
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Commodity>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Commodity>>) = Commands.Exit(amount)
override fun generateMoveCommand() = Commands.Move()
}

View File

@ -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 <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<Fun
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().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<ContractState>.sumCommodities() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
@Suppress("unused")
fun Iterable<ContractState>.sumCommoditiesOrNull() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency if there are none. */
fun Iterable<ContractState>.sumCommoditiesOrZero(currency: Issued<Commodity>) = filterIsInstance<CommodityContract.State>().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.

View File

@ -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()
}

View File

@ -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<CordaX500Name> = emptySet(), // TODO SNI? Or we need a notion of node's network identity?
val connectorFactoryClassName: String = NettyConnectorFactory::class.java.name
) : ConnectionDirection()
}
import java.nio.file.Path
/** Class to set Artemis TCP configuration options. */
class ArtemisTcpTransport {
companion object {
const val VERIFY_PEER_LEGAL_NAME = "corda.verifyPeerCommonName"
/**
* Corda supported TLS schemes.
* <p><ul>
@ -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<String, Any?>(
// 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<NetworkHostAndPort>,
config: SSLConfiguration?,
enableSSL: Boolean = true): List<TransportConfiguration> {
val tcpTransports = ArrayList<TransportConfiguration>(hostAndPortList.size)
hostAndPortList.forEach {
tcpTransports.add(tcpTransport(direction, it, config, enableSSL))
}
fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map {
rpcConnectorTcpTransport(it, config, enableSSL)
}
return tcpTransports
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
}
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
}
}
}
data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String)

View File

@ -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()

View File

@ -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."

View File

@ -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

View File

@ -253,7 +253,6 @@ class AMQPBridgeTest {
doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn(artemisAddress).whenever(it).p2pAddress
doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration
}
artemisConfig.configureWithDevSSLCertificate()

View File

@ -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<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration
doReturn(true).whenever(it).crlCheckSoftFail
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}

View File

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

View File

@ -34,7 +34,7 @@ operator fun Config.plus(overrides: Map<String, Any?>): 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,

View File

@ -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<CertChainPolicyConfig>,
@Deprecated("Do not configure")
override val certificateChainCheckPolicies: List<CertChainPolicyConfig> = emptyList(),
override val devMode: Boolean = false,
override val noLocalShell: Boolean = false,
override val devModeOptions: DevModeOptions? = null,
@ -233,9 +239,9 @@ data class NodeConfigurationImpl(
private val logger = loggerFor<NodeConfigurationImpl>()
}
override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, crlCheckSoftFail))
override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, BrokerRpcSslOptions(baseDirectory / "certificates" / "nodekeystore.jks", keyStorePassword))
private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: SSLConfiguration): NodeRpcOptions {
private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: BrokerRpcSslOptions): NodeRpcOptions {
return when {
explicitAddress != null -> {
require(settings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." }
@ -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<String>) {
val certificateChainCheckPolicy: CertificateChainCheckPolicy
get() {

View File

@ -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()

View File

@ -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
}

View File

@ -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)

View File

@ -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<AppConfigurationEntry> {
val options = mapOf(NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks)
val options = mapOf(
BrokerJaasLoginModule.P2P_SECURITY_CONFIG to P2PJaasConfig(keyStore, trustStore),
BrokerJaasLoginModule.NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
}
}
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig)
}
private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) =
ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL)
}
/**
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
* the CN within that is their legal name.
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
*/
class NodeLoginModule : LoginModule {
companion object {
// Include forbidden username character to prevent name clash with any RPC usernames
const val PEER_ROLE = "SystemRoles/Peer"
const val NODE_ROLE = "SystemRoles/Node"
const val VERIFIER_ROLE = "SystemRoles/Verifier"
const val CERT_CHAIN_CHECKS_OPTION_NAME = "CertChainChecks"
private val log = contextLogger()
}
private var loginSucceeded: Boolean = false
private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler
private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
private lateinit var verifierCertCheck: CertificateChainCheckPolicy.Check
private val principals = ArrayList<Principal>()
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME])
peerCertCheck = certChainChecks[PEER_ROLE]!!
nodeCertCheck = certChainChecks[NODE_ROLE]!!
verifierCertCheck = certChainChecks[VERIFIER_ROLE]!!
}
override fun login(): Boolean {
val nameCallback = NameCallback("Username: ")
val certificateCallback = CertificateCallback()
try {
callbackHandler.handle(arrayOf(nameCallback, certificateCallback))
} catch (e: IOException) {
throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user")
}
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val certificates = certificateCallback.certificates
log.debug { "Processing login for $username" }
try {
val validatedUser = when (determineUserRole(certificates, username)) {
PEER_ROLE -> authenticatePeer(certificates)
NODE_ROLE -> authenticateNode(certificates)
VERIFIER_ROLE -> authenticateVerifier(certificates)
else -> throw FailedLoginException("Peer does not belong on our network")
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
} catch (exception: FailedLoginException) {
log.warn("$exception")
throw exception
}
}
private fun authenticateNode(certificates: Array<javax.security.cert.X509Certificate>): String {
nodeCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(NODE_ROLE)
return certificates.first().subjectDN.name
}
private fun authenticateVerifier(certificates: Array<javax.security.cert.X509Certificate>): String {
verifierCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(VERIFIER_ROLE)
return certificates.first().subjectDN.name
}
private fun authenticatePeer(certificates: Array<javax.security.cert.X509Certificate>): String {
peerCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(PEER_ROLE)
return certificates.first().subjectDN.name
}
private fun determineUserRole(certificates: Array<javax.security.cert.X509Certificate>?, username: String): String? {
fun requireTls() = require(certificates != null) { "No TLS?" }
return when (username) {
PEER_USER -> {
requireTls()
PEER_ROLE
}
NODE_USER -> {
requireTls()
NODE_ROLE
}
VerifierApi.VERIFIER_USERNAME -> {
requireTls()
VERIFIER_ROLE
}
else -> null
}
}
override fun commit(): Boolean {
val result = loginSucceeded
if (result) {
subject.principals.addAll(principals)
}
clear()
return result
}
override fun abort(): Boolean {
clear()
return true
}
override fun logout(): Boolean {
subject.principals.removeAll(principals)
return true
}
private fun clear() {
loginSucceeded = false
}
}

View File

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

View File

@ -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()

View File

@ -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()
}

View File

@ -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<CertChainPolicyConfig>,
private val maxMessageSize: Int,
private val jmxEnabled: Boolean = false,
private val baseDirectory: Path) : ArtemisBroker {
private val baseDirectory: Path,
private val nodeConfiguration: SSLConfiguration,
private val shouldStartLocalShell: Boolean) : ArtemisBroker {
companion object {
private val logger = loggerFor<ArtemisRpcBroker>()
fun withSsl(address: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker {
return ArtemisRpcBroker(address, null, sslOptions, true, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
return ArtemisRpcBroker(address, adminAddress, sslOptions, true, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
}
fun withoutSsl(address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker {
return ArtemisRpcBroker(address, adminAddress, sslOptions, false, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker {
return ArtemisRpcBroker(address, adminAddress, null, false, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell)
}
}
@ -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<AppConfigurationEntry> {
val options = mapOf(
NodeLoginModule.LOGIN_LISTENER_ARG to loginListener,
NodeLoginModule.SECURITY_MANAGER_ARG to securityManager,
NodeLoginModule.USE_SSL_ARG to useSsl,
NodeLoginModule.CERT_CHAIN_CHECKS_ARG to certChecks)
RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl),
NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
)
return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options))
}
}
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
return ActiveMQJAASSecurityManager(BrokerJaasLoginModule::class.java.name, securityConfig)
}
}
typealias LoginListener = (String) -> Unit
private fun <RESULT> CompletableFuture<RESULT>.toObservable() = Observable.from(this)

View File

@ -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<NodeLoginModule>()
}
private var loginSucceeded: Boolean = false
private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler
private lateinit var securityManager: RPCSecurityManager
private lateinit var loginListener: LoginListener
private var useSsl: Boolean? = null
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
private lateinit var rpcCertCheck: CertificateChainCheckPolicy.Check
private val principals = ArrayList<Principal>()
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
securityManager = uncheckedCast(options[SECURITY_MANAGER_ARG])
loginListener = uncheckedCast(options[LOGIN_LISTENER_ARG])
useSsl = options[USE_SSL_ARG] as Boolean
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_ARG])
nodeCertCheck = certChainChecks[NODE_ROLE]!!
rpcCertCheck = certChainChecks[RPC_ROLE]!!
}
override fun login(): Boolean {
val nameCallback = NameCallback("Username: ")
val passwordCallback = PasswordCallback("Password: ", false)
val certificateCallback = CertificateCallback()
try {
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
} catch (e: IOException) {
throw LoginException(e.message)
} catch (e: UnsupportedCallbackException) {
throw LoginException("${e.message} not available to obtain information from user")
}
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
val certificates = certificateCallback.certificates ?: emptyArray()
if (rpcCertCheck is CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck) {
(rpcCertCheck as CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck).username = username
}
log.debug("Logging user in")
try {
val role = determineUserRole(certificates, username, useSsl!!)
val validatedUser = when (role) {
NodeLoginModule.NODE_ROLE -> {
authenticateNode(certificates)
NODE_USER
}
RPC_ROLE -> {
authenticateRpcUser(username, Password(password), certificates, useSsl!!)
username
}
else -> throw FailedLoginException("Peer does not belong on our network")
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
} catch (exception: FailedLoginException) {
log.warn("$exception")
throw exception
}
}
private fun authenticateNode(certificates: Array<X509Certificate>) {
nodeCertCheck.checkCertificateChain(certificates)
principals += RolePrincipal(NodeLoginModule.NODE_ROLE)
}
private fun authenticateRpcUser(username: String, password: Password, certificates: Array<X509Certificate>, useSsl: Boolean) {
if (useSsl) {
rpcCertCheck.checkCertificateChain(certificates)
// no point in matching username with CN because companies wouldn't want to provide a certificate for each user
}
securityManager.authenticate(username, password)
loginListener(username)
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses
}
private fun determineUserRole(certificates: Array<X509Certificate>, username: String, useSsl: Boolean): String? {
return when (username) {
ArtemisMessagingComponent.NODE_USER -> {
require(certificates.isNotEmpty()) { "No TLS?" }
NodeLoginModule.NODE_ROLE
}
else -> {
if (useSsl) {
require(certificates.isNotEmpty()) { "No TLS?" }
}
return RPC_ROLE
}
}
}
override fun commit(): Boolean {
val result = loginSucceeded
if (result) {
subject.principals.addAll(principals)
}
clear()
return result
}
override fun abort(): Boolean {
clear()
return true
}
override fun logout(): Boolean {
subject.principals.removeAll(principals)
return true
}
private fun clear() {
loginSucceeded = false
}
}

View File

@ -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<String, Set<Role>>) : SecuritySettingPlugin {
internal class RolesAdderOnLogin(val systemUsers: List<String> = emptyList(), val source: (String) -> Pair<String, Set<Role>>) : SecuritySettingPlugin {
private lateinit var repository: RolesRepository
fun onLogin(username: String) {
@ -34,6 +34,8 @@ internal class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>
override fun setSecurityRepository(repository: RolesRepository) {
this.repository = repository
// This dynamically adds roles to the specified system users as soon as the repository is initialized.
systemUsers.forEach(::onLogin)
}
override fun stop() = this

View File

@ -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 {

View File

@ -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,

View File

@ -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<CertChainPolicyConfig> = listOf()
val artemisBroker: ArtemisBroker = if (useSslForBroker) {
ArtemisRpcBroker.withSsl(address, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
ArtemisRpcBroker.withSsl(nodeSSlconfig, address, adminAddress, brokerSslOptions!!, securityManager, maxMessageSize, jmxEnabled, baseDirectory, false)
} else {
ArtemisRpcBroker.withoutSsl(address, adminAddress, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
ArtemisRpcBroker.withoutSsl(nodeSSlconfig, address, adminAddress, securityManager, maxMessageSize, jmxEnabled, baseDirectory, false)
}
artemisBroker.use { broker ->
broker.start()
RPCMessagingClient(brokerSslOptions, broker.addresses.admin, maxMessageSize).use { server ->
InternalRPCMessagingClient(nodeSSlconfig, adminAddress, maxMessageSize, CordaX500Name("MegaCorp", "London", "GB")).use { server ->
server.start(TestRpcOpsImpl(), securityManager, broker.serverControl)
val client = RPCClient<TestRpcOps>(tcpTransport(ConnectionDirection.Outbound(), broker.addresses.primary, clientSslOptions))
val client = RPCClient<TestRpcOps>(rpcConnectorTcpTransport(broker.addresses.primary, clientSslOptions))
clientConnectionSpy {
client.start(TestRpcOps::class.java, user.username, user.password).use { connection ->
connection.proxy.apply {
val greeting = greet("Frodo")
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 <OPS : RPCOps> RPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) {
private fun <OPS : RPCOps> InternalRPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) {
apply {
start(ops, securityManager)
start2(brokerControl)
init(ops, securityManager)
start(brokerControl)
}
}
private fun <EXCEPTION : Exception> expectExceptionOfType(exceptionType: KClass<EXCEPTION>): (() -> Unit) -> Unit {
return { action -> assertThatThrownBy { action.invoke() }.isInstanceOf(exceptionType.java) }
}
interface TestRpcOps : RPCOps {
fun greet(name: String): String
}
@ -223,4 +143,7 @@ class ArtemisRpcTests {
override val protocolVersion: Int = 1
}
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
}

View File

@ -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<User>
/** The location of the node's base directory **/

View File

@ -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
}

View File

@ -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<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.corda.rpcOptions.address!!
val client = if (config.corda.rpcOptions.useSsl) {
createCordaRPCClientWithSsl(rpcAddress, sslConfiguration = config.corda.rpcOptions.sslConfig)
} else {
CordaRPCClient(rpcAddress)
}
val rpcAddress = config.corda.rpcOptions.address
val client = createCordaRPCClientWithInternalSslAndClassLoader(config.corda.rpcOptions.adminAddress, sslConfiguration = config.corda)
val connectionFuture = poll(executorService, "RPC connection") {
try {
config.corda.rpcUsers[0].run { client.start(username, password) }
@ -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<String, Any>("rpcSettings.address" to portAllocation.nextHostAndPort().toString())
cordform.config.apply {
if (!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) {
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
}
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
}
overrides
} else {
val overrides = mutableMapOf<String, Any>()
cordform.config.apply {
if ((!hasPath("rpcSettings.useSsl") || !getBoolean("rpcSettings.useSsl")) && !hasPath("rpcSettings.adminAddress")) {
overrides += "rpcSettings.adminAddress" to portAllocation.nextHostAndPort().toString()
}
overrides += "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<Pair<String, String>> {
private fun inheritFromParentProcess(): Iterable<Pair<String, String>> {
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()

View File

@ -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<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(VerifierType.InMemory).whenever(it).verifierType
doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).p2pMessagingRetry
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec

View File

@ -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<I> {
return driverDSL.executorService.fork {
val client = RPCClient<I>(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration)
val client = RPCClient<I>(ArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration)
val connection = client.start(rpcOpsClass, username, password, externalTrace)
driverDSL.shutdownManager.registerShutdown {
connection.close()

View File

@ -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<String, Any> {
fun BrokerRpcSslOptions.useSslRpcOverrides(): Map<String, String> {
return mapOf(
"rpcSettings.useSsl" to "true",
"rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(),
"rpcSettings.ssl.keyStorePassword" to keyStorePassword,
"rpcSettings.ssl.trustStorePassword" to trustStorePassword,
"rpcSettings.ssl.crlCheckSoftFail" to true
"rpcSettings.ssl.keyStorePath" to keyStorePath.toAbsolutePath().toString(),
"rpcSettings.ssl.keyStorePassword" to keyStorePassword
)
}
@ -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<KeyPair, X509Certificate> {
val rpcKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, rpcKeyPair)
return Pair(rpcKeyPair, selfSignCert)
}
fun saveToKeyStore(keyStorePath: Path, rpcKeyPair: KeyPair, selfSignCert: X509Certificate, password: String = "password"): Path {
val keyStore = loadOrCreateKeyStore(keyStorePath, password)
keyStore.addOrReplaceKey("Key", rpcKeyPair.private, password.toCharArray(), arrayOf(selfSignCert))
keyStore.save(keyStorePath, password)
return keyStorePath
}
fun saveToTrustStore(trustStorePath: Path, selfSignCert: X509Certificate, password: String = "password"): Path {
val trustStore = loadOrCreateKeyStore(trustStorePath, password)
trustStore.addOrReplaceCertificate("Key", selfSignCert)
trustStore.save(trustStorePath, password)
return trustStorePath
}

View File

@ -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<Issued<Commodity>>, 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<Commodity>, issuerServices: ServiceHub, issuedBy: PartyAndReference): Vault<CommodityContract.State> {
fun fillWithSomeTestCommodity(amount: Amount<Commodity>, issuerServices: ServiceHub, issuedBy: PartyAndReference): Vault<CommodityState> {
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<Issued<Commodity>>,
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: AbstractParty
) : FungibleAsset<Commodity> {
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: AbstractParty)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val exitKeys: Set<PublicKey> = Collections.singleton(owner.owningKey)
override val participants = listOf(owner)
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Commodity>>, newOwner: AbstractParty): FungibleAsset<Commodity>
= 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))
}

View File

@ -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<SSHServerTest.FlowICanRun>(),
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/))
withCertificates { server, client, createSelfSigned, createSignedBy ->
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
// truststore needs to contain root CA for how the driver works...
server.keyStore["cordaclienttls"] = rootCertificate
server.trustStore["cordaclienttls"] = rootCertificate
server.trustStore["shell"] = markCertificate
val (keyPair, cert) = createKeyPairAndSelfSignedCertificate()
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
client.keyStore["shell"] = markCertificate
client.trustStore["cordaclienttls"] = rootCertificate
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()
}
}
}

View File

@ -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

View File

@ -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("")
}

View File

@ -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(

View File

@ -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"),

View File

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