Node verifies the peer it connects to by checking its TLS common name

This commit is contained in:
Shams Asari 2016-12-22 14:48:27 +00:00
parent 32e1c291d1
commit 08e391579c
41 changed files with 939 additions and 1063 deletions

View File

@ -6,5 +6,8 @@ trustStorePassword : "trustpass"
artemisAddress : "localhost:31337" artemisAddress : "localhost:31337"
webAddress : "localhost:31339" webAddress : "localhost:31339"
extraAdvertisedServiceIds: "corda.interest_rates" extraAdvertisedServiceIds: "corda.interest_rates"
networkMapAddress : "localhost:12345" networkMapService : {
address : "localhost:12345"
legalName : "Network Map Service"
}
useHTTPS : false useHTTPS : false

View File

@ -6,5 +6,8 @@ trustStorePassword : "trustpass"
artemisAddress : "localhost:31338" artemisAddress : "localhost:31338"
webAddress : "localhost:31340" webAddress : "localhost:31340"
extraAdvertisedServiceIds: "corda.interest_rates" extraAdvertisedServiceIds: "corda.interest_rates"
networkMapAddress : "localhost:12345" networkMapService : {
address : "localhost:12345"
legalName : "Network Map Service"
}
useHTTPS : false useHTTPS : false

View File

@ -1,185 +0,0 @@
package net.corda.core.crypto
import sun.security.util.HostnameChecker
import java.net.InetAddress
import java.net.Socket
import java.net.UnknownHostException
import java.security.KeyStore
import java.security.Provider
import java.security.Security
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.ConcurrentHashMap
import javax.net.ssl.*
/**
* Call this to change the default verification algorithm and this use the WhitelistTrustManager
* implementation. This is a work around to the fact that ArtemisMQ and probably many other libraries
* don't correctly configure the SSLParameters with setEndpointIdentificationAlgorithm and thus don't check
* that the certificate matches with the DNS entry requested. This exposes us to man in the middle attacks.
* The issue has been raised with ArtemisMQ: https://issues.apache.org/jira/browse/ARTEMIS-656
*/
fun registerWhitelistTrustManager() {
if (Security.getProvider("WhitelistTrustManager") == null) {
WhitelistTrustManagerProvider.register()
}
// Forcibly change the TrustManagerFactory defaultAlgorithm to be us
// This will apply to all code using TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
// Which includes the standard HTTPS implementation and most other SSL code
// TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)) will
// allow access to the original implementation which is normally "PKIX"
Security.setProperty("ssl.TrustManagerFactory.algorithm", "whitelistTrustManager")
}
/**
* Custom Security Provider that forces the TrustManagerFactory to be our custom one.
* Also holds the identity of the original TrustManager algorithm so
* that we can delegate most of the checking to the proper Java code. We simply add some more checks.
*
* The whitelist automatically includes the local server DNS name and IP address
*
*/
object WhitelistTrustManagerProvider : Provider("WhitelistTrustManager",
1.0,
"Provider for custom trust manager that always validates certificate names") {
val originalTrustProviderAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm")
private val _whitelist = ConcurrentHashMap.newKeySet<String>()
val whitelist: Set<String> get() = _whitelist.toSet() // The acceptable IP and DNS names for clients and servers.
init {
// Add ourselves to whitelist as currently we have to connect to a local ArtemisMQ broker
val host = InetAddress.getLocalHost()
addWhitelistEntry(host.hostName)
}
/**
* Security provider registration function for WhitelistTrustManagerProvider
*/
fun register() {
Security.addProvider(WhitelistTrustManagerProvider)
// Register our custom TrustManagerFactorySpi
put("TrustManagerFactory.whitelistTrustManager", "net.corda.core.crypto.WhitelistTrustManagerSpi")
}
/**
* Adds an extra name to the whitelist if not already present
* If this is a new entry it will internally request a DNS lookup which may block the calling thread.
*/
fun addWhitelistEntry(serverName: String) {
if (!_whitelist.contains(serverName)) { // Safe as we never delete from the set
addWhitelistEntries(listOf(serverName))
}
}
/**
* Adds a list of servers to the whitelist and also adds their fully resolved name/ip address after DNS lookup
* If the server name is not an actual DNS name this is silently ignored.
* The DNS request may block the calling thread.
*/
fun addWhitelistEntries(serverNames: List<String>) {
_whitelist.addAll(serverNames)
for (name in serverNames) {
try {
val addresses = InetAddress.getAllByName(name).toList()
_whitelist.addAll(addresses.map { y -> y.canonicalHostName })
_whitelist.addAll(addresses.map { y -> y.hostAddress })
} catch (ex: UnknownHostException) {
// Ignore if the server name is not resolvable e.g. for wildcard addresses, or addresses that can only be resolved externally
}
}
}
}
/**
* Registered TrustManagerFactorySpi
*/
class WhitelistTrustManagerSpi : TrustManagerFactorySpi() {
// Get the original implementation to delegate to (can't use Kotlin delegation on abstract classes unfortunately).
val originalProvider = TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)
override fun engineInit(keyStore: KeyStore?) {
originalProvider.init(keyStore)
}
override fun engineInit(managerFactoryParameters: ManagerFactoryParameters?) {
originalProvider.init(managerFactoryParameters)
}
override fun engineGetTrustManagers(): Array<out TrustManager> {
val parent = originalProvider.trustManagers.first() as X509ExtendedTrustManager
// Wrap original provider in ours and return
return arrayOf(WhitelistTrustManager(parent))
}
}
/**
* Our TrustManager extension takes the standard certificate checker and first delegates all the
* chain checking to that. If everything is well formed we then simply add a check against our whitelist
*/
class WhitelistTrustManager(val originalProvider: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
// Use same Helper class as standard HTTPS library validator
val checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS)
private fun checkIdentity(hostname: String?, cert: X509Certificate) {
// Based on standard code in sun.security.ssl.X509TrustManagerImpl.checkIdentity
// if IPv6 strip off the "[]"
if ((hostname != null) && hostname.startsWith("[") && hostname.endsWith("]")) {
checker.match(hostname.substring(1, hostname.length - 1), cert)
} else {
checker.match(hostname, cert)
}
}
/**
* scan whitelist and confirm the certificate matches at least one entry
*/
private fun checkWhitelist(cert: X509Certificate) {
for (whiteListEntry in WhitelistTrustManagerProvider.whitelist) {
try {
checkIdentity(whiteListEntry, cert)
return // if we get here without throwing we had a match
} catch(ex: CertificateException) {
// Ignore and check the next entry until we find a match, or exhaust the whitelist
}
}
throw CertificateException("Certificate not on whitelist ${cert.subjectDN}")
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
originalProvider.checkClientTrusted(chain, authType, socket)
checkWhitelist(chain[0])
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
originalProvider.checkClientTrusted(chain, authType, engine)
checkWhitelist(chain[0])
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
originalProvider.checkClientTrusted(chain, authType)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
originalProvider.checkServerTrusted(chain, authType, socket)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
originalProvider.checkServerTrusted(chain, authType, engine)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
originalProvider.checkServerTrusted(chain, authType)
checkWhitelist(chain[0])
}
override fun getAcceptedIssuers(): Array<out X509Certificate> {
return originalProvider.acceptedIssuers
}
}

View File

@ -11,7 +11,7 @@ sealed class ServiceType(val id: String) {
// //
// * IDs must start with a lower case letter // * IDs must start with a lower case letter
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters // * IDs can only contain alphanumeric, full stop and underscore ASCII characters
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+"))) require(id.matches(Regex("[a-z][a-zA-Z0-9._]+"))) { id }
} }
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId") private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId")

View File

@ -1,205 +0,0 @@
package net.corda.core.crypto
import org.junit.BeforeClass
import org.junit.Test
import java.net.Socket
import java.security.KeyStore
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLEngine
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedTrustManager
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
// TODO: This suppress is needed due to KT-260, fixed in Kotlin 1.0.4 so remove after upgrading.
@Suppress("CAST_NEVER_SUCCEEDS")
class WhitelistTrustManagerTest {
companion object {
@BeforeClass
@JvmStatic
fun registerTrustManager() {
// Validate original factory
assertEquals("PKIX", TrustManagerFactory.getDefaultAlgorithm())
//register for all tests
registerWhitelistTrustManager()
}
}
private fun getTrustmanagerAndCert(whitelist: String, certificateName: String): Pair<X509ExtendedTrustManager, X509Certificate> {
WhitelistTrustManagerProvider.addWhitelistEntry(whitelist)
val caCertAndKey = X509Utilities.createSelfSignedCACert(certificateName)
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, caCertAndKey.certificate)
}
private fun getTrustmanagerAndUntrustedChainCert(): Pair<X509ExtendedTrustManager, X509Certificate> {
WhitelistTrustManagerProvider.addWhitelistEntry("test.r3corda.com")
val otherCaCertAndKey = X509Utilities.createSelfSignedCACert("bad root")
val caCertAndKey = X509Utilities.createSelfSignedCACert("good root")
val subject = X509Utilities.getDevX509Name("test.r3corda.com")
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
val serverCert = X509Utilities.createServerCert(subject,
serverKey.public,
otherCaCertAndKey,
listOf(),
listOf())
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, serverCert)
}
@Test
fun `getDefaultAlgorithm TrustManager is WhitelistTrustManager`() {
registerWhitelistTrustManager() // Check double register is safe
assertEquals("whitelistTrustManager", TrustManagerFactory.getDefaultAlgorithm())
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
assertTrue { trustManagers.all { it is WhitelistTrustManager } }
}
@Test
fun `check certificate works for whitelisted certificate and specific domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check certificate works for specific certificate and wildcard permitted domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check certificate works for wildcard certificate and non wildcard domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check unknown certificate rejected`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.notr3.com")
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check unknown wildcard certificate rejected`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "*.notr3.com")
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check unknown certificate rejected against mismatched wildcard`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.notr3.com")
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check certificate signed by untrusted root is still rejected, despite matched name`() {
val (trustManager, cert) = getTrustmanagerAndUntrustedChainCert()
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
}

View File

@ -15,8 +15,9 @@ and for rarely changed properties this defaulting allows the property to be excl
Format Format
------ ------
Corda uses the Typesafe configuration library to parse the configuration see the `typesafe config on Github <https://github.com/typesafehub/config/>`_ the format of the configuration files can be simple JSON, but for the more powerful substitution features The Corda configuration file uses the HOCON format which is superset of JSON. It has several features which makes it
uses HOCON format see `HOCON documents <https://github.com/typesafehub/config/blob/master/HOCON.md>`_ very useful as a configuration format. Please visit their `page <https://github.com/typesafehub/config/blob/master/HOCON.md>`_
for further details.
Examples Examples
-------- --------
@ -46,50 +47,73 @@ NetworkMapService plus Simple Notary configuration file.
Fields Fields
------ ------
:basedir: This specifies the node workspace folder either as an absolute path, or relative to the current working directory. It can be overidden by the ``--base-directory`` command line option, in which case the the value in the file is ignored and a ``node.conf`` file is expected in that workspace directory as the configuration source. :basedir: This specifies the node workspace folder either as an absolute path, or relative to the current working directory.
It can be overidden by the ``--base-directory`` command line option, in which case the the value in the file is ignored
and a ``node.conf`` file is expected in that workspace directory as the configuration source.
:myLegalName: The legal identity of the node acts as a human readable alias to the node's public key and several demos use this to lookup the NodeInfo. :myLegalName: The legal identity of the node acts as a human readable alias to the node's public key and several demos use
this to lookup the NodeInfo.
:nearestCity: The location of the node as used to locate coordinates on the world map when running the network simulator demo. See :doc:`network-simulator`. :nearestCity: The location of the node as used to locate coordinates on the world map when running the network simulator
demo. See :doc:`network-simulator`.
:keyStorePassword: :keyStorePassword: The password to unlock the KeyStore file (``<workspace>/certificates/sslkeystore.jks``) containing the
The password to unlock the KeyStore file (``<workspace>/certificates/sslkeystore.jks``) containing the node certificate and private key. node certificate and private key.
note:: This is the non-secret value for the development certificates automatically generated during the first node run. Longer term these keys will be managed in secure hardware devices. .. note:: This is the non-secret value for the development certificates automatically generated during the first node run.
Longer term these keys will be managed in secure hardware devices.
:trustStorePassword: :trustStorePassword: The password to unlock the Trust store file (``<workspace>/certificates/truststore.jks``) containing
The password to unlock the Trust store file (``<workspace>/certificates/truststore.jks``) containing the R3 Corda root certificate. This is the non-secret value for the development certificates automatically generated during the first node run. the Corda network root certificate. This is the non-secret value for the development certificates automatically
generated during the first node run.
.. note:: Longer term these keys will be managed in secure hardware devices. .. note:: Longer term these keys will be managed in secure hardware devices.
:dataSourceProperties: :dataSourceProperties: This section is used to configure the jdbc connection and database driver used for the nodes persistence.
This section is used to configure the jdbc connection and database driver used for the nodes persistence. Currently the defaults in ``/node/src/main/resources/reference.conf`` are as shown in the first example. This is currently the only configuration that has been tested, although in the future full support for other storage layers will be validated. Currently the defaults in ``/node/src/main/resources/reference.conf`` are as shown in the first example. This is currently
the only configuration that has been tested, although in the future full support for other storage layers will be validated.
:artemisAddress: :artemisAddress: The host and port on which the node is available for protocol operations over ArtemisMQ.
The host and port on which the node is available for protocol operations over ArtemisMQ.
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However, note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed here must be externally accessible when running nodes across a cluster of machines. .. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed
here must be externally accessible when running nodes across a cluster of machines.
:messagingServerAddress: :messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally.
The address of the ArtemisMQ broker instance. If not provided the node will run one locally.
:webAddress: :webAddress: The host and port on which the node is available for web operations.
The host and port on which the node is available for web operations.
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one of either the machine name, fully qualified machine name, or server IP address to line up with the Subject Alternative Names contained within the development certificates. This is addition to requiring the ``/config/dev/corda_dev_ca.cer`` root certificate be installed as a Trusted CA. .. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
of either the machine name, fully qualified machine name, or server IP address to line up with the Subject Alternative
Names contained within the development certificates. This is addition to requiring the ``/config/dev/corda_dev_ca.cer``
root certificate be installed as a Trusted CA.
:extraAdvertisedServiceIds: A list of ServiceType id strings to be advertised to the NetworkMapService and thus be available when other nodes query the NetworkMapCache for supporting nodes. This can also include plugin services loaded from .jar files in the plugins folder. Optionally, a custom advertised service name can be provided by appending it to the service type id: ``"corda.notary.validating|Notary A"`` :extraAdvertisedServiceIds: A list of ServiceType id strings to be advertised to the NetworkMapService and thus be available
when other nodes query the NetworkMapCache for supporting nodes. This can also include plugin services loaded from .jar
files in the plugins folder. Optionally, a custom advertised service name can be provided by appending it to the service
type id: ``"corda.notary.validating|Notary A"``
:notaryNodeAddress: The host and port to which to bind the embedded Raft server. Required only when running a distributed notary service. A group of Corda nodes can run a distributed notary service by each running an embedded Raft server and joining them to the same cluster to replicate the committed state log. Note that the Raft cluster uses a separate transport layer for communication that does not integrate with ArtemisMQ messaging services. :notaryNodeAddress: The host and port to which to bind the embedded Raft server. Required only when running a distributed
notary service. A group of Corda nodes can run a distributed notary service by each running an embedded Raft server and
joining them to the same cluster to replicate the committed state log. Note that the Raft cluster uses a separate transport
layer for communication that does not integrate with ArtemisMQ messaging services.
:notaryClusterAddresses: List of Raft cluster member addresses used to joining the cluster. At least one of the specified members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be bootstrapped. Required only when running a distributed notary service. :notaryClusterAddresses: List of Raft cluster member addresses used to joining the cluster. At least one of the specified
members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be
bootstrapped. Required only when running a distributed notary service.
:networkMapAddress: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise the configuration value is the remote HostAndPort string for the ArtemisMQ service on the hosting node. :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is
a config object with the details of the network map service:
:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private key from the ``<workspace>/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled then unencrypted HTTP traffic to the node's **webAddress** port is not supported. :address: Host and port string of the ArtemisMQ broker hosting the network map node
:legalName: Legal name of the node. This is required as part of the TLS host verification process. The node will
reject the connection to the network map service if it provides a TLS common name which doesn't match with this value.
:rpcUsers: :useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private
A list of users who are authorised to access the RPC system. Each user in the list is a config object with the key from the ``<workspace>/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled
then unencrypted HTTP traffic to the node's **webAddress** port is not supported.
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
following fields: following fields:
:user: Username consisting only of word characters (a-z, A-Z, 0-9 and _) :user: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
@ -98,8 +122,9 @@ Fields
If this field is absent or an empty list then RPC is effectively locked down. If this field is absent or an empty list then RPC is effectively locked down.
:devMode: :devMode: This flag indicate if the node is running in development mode. On startup, if the keystore ``<workspace>/certificates/sslkeystore.jks``
This flag indicate if the node is running in development mode. On startup, if the keystore ``<workspace>/certificates/sslkeystore.jks`` does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false and keystore does not exist. does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false
and keystore does not exist.
:certificateSigningService: :certificateSigningService: Certificate Signing Server address. It is used by the certificate signing request utility to
Certificate Signing Server address. It is used by the certificate signing request utility to obtain SSL certificate. (See :doc:`permissioning` for more information.) obtain SSL certificate. (See :doc:`permissioning` for more information.)

View File

@ -209,7 +209,7 @@ is a three node example;
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
directory "./build/nodes" // The output directory directory "./build/nodes" // The output directory
networkMap "Controller" // The artemis address of the node named here will be used as the networkMapAddress on all other nodes. networkMap "Controller" // The artemis address of the node named here will be used as the networkMapService.address on all other nodes.
node { node {
name "Controller" name "Controller"
dirName "controller" dirName "controller"

View File

@ -12,7 +12,10 @@ dataSourceProperties : {
artemisAddress : "my-corda-node:10002" artemisAddress : "my-corda-node:10002"
webAddress : "localhost:10003" webAddress : "localhost:10003"
extraAdvertisedServiceIds: "corda.interest_rates" extraAdvertisedServiceIds: "corda.interest_rates"
networkMapAddress : "my-network-map:10000" networkMapService : {
address : "my-network-map:10000"
legalName : "Network Map Service"
}
useHTTPS : false useHTTPS : false
rpcUsers : [ rpcUsers : [
{ user=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] } { user=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] }

View File

@ -107,6 +107,9 @@ the validated user is the username itself and the RPC framework uses this to det
.. note:: ``Party`` lookup is currently done by the legal name which isn't guaranteed to be unique. A future version will .. note:: ``Party`` lookup is currently done by the legal name which isn't guaranteed to be unique. A future version will
use the full X.500 name as it can provide additional structures for uniqueness. use the full X.500 name as it can provide additional structures for uniqueness.
The broker also does host verification when connecting to another peer. It checks that the TLS certificate common name
matches with the advertised legal name from the network map service.
Messaging types Messaging types
--------------- ---------------

View File

@ -46,8 +46,8 @@ The most important fields regarding network configuration are:
but rather ``::`` (all addresses on all interfaces). The hostname specified is the hostname *that must be externally but rather ``::`` (all addresses on all interfaces). The hostname specified is the hostname *that must be externally
resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a machine in a vpn. resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a machine in a vpn.
* ``webAddress``: The address the webserver should bind. Note that the port should be distinct from that of ``artemisAddress``. * ``webAddress``: The address the webserver should bind. Note that the port should be distinct from that of ``artemisAddress``.
* ``networkMapAddress``: The resolvable name and artemis port of the network map node. Note that if this node itself * ``networkMapService``: Details of the node running the network map service. If it's this node that's running the service
is to be the network map this field should not be specified. then this field must not be specified.
Starting the nodes Starting the nodes
------------------ ------------------

View File

@ -392,8 +392,7 @@ Corda nodes can be run on separate machines with little additional configuration
When you have successfully run the ``deployNodes`` gradle task, choose which nodes you would like to run on separate When you have successfully run the ``deployNodes`` gradle task, choose which nodes you would like to run on separate
machines. Copy the folders for those nodes from ``kotlin/build/nodes`` to the other machines. Make sure that you set the machines. Copy the folders for those nodes from ``kotlin/build/nodes`` to the other machines. Make sure that you set the
``networkMapAddress`` property in ``node.conf`` to the correct hostname:port where the network map service node is ``networkMapService`` config in ``node.conf`` to the correct hostname:port and legal name of the network map service node.
hosted.
The nodes can be run on each machine with ``java -jar corda.jar`` from the node's directory. The nodes can be run on each machine with ``java -jar corda.jar`` from the node's directory.
@ -891,9 +890,9 @@ the following changes:
* Change the artemis address to the machine's ip address (e.g. * Change the artemis address to the machine's ip address (e.g.
`artemisAddress="10.18.0.166:10006"`) `artemisAddress="10.18.0.166:10006"`)
* Change the network map address to the ip address of the machine where the * Change the network map service details to the ip address of the machine where the
controller node is running (e.g. `networkMapAddress="10.18.0.166:10002"`) controller node is running and to its legal name (e.g. `networkMapService.address="10.18.0.166:10002"` and
(please note that the controller will not have a network map address) `networkMapService.legalName=controller`) (please note that the controller will not have the `networkMapService` config)
Each machine should now run its nodes using `runnodes` or `runnodes.bat` Each machine should now run its nodes using `runnodes` or `runnodes.bat`
files. Once they are up and running, the nodes should be able to place files. Once they are up and running, the nodes should be able to place

View File

@ -22,7 +22,7 @@ class Cordform extends DefaultTask {
* @param directory The directory the nodes will be installed into. * @param directory The directory the nodes will be installed into.
* @return * @return
*/ */
public void directory(String directory) { void directory(String directory) {
this.directory = Paths.get(directory) this.directory = Paths.get(directory)
} }
@ -32,7 +32,7 @@ class Cordform extends DefaultTask {
* @warning Ensure the node name is one of the configured nodes. * @warning Ensure the node name is one of the configured nodes.
* @param nodeName The name of the node that will host the network map. * @param nodeName The name of the node that will host the network map.
*/ */
public void networkMap(String nodeName) { void networkMap(String nodeName) {
networkMapNodeName = nodeName networkMapNodeName = nodeName
} }
@ -41,7 +41,7 @@ class Cordform extends DefaultTask {
* *
* @param configureClosure A node configuration that will be deployed. * @param configureClosure A node configuration that will be deployed.
*/ */
public void node(Closure configureClosure) { void node(Closure configureClosure) {
nodes << project.configure(new Node(project), configureClosure) nodes << project.configure(new Node(project), configureClosure)
} }
@ -85,7 +85,7 @@ class Cordform extends DefaultTask {
Node networkMapNode = getNodeByName(networkMapNodeName) Node networkMapNode = getNodeByName(networkMapNodeName)
nodes.each { nodes.each {
if(it != networkMapNode) { if(it != networkMapNode) {
it.networkMapAddress(networkMapNode.getArtemisAddress()) it.networkMapAddress(networkMapNode.getArtemisAddress(), networkMapNodeName)
} }
it.build(directory.toFile()) it.build(directory.toFile())
} }

View File

@ -122,11 +122,14 @@ class Node {
* *
* @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the * @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the
* Cordform task instead. * Cordform task instead.
* @param networkMapAddress Network map address. * @param networkMapAddress Network map node address.
* @param networkMapLegalName Network map node legal name.
*/ */
void networkMapAddress(String networkMapAddress) { void networkMapAddress(String networkMapAddress, String networkMapLegalName) {
config = config.withValue("networkMapAddress", def networkMapService = new HashMap()
ConfigValueFactory.fromAnyRef(networkMapAddress)) networkMapService.put("address", networkMapAddress)
networkMapService.put("legalName", networkMapLegalName)
config = config.withValue("networkMapService", ConfigValueFactory.fromMap(networkMapService))
} }
Node(Project project) { Node(Project project) {

View File

@ -1,11 +1,14 @@
package net.corda.node.services package net.corda.node.services
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.DummyContract import net.corda.core.contracts.DummyContract
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.div import net.corda.core.div
import net.corda.core.flatMap
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.flows.NotaryError import net.corda.flows.NotaryError
@ -30,8 +33,7 @@ class RaftNotaryServiceTests : NodeBasedTest() {
@Test @Test
fun `detect double spend`() { fun `detect double spend`() {
val masterNode = createNotaryCluster() val (masterNode, alice) = Futures.allAsList(createNotaryCluster(), startNode("Alice")).getOrThrow()
val alice = startNode("Alice")
val notaryParty = alice.netMapCache.getNotary(notaryName)!! val notaryParty = alice.netMapCache.getNotary(notaryName)!!
@ -60,7 +62,7 @@ class RaftNotaryServiceTests : NodeBasedTest() {
assertEquals(error.tx, stx.tx) assertEquals(error.tx, stx.tx)
} }
private fun createNotaryCluster(): Node { private fun createNotaryCluster(): ListenableFuture<Node> {
val notaryService = ServiceInfo(RaftValidatingNotaryService.type, notaryName) val notaryService = ServiceInfo(RaftValidatingNotaryService.type, notaryName)
val notaryAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } val notaryAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() }
ServiceIdentityGenerator.generateToDisk( ServiceIdentityGenerator.generateToDisk(
@ -73,16 +75,16 @@ class RaftNotaryServiceTests : NodeBasedTest() {
advertisedServices = setOf(notaryService), advertisedServices = setOf(notaryService),
configOverrides = mapOf("notaryNodeAddress" to notaryAddresses[0])) configOverrides = mapOf("notaryNodeAddress" to notaryAddresses[0]))
for (i in 1 until clusterSize) { val remainingNodes = (1 until clusterSize).map {
startNode( startNode(
"Notary$i", "Notary$it",
advertisedServices = setOf(notaryService), advertisedServices = setOf(notaryService),
configOverrides = mapOf( configOverrides = mapOf(
"notaryNodeAddress" to notaryAddresses[i], "notaryNodeAddress" to notaryAddresses[it],
"notaryClusterAddresses" to listOf(notaryAddresses[0]))) "notaryClusterAddresses" to listOf(notaryAddresses[0])))
} }
return masterNode return Futures.allAsList(remainingNodes).flatMap { masterNode }
} }
private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> { private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> {

View File

@ -0,0 +1,50 @@
package net.corda.services.messaging
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
import net.corda.testing.messaging.SimpleMQClient
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
/**
* Runs the security tests with the attacker pretending to be a node on the network.
*/
class MQSecurityAsNodeTest : MQSecurityTest() {
override fun startAttacker(attacker: SimpleMQClient) {
attacker.start(PEER_USER, PEER_USER) // Login as a peer
}
@Test
fun `send message to RPC requests address`() {
assertSendAttackFails(RPC_REQUESTS_QUEUE)
}
@Test
fun `only the node running the broker can login using the special node user`() {
val attacker = clientTo(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(NODE_USER, NODE_USER)
}
}
@Test
fun `login as the default cluster user`() {
val attacker = clientTo(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQClusterSecurityException::class.java).isThrownBy {
attacker.start(ActiveMQDefaultConfiguration.getDefaultClusterUser(), ActiveMQDefaultConfiguration.getDefaultClusterPassword())
}
}
@Test
fun `login without a username and password`() {
val attacker = clientTo(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start()
}
}
}

View File

@ -6,7 +6,7 @@ import net.corda.testing.messaging.SimpleMQClient
/** /**
* Runs the security tests with the attacker being a valid RPC user of Alice. * Runs the security tests with the attacker being a valid RPC user of Alice.
*/ */
class RPCSecurityTest : MQSecurityTest() { class MQSecurityAsRPCTest : MQSecurityTest() {
override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet())) override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet()))
override fun startAttacker(attacker: SimpleMQClient) { override fun startAttacker(attacker: SimpleMQClient) {

View File

@ -12,8 +12,11 @@ import net.corda.core.random63BitValue
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_ADDRESS import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_QUEUE
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEERS_PREFIX
@ -44,8 +47,8 @@ abstract class MQSecurityTest : NodeBasedTest() {
@Before @Before
fun start() { fun start() {
alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser) alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser).getOrThrow()
attacker = SimpleMQClient(alice.configuration.artemisAddress) attacker = clientTo(alice.configuration.artemisAddress)
startAttacker(attacker) startAttacker(attacker)
} }
@ -70,27 +73,31 @@ abstract class MQSecurityTest : NodeBasedTest() {
} }
@Test @Test
fun `send message to peer address`() { fun `send message to address of peer which has been communicated with`() {
val bobParty = startBobAndCommunicateWithAlice() val bobParty = startBobAndCommunicateWithAlice()
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}") assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
} }
@Test
fun `create queue for peer which has not been communciated with`() {
val bob = startNode("Bob").getOrThrow()
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.legalIdentity.owningKey.toBase58String()}")
}
@Test @Test
fun `create queue for unknown peer`() { fun `create queue for unknown peer`() {
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.composite.toBase58String()}" val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.composite.toBase58String()}"
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = true) assertAllQueueCreationAttacksFail(invalidPeerQueue)
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = false)
assertTempQueueCreationAttackFails(invalidPeerQueue)
} }
@Test @Test
fun `consume message from network map queue`() { fun `consume message from network map queue`() {
assertConsumeAttackFails(NETWORK_MAP_ADDRESS.toString()) assertConsumeAttackFails(NETWORK_MAP_QUEUE)
} }
@Test @Test
fun `send message to network map address`() { fun `send message to network map address`() {
assertSendAttackFails(NETWORK_MAP_ADDRESS.toString()) assertSendAttackFails(NETWORK_MAP_QUEUE)
} }
@Test @Test
@ -133,15 +140,19 @@ abstract class MQSecurityTest : NodeBasedTest() {
} }
@Test @Test
fun `create random queue`() { fun `create random internal queue`() {
val randomQueue = random63BitValue().toString() val randomQueue = "$INTERNAL_PREFIX${random63BitValue()}"
assertNonTempQueueCreationAttackFails(randomQueue, durable = false) assertAllQueueCreationAttacksFail(randomQueue)
assertNonTempQueueCreationAttackFails(randomQueue, durable = true)
assertTempQueueCreationAttackFails(randomQueue)
} }
fun clientTo(target: HostAndPort): SimpleMQClient { @Test
val client = SimpleMQClient(target) fun `create random queue`() {
val randomQueue = random63BitValue().toString()
assertAllQueueCreationAttacksFail(randomQueue)
}
fun clientTo(target: HostAndPort, config: NodeSSLConfiguration = configureTestSSL()): SimpleMQClient {
val client = SimpleMQClient(target, config)
clients += client clients += client
return client return client
} }
@ -164,6 +175,12 @@ abstract class MQSecurityTest : NodeBasedTest() {
return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString() return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString()
} }
fun assertAllQueueCreationAttacksFail(queue: String) {
assertNonTempQueueCreationAttackFails(queue, durable = true)
assertNonTempQueueCreationAttackFails(queue, durable = false)
assertTempQueueCreationAttackFails(queue)
}
fun assertTempQueueCreationAttackFails(queue: String) { fun assertTempQueueCreationAttackFails(queue: String) {
assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") { assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") {
attacker.session.createTemporaryQueue(queue, queue) attacker.session.createTemporaryQueue(queue, queue)
@ -210,7 +227,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
} }
private fun startBobAndCommunicateWithAlice(): Party { private fun startBobAndCommunicateWithAlice(): Party {
val bob = startNode("Bob") val bob = startNode("Bob").getOrThrow()
bob.services.registerFlowInitiator(SendFlow::class, ::ReceiveFlow) bob.services.registerFlowInitiator(SendFlow::class, ::ReceiveFlow)
val bobParty = bob.info.legalIdentity val bobParty = bob.info.legalIdentity
// Perform a protocol exchange to force the peer queue to be created // Perform a protocol exchange to force the peer queue to be created

View File

@ -1,6 +1,7 @@
package net.corda.services.messaging package net.corda.services.messaging
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.Futures
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.div import net.corda.core.div
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
@ -21,9 +22,7 @@ class P2PMessagingTest : NodeBasedTest() {
@Test @Test
fun `network map will work after restart`() { fun `network map will work after restart`() {
fun startNodes() { fun startNodes() {
startNode("NodeA") Futures.allAsList(startNode("NodeA"), startNode("NodeB"), startNode("Notary")).getOrThrow()
startNode("NodeB")
startNode("Notary")
} }
startNodes() startNodes()
@ -41,7 +40,8 @@ class P2PMessagingTest : NodeBasedTest() {
startNetworkMapNode(advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) startNetworkMapNode(advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
networkMapNode.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(it, "Hello") } networkMapNode.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(it, "Hello") }
val serviceParty = networkMapNode.services.networkMapCache.getAnyNotary()!! val serviceParty = networkMapNode.services.networkMapCache.getAnyNotary()!!
val received = startNode("Alice").services.startFlow(ReceiveFlow(serviceParty)).resultFuture.getOrThrow(10.seconds) val alice = startNode("Alice").getOrThrow()
val received = alice.services.startFlow(ReceiveFlow(serviceParty)).resultFuture.getOrThrow(10.seconds)
assertThat(received).isEqualTo("Hello") assertThat(received).isEqualTo("Hello")
} }
@ -63,13 +63,15 @@ class P2PMessagingTest : NodeBasedTest() {
"NetworkMap", "NetworkMap",
advertisedServices = setOf(distributedService), advertisedServices = setOf(distributedService),
configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString()))
val alice = startNode( val (alice, bob) = Futures.allAsList(
startNode(
"Alice", "Alice",
advertisedServices = setOf(distributedService), advertisedServices = setOf(distributedService),
configOverrides = mapOf( configOverrides = mapOf(
"notaryNodeAddress" to freeLocalHostAndPort().toString(), "notaryNodeAddress" to freeLocalHostAndPort().toString(),
"notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))) "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))),
val bob = startNode("Bob") startNode("Bob")
).getOrThrow()
// Setup each node in the distributed service to return back it's Party so that we can know which node is being used // Setup each node in the distributed service to return back it's Party so that we can know which node is being used
val serviceNodes = listOf(networkMapNode, alice) val serviceNodes = listOf(networkMapNode, alice)

View File

@ -1,53 +1,71 @@
package net.corda.services.messaging package net.corda.services.messaging
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER import com.google.common.util.concurrent.ListenableFuture
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER import kotlinx.support.jdk7.use
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE import net.corda.core.crypto.Party
import net.corda.testing.messaging.SimpleMQClient import net.corda.core.div
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import net.corda.core.getOrThrow
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import net.corda.core.node.NodeInfo
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import net.corda.core.random63BitValue
import net.corda.core.seconds
import net.corda.flows.sendRequest
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_FLOW_TOPIC
import net.corda.node.services.network.NetworkMapService.RegistrationRequest
import net.corda.node.services.network.NodeRegistration
import net.corda.node.utilities.AddOrRemove
import net.corda.testing.TestNodeConfiguration
import net.corda.testing.node.NodeBasedTest
import net.corda.testing.node.SimpleNode
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test import org.junit.Test
import java.time.Instant
import java.util.concurrent.TimeoutException
/** class P2PSecurityTest : NodeBasedTest() {
* Runs the security tests with the attacker pretending to be a node on the network.
*/
class P2PSecurityTest : MQSecurityTest() {
override fun startAttacker(attacker: SimpleMQClient) { @Test
attacker.start(PEER_USER, PEER_USER) // Login as a peer fun `incorrect legal name for the network map service config`() {
val incorrectNetworkMapName = random63BitValue().toString()
val node = startNode("Bob", configOverrides = mapOf(
"networkMapService" to mapOf(
"address" to networkMapNode.configuration.artemisAddress.toString(),
"legalName" to incorrectNetworkMapName
)
))
// The connection will be rejected as the legal name doesn't match
assertThatThrownBy { node.getOrThrow() }.hasMessageContaining(incorrectNetworkMapName)
} }
@Test @Test
fun `send message to RPC requests address`() { fun `register with the network map service using a legal name different from the TLS CN`() {
assertSendAttackFails(RPC_REQUESTS_QUEUE) startSimpleNode("Attacker").use {
// Register with the network map using a different legal name
val response = it.registerWithNetworkMap("Legit Business")
// We don't expect a response because the network map's host verification will prevent a connection back
// to the attacker as the TLS CN will not match the legal name it has just provided
assertThatExceptionOfType(TimeoutException::class.java).isThrownBy {
response.getOrThrow(2.seconds)
}
}
} }
@Test private fun startSimpleNode(legalName: String): SimpleNode {
fun `only the node running the broker can login using the special node user`() { val config = TestNodeConfiguration(
val attacker = SimpleMQClient(alice.configuration.artemisAddress) basedir = tempFolder.root.toPath() / legalName,
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { myLegalName = legalName,
attacker.start(NODE_USER, NODE_USER) networkMapService = NetworkMapInfo(networkMapNode.configuration.artemisAddress, networkMapNode.info.legalIdentity.name))
} config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name
attacker.stop() return SimpleNode(config).apply { start() }
} }
@Test private fun SimpleNode.registerWithNetworkMap(registrationName: String): ListenableFuture<NetworkMapService.RegistrationResponse> {
fun `login as the default cluster user`() { val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public))
val attacker = SimpleMQClient(alice.configuration.artemisAddress) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX)
assertThatExceptionOfType(ActiveMQClusterSecurityException::class.java).isThrownBy { val request = RegistrationRequest(registration.toWire(identity.private), net.myAddress)
attacker.start(ActiveMQDefaultConfiguration.getDefaultClusterUser(), ActiveMQDefaultConfiguration.getDefaultClusterPassword()) return net.sendRequest<NetworkMapService.RegistrationResponse>(REGISTER_FLOW_TOPIC, request, networkMapNode.net.myAddress)
}
attacker.stop()
}
@Test
fun `login without a username and password`() {
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start()
}
attacker.stop()
} }
} }

View File

@ -2,10 +2,7 @@ package net.corda.node
import com.typesafe.config.ConfigException import com.typesafe.config.ConfigException
import joptsimple.OptionParser import joptsimple.OptionParser
import net.corda.core.div import net.corda.core.*
import net.corda.core.randomOrNull
import net.corda.core.rootCause
import net.corda.core.then
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
@ -82,32 +79,33 @@ fun main(args: Array<String>) {
} }
val dir = conf.basedir.toAbsolutePath().normalize() val dir = conf.basedir.toAbsolutePath().normalize()
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().getPath()}") log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
val info = ManagementFactory.getRuntimeMXBean() val info = ManagementFactory.getRuntimeMXBean()
log.info("CommandLine Args: ${info.getInputArguments().joinToString(" ")}") log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
log.info("Application Args: ${args.joinToString(" ")}") log.info("Application Args: ${args.joinToString(" ")}")
log.info("bootclasspath: ${info.bootClassPath}") log.info("bootclasspath: ${info.bootClassPath}")
log.info("classpath: ${info.classPath}") log.info("classpath: ${info.classPath}")
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}") log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${InetAddress.getLocalHost().hostName}") log.info("Machine: ${InetAddress.getLocalHost().hostName}")
log.info("Working Directory: ${dir}") log.info("Working Directory: $dir")
try { try {
val dirFile = dir.toFile() dir.createDirectories()
if (!dirFile.exists())
dirFile.mkdirs()
val node = conf.createNode() val node = conf.createNode()
node.start() node.start()
printPluginsAndServices(node) printPluginsAndServices(node)
node.networkMapRegistrationFuture.then { node.networkMapRegistrationFuture.success {
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
printBasicNodeInfo("Node started up and registered in $elapsed sec") printBasicNodeInfo("Node started up and registered in $elapsed sec")
if (renderBasicInfoToConsole) if (renderBasicInfoToConsole)
ANSIProgressObserver(node.smm) ANSIProgressObserver(node.smm)
} failure {
log.error("Error during network map registration", it)
exitProcess(1)
} }
node.run() node.run()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -19,7 +19,6 @@ import net.corda.core.utilities.loggerFor
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
@ -260,7 +259,7 @@ open class DriverDSL(
val isDebug: Boolean val isDebug: Boolean
) : DriverDSLInternalInterface { ) : DriverDSLInternalInterface {
private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2)
private val networkMapName = "NetworkMapService" private val networkMapLegalName = "NetworkMapService"
private val networkMapAddress = portAllocation.nextHostAndPort() private val networkMapAddress = portAllocation.nextHostAndPort()
class State { class State {
@ -291,9 +290,7 @@ open class DriverDSL(
override fun shutdown() { override fun shutdown() {
state.locked { state.locked {
clients.forEach { clients.forEach(NodeMessagingClient::stop)
it.stop()
}
registeredProcesses.forEach { registeredProcesses.forEach {
it.get().destroy() it.get().destroy()
} }
@ -353,7 +350,10 @@ open class DriverDSL(
"artemisAddress" to messagingAddress.toString(), "artemisAddress" to messagingAddress.toString(),
"webAddress" to apiAddress.toString(), "webAddress" to apiAddress.toString(),
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","), "extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
"networkMapAddress" to networkMapAddress.toString(), "networkMapService" to mapOf(
"address" to networkMapAddress.toString(),
"legalName" to networkMapLegalName
),
"useTestClock" to useTestClock, "useTestClock" to useTestClock,
"rpcUsers" to rpcUsers.map { "rpcUsers" to rpcUsers.map {
mapOf( mapOf(
@ -416,12 +416,12 @@ open class DriverDSL(
val apiAddress = portAllocation.nextHostAndPort() val apiAddress = portAllocation.nextHostAndPort()
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val baseDirectory = driverDirectory / networkMapName val baseDirectory = driverDirectory / networkMapLegalName
val config = ConfigHelper.loadConfig( val config = ConfigHelper.loadConfig(
baseDirectoryPath = baseDirectory, baseDirectoryPath = baseDirectory,
allowMissingConfig = true, allowMissingConfig = true,
configOverrides = mapOf( configOverrides = mapOf(
"myLegalName" to networkMapName, "myLegalName" to networkMapLegalName,
"basedir" to baseDirectory.normalize().toString(), "basedir" to baseDirectory.normalize().toString(),
"artemisAddress" to networkMapAddress.toString(), "artemisAddress" to networkMapAddress.toString(),
"webAddress" to apiAddress.toString(), "webAddress" to apiAddress.toString(),

View File

@ -45,7 +45,7 @@ import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.transactions.* import net.corda.node.services.transactions.*
import net.corda.node.services.vault.CashBalanceAsMetricsObserver import net.corda.node.services.vault.CashBalanceAsMetricsObserver
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
@ -72,8 +72,9 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
// TODO: Where this node is the initial network map service, currently no networkMapService is provided. // TODO: Where this node is the initial network map service, currently no networkMapService is provided.
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
abstract class AbstractNode(open val configuration: NodeConfiguration, val networkMapService: SingleMessageRecipient?, abstract class AbstractNode(open val configuration: NodeConfiguration,
val advertisedServices: Set<ServiceInfo>, val platformClock: Clock) : SingletonSerializeAsToken() { val advertisedServices: Set<ServiceInfo>,
val platformClock: Clock) : SingletonSerializeAsToken() {
companion object { companion object {
val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PRIVATE_KEY_FILE_NAME = "identity-private-key"
val PUBLIC_IDENTITY_FILE_NAME = "identity-public" val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
@ -95,6 +96,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
var networkMapSeq: Long = 1 var networkMapSeq: Long = 1
protected abstract val log: Logger protected abstract val log: Logger
protected abstract val networkMapAddress: SingleMessageRecipient?
// We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the // We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the
// low-performance prototyping period. // low-performance prototyping period.
@ -174,8 +176,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
var isPreviousCheckpointsPresent = false var isPreviousCheckpointsPresent = false
private set private set
protected val _networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create()
/** Completes once the node has successfully registered with the network map service */ /** Completes once the node has successfully registered with the network map service */
private val _networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create()
val networkMapRegistrationFuture: ListenableFuture<Unit> val networkMapRegistrationFuture: ListenableFuture<Unit>
get() = _networkMapRegistrationFuture get() = _networkMapRegistrationFuture
@ -259,7 +261,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
} }
startMessagingService(CordaRPCOpsImpl(services, smm, database)) startMessagingService(CordaRPCOpsImpl(services, smm, database))
runOnStop += Runnable { net.stop() } runOnStop += Runnable { net.stop() }
_networkMapRegistrationFuture.setFuture(registerWithNetworkMap()) _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
smm.start() smm.start()
// Shut down the SMM so no Fibers are scheduled. // Shut down the SMM so no Fibers are scheduled.
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) } runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
@ -355,7 +357,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
return serviceList return serviceList
} }
/** /**
* Run any tasks that are needed to ensure the node is in a correct state before running start(). * Run any tasks that are needed to ensure the node is in a correct state before running start().
*/ */
@ -374,27 +375,43 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
} }
} }
/** private fun registerWithNetworkMapIfConfigured(): ListenableFuture<Unit> {
* Register this node with the network map cache, and load network map from a remote service (and register for require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) {
* updates) if one has been supplied.
*/
private fun registerWithNetworkMap(): ListenableFuture<Unit> {
require(networkMapService != null || NetworkMapService.type in advertisedServices.map { it.type }) {
"Initial network map address must indicate a node that provides a network map service" "Initial network map address must indicate a node that provides a network map service"
} }
services.networkMapCache.addNode(info) services.networkMapCache.addNode(info)
// In the unit test environment, we may run without any network map service sometimes. // In the unit test environment, we may run without any network map service sometimes.
if (networkMapService == null && inNodeNetworkMapService == null) { return if (networkMapAddress == null && inNodeNetworkMapService == null) {
services.networkMapCache.runWithoutMapService() services.networkMapCache.runWithoutMapService()
return noNetworkMapConfigured() noNetworkMapConfigured() // TODO This method isn't needed as runWithoutMapService sets the Future in the cache
} else {
registerWithNetworkMap()
} }
return registerWithNetworkMap(networkMapService ?: info.address)
} }
private fun registerWithNetworkMap(networkMapServiceAddress: SingleMessageRecipient): ListenableFuture<Unit> { /**
* Register this node with the network map cache, and load network map from a remote service (and register for
* updates) if one has been supplied.
*/
protected open fun registerWithNetworkMap(): ListenableFuture<Unit> {
val address = networkMapAddress ?: info.address
// Register for updates, even if we're the one running the network map. // Register for updates, even if we're the one running the network map.
updateRegistration(networkMapServiceAddress, AddOrRemove.ADD) return sendNetworkMapRegistration(address).flatMap { response ->
return services.networkMapCache.addMapService(net, networkMapServiceAddress, true, null) check(response.success) { "The network map service rejected our registration request" }
// This Future will complete on the same executor as sendNetworkMapRegistration, namely the one used by net
services.networkMapCache.addMapService(net, address, true, null)
}
}
private fun sendNetworkMapRegistration(networkMapAddress: SingleMessageRecipient): ListenableFuture<RegistrationResponse> {
// Register this node against the network
val instant = platformClock.instant()
val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires)
val legalIdentityKey = obtainLegalIdentityKey()
val request = NetworkMapService.RegistrationRequest(reg.toWire(legalIdentityKey.private), net.myAddress)
return net.sendRequest(REGISTER_FLOW_TOPIC, request, networkMapAddress)
} }
/** This is overriden by the mock node implementation to enable operation without any network map service */ /** This is overriden by the mock node implementation to enable operation without any network map service */
@ -404,16 +421,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
"has any other map node been configured.") "has any other map node been configured.")
} }
private fun updateRegistration(networkMapAddr: SingleMessageRecipient, type: AddOrRemove): ListenableFuture<RegistrationResponse> {
// Register this node against the network
val instant = platformClock.instant()
val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
val reg = NodeRegistration(info, instant.toEpochMilli(), type, expires)
val legalIdentityKey = obtainLegalIdentityKey()
val request = NetworkMapService.RegistrationRequest(reg.toWire(legalIdentityKey.private), net.myAddress)
return net.sendRequest(REGISTER_FLOW_TOPIC, request, networkMapAddr)
}
protected open fun makeKeyManagementService(): KeyManagementService = PersistentKeyManagementService(partyKeys) protected open fun makeKeyManagementService(): KeyManagementService = PersistentKeyManagementService(partyKeys)
open protected fun makeNetworkMapService() { open protected fun makeNetworkMapService() {

View File

@ -1,15 +1,18 @@
package net.corda.node.internal package net.corda.node.internal
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.div import net.corda.core.div
import net.corda.core.getOrThrow import net.corda.core.flatMap
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.core.success
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo import net.corda.node.printBasicNodeInfo
import net.corda.node.serialization.NodeClock import net.corda.node.serialization.NodeClock
@ -17,11 +20,11 @@ import net.corda.node.services.RPCUserService
import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.services.api.MessagingServiceInternal import net.corda.node.services.api.MessagingServiceInternal
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.CordaRPCClient import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.transactions.RaftUniquenessProvider
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
@ -53,24 +56,21 @@ import java.util.*
import javax.management.ObjectName import javax.management.ObjectName
import javax.servlet.* import javax.servlet.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
class ConfigurationException(message: String) : Exception(message)
/** /**
* A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub], * A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub],
* loads important data off disk and starts listening for connections. * loads important data off disk and starts listening for connections.
* *
* @param configuration This is typically loaded from a TypeSafe HOCON configuration file. * @param configuration This is typically loaded from a TypeSafe HOCON configuration file.
* @param networkMapAddress An external network map service to use. Should only ever be null when creating the first
* network map service, while bootstrapping a network.
* @param advertisedServices The services this node advertises. This must be a subset of the services it runs, * @param advertisedServices The services this node advertises. This must be a subset of the services it runs,
* but nodes are not required to advertise services they run (hence subset). * but nodes are not required to advertise services they run (hence subset).
* @param clock The clock used within the node and by all flows etc. * @param clock The clock used within the node and by all flows etc.
*/ */
class Node(override val configuration: FullNodeConfiguration, networkMapAddress: SingleMessageRecipient?, class Node(override val configuration: FullNodeConfiguration,
advertisedServices: Set<ServiceInfo>, clock: Clock = NodeClock()) : AbstractNode(configuration, networkMapAddress, advertisedServices, clock) { advertisedServices: Set<ServiceInfo>,
clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) {
override val log = loggerFor<Node>() override val log = loggerFor<Node>()
override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress)
// DISCUSSION // DISCUSSION
// //
@ -125,25 +125,22 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
override fun makeMessagingService(): MessagingServiceInternal { override fun makeMessagingService(): MessagingServiceInternal {
userService = RPCUserServiceImpl(configuration) userService = RPCUserServiceImpl(configuration)
val serverAddr = with(configuration) { val serverAddress = with(configuration) {
messagingServerAddress ?: { messagingServerAddress ?: {
messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService) messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService)
artemisAddress artemisAddress
}() }()
} }
val legalIdentity = obtainLegalIdentity() val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null
val myIdentityOrNullIfNetworkMapService = if (networkMapService != null) legalIdentity.owningKey else null return NodeMessagingClient(configuration, serverAddress, myIdentityOrNullIfNetworkMapService, serverThread, database,
return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread, database, networkMapRegistrationFuture) networkMapRegistrationFuture)
} }
override fun startMessagingService(rpcOps: RPCOps) { override fun startMessagingService(rpcOps: RPCOps) {
// Start up the embedded MQ server // Start up the embedded MQ server
messageBroker?.apply { messageBroker?.apply {
runOnStop += Runnable { messageBroker?.stop() } runOnStop += Runnable { stop() }
start() start()
if (networkMapService is NetworkMapAddress) {
deployBridgeIfAbsent(networkMapService.queueName, networkMapService.hostAndPort)
}
} }
// Start up the MQ client. // Start up the MQ client.
@ -151,6 +148,15 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
net.start(rpcOps, userService) net.start(rpcOps, userService)
} }
/**
* Insert an initial step in the registration process which will throw an exception if a non-recoverable error is
* encountered when trying to connect to the network map node.
*/
override fun registerWithNetworkMap(): ListenableFuture<Unit> {
val networkMapConnection = messageBroker?.networkMapConnectionFuture ?: Futures.immediateFuture(Unit)
return networkMapConnection.flatMap { super.registerWithNetworkMap() }
}
// TODO: add flag to enable/disable webserver // TODO: add flag to enable/disable webserver
private fun initWebServer(localRpc: CordaRPCOps): Server { private fun initWebServer(localRpc: CordaRPCOps): Server {
// Note that the web server handlers will all run concurrently, and not on the node thread. // Note that the web server handlers will all run concurrently, and not on the node thread.
@ -308,9 +314,11 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
override fun start(): Node { override fun start(): Node {
alreadyRunningNodeCheck() alreadyRunningNodeCheck()
super.start() super.start()
// Only start the service API requests once the network map registration is complete
// Only start the service API requests once the network map registration is successfully complete
networkMapRegistrationFuture.success {
// This needs to be in a seperate thread so that we can reply to our own request to become RPC clients
thread(name = "WebServer") { thread(name = "WebServer") {
networkMapRegistrationFuture.getOrThrow()
try { try {
webServer = initWebServer(connectLocalRpcAsNodeUser()) webServer = initWebServer(connectLocalRpcAsNodeUser())
} catch(ex: Exception) { } catch(ex: Exception) {
@ -334,6 +342,8 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
build(). build().
start() start()
} }
}
shutdownThread = thread(start = false) { shutdownThread = thread(start = false) {
stop() stop()
} }
@ -405,16 +415,16 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
// Servlet filter to wrap API requests with a database transaction. // Servlet filter to wrap API requests with a database transaction.
private class DatabaseTransactionFilter(val database: Database) : Filter { private class DatabaseTransactionFilter(val database: Database) : Filter {
override fun init(filterConfig: FilterConfig?) {
}
override fun destroy() {
}
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
databaseTransaction(database) { databaseTransaction(database) {
chain.doFilter(request, response) chain.doFilter(request, response)
} }
} }
override fun init(filterConfig: FilterConfig?) {}
override fun destroy() {}
} }
} }
class ConfigurationException(message: String) : Exception(message)
data class NetworkMapInfo(val address: HostAndPort, val legalName: String)

View File

@ -1,7 +1,7 @@
package net.corda.node.services package net.corda.node.services
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.NodeConfiguration
/** /**
* Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User] * Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User]
@ -15,7 +15,7 @@ interface RPCUserService {
// TODO Store passwords as salted hashes // TODO Store passwords as salted hashes
// TODO Or ditch this and consider something like Apache Shiro // TODO Or ditch this and consider something like Apache Shiro
class RPCUserServiceImpl(config: FullNodeConfiguration) : RPCUserService { class RPCUserServiceImpl(config: NodeConfiguration) : RPCUserService {
private val _users = config.rpcUsers.associateBy(User::username) private val _users = config.rpcUsers.associateBy(User::username)

View File

@ -119,12 +119,13 @@ private fun NodeSSLConfiguration.configureDevKeyAndTrustStores(myLegalName: Stri
} }
// TODO Move this to CoreTestUtils.kt once we can pry this from the explorer // TODO Move this to CoreTestUtils.kt once we can pry this from the explorer
fun configureTestSSL(): NodeSSLConfiguration = object : NodeSSLConfiguration { @JvmOverloads
fun configureTestSSL(legalName: String = "Mega Corp."): NodeSSLConfiguration = object : NodeSSLConfiguration {
override val certificatesPath = Files.createTempDirectory("certs") override val certificatesPath = Files.createTempDirectory("certs")
override val keyStorePassword: String get() = "cordacadevpass" override val keyStorePassword: String get() = "cordacadevpass"
override val trustStorePassword: String get() = "trustpass" override val trustStorePassword: String get() = "trustpass"
init { init {
configureDevKeyAndTrustStores("Mega Corp.") configureDevKeyAndTrustStores(legalName)
} }
} }

View File

@ -3,12 +3,11 @@ package net.corda.node.services.config
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.typesafe.config.Config import com.typesafe.config.Config
import net.corda.core.div import net.corda.core.div
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.serialization.NodeClock import net.corda.node.serialization.NodeClock
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.utilities.TestClock import net.corda.node.utilities.TestClock
import java.nio.file.Path import java.nio.file.Path
@ -26,10 +25,12 @@ interface NodeConfiguration : NodeSSLConfiguration {
val basedir: Path val basedir: Path
override val certificatesPath: Path get() = basedir / "certificates" override val certificatesPath: Path get() = basedir / "certificates"
val myLegalName: String val myLegalName: String
val networkMapService: NetworkMapInfo?
val nearestCity: String val nearestCity: String
val emailAddress: String val emailAddress: String
val exportJMXto: String val exportJMXto: String
val dataSourceProperties: Properties get() = Properties() val dataSourceProperties: Properties get() = Properties()
val rpcUsers: List<User> get() = emptyList()
val devMode: Boolean val devMode: Boolean
} }
@ -38,22 +39,18 @@ class FullNodeConfiguration(val config: Config) : NodeConfiguration {
override val myLegalName: String by config override val myLegalName: String by config
override val nearestCity: String by config override val nearestCity: String by config
override val emailAddress: String by config override val emailAddress: String by config
override val exportJMXto: String = "http" override val exportJMXto: String get() = "http"
override val keyStorePassword: String by config override val keyStorePassword: String by config
override val trustStorePassword: String by config override val trustStorePassword: String by config
override val dataSourceProperties: Properties by config override val dataSourceProperties: Properties by config
override val devMode: Boolean by config.getOrElse { false } override val devMode: Boolean by config.getOrElse { false }
val networkMapAddress: HostAndPort? by config.getOrElse { null } override val networkMapService: NetworkMapInfo? = config.getOptionalConfig("networkMapService")?.run {
val useHTTPS: Boolean by config NetworkMapInfo(
val artemisAddress: HostAndPort by config HostAndPort.fromString(getString("address")),
val webAddress: HostAndPort by config getString("legalName"))
val messagingServerAddress: HostAndPort? by config.getOrElse { null } }
val extraAdvertisedServiceIds: String by config override val rpcUsers: List<User> = config
val useTestClock: Boolean by config.getOrElse { false } .getListOrElse<Config>("rpcUsers") { emptyList() }
val notaryNodeAddress: HostAndPort? by config.getOrElse { null }
val notaryClusterAddresses: List<HostAndPort> = config.getListOrElse<String>("notaryClusterAddresses") { emptyList<String>() }.map { HostAndPort.fromString(it) }
val rpcUsers: List<User> =
config.getListOrElse<Config>("rpcUsers") { emptyList() }
.map { .map {
val username = it.getString("user") val username = it.getString("user")
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" } require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
@ -61,20 +58,30 @@ class FullNodeConfiguration(val config: Config) : NodeConfiguration {
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet() val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet()
User(username, password, permissions) User(username, password, permissions)
} }
val useHTTPS: Boolean by config
val artemisAddress: HostAndPort by config
val webAddress: HostAndPort by config
val messagingServerAddress: HostAndPort? by config.getOrElse { null }
val extraAdvertisedServiceIds: String by config
val useTestClock: Boolean by config.getOrElse { false }
val notaryNodeAddress: HostAndPort? by config.getOrElse { null }
val notaryClusterAddresses: List<HostAndPort> = config
.getListOrElse<String>("notaryClusterAddresses") { emptyList() }
.map { HostAndPort.fromString(it) }
fun createNode(): Node { fun createNode(): Node {
// This is a sanity feature do not remove. // This is a sanity feature do not remove.
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" } require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
val advertisedServices = mutableSetOf<ServiceInfo>() val advertisedServices = extraAdvertisedServiceIds
if (!extraAdvertisedServiceIds.isNullOrEmpty()) { .split(",")
for (serviceId in extraAdvertisedServiceIds.split(",")) { .filter(String::isNotBlank)
advertisedServices.add(ServiceInfo.parse(serviceId)) .map { ServiceInfo.parse(it) }
} .toMutableSet()
} if (networkMapService == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
if (networkMapAddress == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
val networkMapMessageAddress: SingleMessageRecipient? = if (networkMapAddress == null) null else NodeMessagingClient.makeNetworkMapAddress(networkMapAddress!!) return Node(this, advertisedServices, if (useTestClock) TestClock() else NodeClock())
return Node(this, networkMapMessageAddress, advertisedServices, if (useTestClock == true) TestClock() else NodeClock())
} }
} }
private fun Config.getOptionalConfig(path: String): Config? = if (hasPath(path)) getConfig(path) else null

View File

@ -9,10 +9,10 @@ import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.read import net.corda.core.read
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.config.NodeSSLConfiguration import net.corda.node.services.config.NodeSSLConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Inbound
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import java.nio.file.FileSystems import java.nio.file.FileSystems
import java.nio.file.Path import java.nio.file.Path
@ -41,9 +41,9 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
const val RPC_REQUESTS_QUEUE = "rpc.requests" const val RPC_REQUESTS_QUEUE = "rpc.requests"
const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals" const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap"
@JvmStatic const val VERIFY_PEER_COMMON_NAME = "corda.verifyPeerCommonName"
val NETWORK_MAP_ADDRESS = "${INTERNAL_PREFIX}networkmap"
/** /**
* Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should * Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should
@ -59,7 +59,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
} }
interface ArtemisAddress : MessageRecipients { interface ArtemisAddress : MessageRecipients {
val queueName: SimpleString val queueName: String
} }
interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient { interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient {
@ -67,7 +67,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
} }
data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress { data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress {
override val queueName = SimpleString(NETWORK_MAP_ADDRESS) override val queueName: String get() = NETWORK_MAP_QUEUE
} }
/** /**
@ -75,22 +75,21 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
* may change or evolve and code that relies upon it being a simple host/port may not function correctly. * may change or evolve and code that relies upon it being a simple host/port may not function correctly.
* For instance it may contain onion routing data. * For instance it may contain onion routing data.
* *
* [NodeAddress] identifies a specific peer node and an associated queue. The queue may be the peer's p2p queue or * [NodeAddress] identifies a specific peer node and an associated queue. The queue may be the peer's own queue or
* an advertised service's queue. * an advertised service's queue.
* *
* @param queueName The name of the queue this address is associated with. * @param queueName The name of the queue this address is associated with.
* @param hostAndPort The address of the node. * @param hostAndPort The address of the node.
*/ */
data class NodeAddress(override val queueName: SimpleString, override val hostAndPort: HostAndPort) : ArtemisPeerAddress { data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress {
companion object { companion object {
fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress(SimpleString("$PEERS_PREFIX${peerIdentity.toBase58String()}"), hostAndPort) return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort)
} }
fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress(SimpleString("$SERVICES_PREFIX${serviceIdentity.toBase58String()}"), hostAndPort) return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort)
} }
} }
override fun toString(): String = "${javaClass.simpleName}(queue = $queueName, $hostAndPort)"
} }
/** /**
@ -103,14 +102,12 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
* @param identity The service identity's owning key. * @param identity The service identity's owning key.
*/ */
data class ServiceAddress(val identity: CompositeKey) : ArtemisAddress, MessageRecipientGroup { data class ServiceAddress(val identity: CompositeKey) : ArtemisAddress, MessageRecipientGroup {
override val queueName: SimpleString = SimpleString("$SERVICES_PREFIX${identity.toBase58String()}") override val queueName: String = "$SERVICES_PREFIX${identity.toBase58String()}"
} }
/** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */ /** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */
abstract val config: NodeSSLConfiguration abstract val config: NodeSSLConfiguration
protected enum class ConnectionDirection { INBOUND, OUTBOUND }
// Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher. // Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher.
// Our self-generated certificates all use ECDSA for handshakes, but we allow classical RSA certificates to work // Our self-generated certificates all use ECDSA for handshakes, but we allow classical RSA certificates to work
// in case we need to use keytool certificates in some demos // in case we need to use keytool certificates in some demos
@ -142,8 +139,8 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
config.trustStorePath.expectedOnDefaultFileSystem() config.trustStorePath.expectedOnDefaultFileSystem()
return TransportConfiguration( return TransportConfiguration(
when (direction) { when (direction) {
ConnectionDirection.INBOUND -> NettyAcceptorFactory::class.java.name is Inbound -> NettyAcceptorFactory::class.java.name
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name is Outbound -> VerifyingNettyConnectorFactory::class.java.name
}, },
mapOf( mapOf(
// Basic TCP target details // Basic TCP target details
@ -167,9 +164,8 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword, TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2", TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
VERIFY_PEER_COMMON_NAME to (direction as? Outbound)?.expectedCommonName
// TODO: Set up the connector's host name verifier logic to ensure we connect to the expected node even in case of MITM or BGP hijacks
) )
) )
} }
@ -177,4 +173,9 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
protected fun Path.expectedOnDefaultFileSystem() { protected fun Path.expectedOnDefaultFileSystem() {
require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" } require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
} }
protected sealed class ConnectionDirection {
object Inbound : ConnectionDirection()
class Outbound(val expectedCommonName: String? = null) : ConnectionDirection()
}
} }

View File

@ -1,46 +1,58 @@
package net.corda.node.services.messaging package net.corda.node.services.messaging
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import io.netty.handler.ssl.SslHandler
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.crypto.AddressFormatException import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.core.crypto.newSecureRandom
import net.corda.core.div import net.corda.core.div
import net.corda.core.minutes
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.seconds
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo import net.corda.node.printBasicNodeInfo
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.INBOUND import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.OUTBOUND import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.PEER_ROLE import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Inbound
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.RPC_ROLE import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
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.RPC_ROLE
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.Configuration
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.spi.core.remoting.*
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import org.bouncycastle.asn1.x500.X500Name
import rx.Subscription import rx.Subscription
import java.io.IOException import java.io.IOException
import java.math.BigInteger import java.math.BigInteger
import java.security.Principal import java.security.Principal
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.ScheduledExecutorService
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.Subject import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler import javax.security.auth.callback.CallbackHandler
@ -52,6 +64,7 @@ import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.RE
import javax.security.auth.login.FailedLoginException import javax.security.auth.login.FailedLoginException
import javax.security.auth.login.LoginException import javax.security.auth.login.LoginException
import javax.security.auth.spi.LoginModule import javax.security.auth.spi.LoginModule
import javax.security.cert.X509Certificate
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman. // TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later) // TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
@ -81,6 +94,12 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
private val mutex = ThreadBox(InnerState()) private val mutex = ThreadBox(InnerState())
private lateinit var activeMQServer: ActiveMQServer private lateinit var activeMQServer: ActiveMQServer
private val _networkMapConnectionFuture = config.networkMapService?.let { SettableFuture.create<Unit>() }
/**
* A [ListenableFuture] which completes when the server successfully connects to the network map node. If a
* non-recoverable error is encountered then the Future will complete with an exception.
*/
val networkMapConnectionFuture: SettableFuture<Unit>? get() = _networkMapConnectionFuture
private var networkChangeHandle: Subscription? = null private var networkChangeHandle: Subscription? = null
init { init {
@ -88,13 +107,15 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
} }
/** /**
* The server will make sure the bridge exists on network map changes, see method [destroyOrCreateBridge] * The server will make sure the bridge exists on network map changes, see method [updateBridgesOnNetworkChange]
* We assume network map will be updated accordingly when the client node register with the network map server. * We assume network map will be updated accordingly when the client node register with the network map server.
*/ */
fun start() = mutex.locked { fun start() = mutex.locked {
if (!running) { if (!running) {
configureAndStartServer() configureAndStartServer()
networkChangeHandle = networkMapCache.changed.subscribe { destroyOrCreateBridges(it) } // Deploy bridge to the network map service
config.networkMapService?.let { deployBridge(NetworkMapAddress(it.address), it.legalName) }
networkChangeHandle = networkMapCache.changed.subscribe { updateBridgesOnNetworkChange(it) }
running = true running = true
} }
} }
@ -106,48 +127,6 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
running = false running = false
} }
/**
* The bridge will be created automatically when the queues are created, however, this is not the case when the network map restarted.
* The queues are restored from the journal, and because the queues are added before we register the callback handler, this method will never get called for existing queues.
* This results in message queues up and never get send out. (https://github.com/corda/corda/issues/37)
*
* We create the bridges indirectly now because the network map is not persisted and there are no ways to obtain host and port information on startup.
* TODO : Create the bridge directly from the list of queues on start up when we have a persisted network map service.
*/
private fun destroyOrCreateBridges(change: MapChange) {
fun addAddresses(node: NodeInfo, targets: MutableSet<ArtemisPeerAddress>) {
// Add the node's address with the p2p queue.
val nodeAddress = node.address as ArtemisPeerAddress
targets.add(nodeAddress)
// Add the node's address with service queues, one per service.
node.advertisedServices.forEach {
targets.add(NodeAddress.asService(it.identity.owningKey, nodeAddress.hostAndPort))
}
}
val addressesToCreateBridgesTo = HashSet<ArtemisPeerAddress>()
val addressesToRemoveBridgesFrom = HashSet<ArtemisPeerAddress>()
when (change) {
is MapChange.Modified -> {
addAddresses(change.node, addressesToCreateBridgesTo)
addAddresses(change.previousNode, addressesToRemoveBridgesFrom)
}
is MapChange.Removed -> {
addAddresses(change.node, addressesToRemoveBridgesFrom)
}
is MapChange.Added -> {
addAddresses(change.node, addressesToCreateBridgesTo)
}
}
(addressesToRemoveBridgesFrom - addressesToCreateBridgesTo).forEach {
activeMQServer.destroyBridge(getBridgeName(it.queueName, it.hostAndPort))
}
addressesToCreateBridgesTo.filter { activeMQServer.queueQuery(it.queueName).isExists }.forEach {
deployBridgeIfAbsent(it.queueName, it.hostAndPort)
}
}
private fun configureAndStartServer() { private fun configureAndStartServer() {
val config = createArtemisConfig() val config = createArtemisConfig()
val securityManager = createArtemisSecurityManager() val securityManager = createArtemisSecurityManager()
@ -156,57 +135,19 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
registerActivationFailureListener { exception -> throw exception } registerActivationFailureListener { exception -> throw exception }
// Some types of queue might need special preparation on our side, like dialling back or preparing // Some types of queue might need special preparation on our side, like dialling back or preparing
// a lazily initialised subsystem. // a lazily initialised subsystem.
registerPostQueueCreationCallback { deployBridgeFromNewQueue(it) } registerPostQueueCreationCallback { deployBridgesFromNewQueue(it.toString()) }
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
} }
activeMQServer.start() activeMQServer.start()
printBasicNodeInfo("Node listening on address", myHostPort.toString()) printBasicNodeInfo("Node listening on address", myHostPort.toString())
} }
private fun maybeDeployBridgeForNode(queueName: SimpleString, nodeInfo: NodeInfo) {
val address = nodeInfo.address
if (address is ArtemisPeerAddress) {
log.debug("Deploying bridge for $queueName to $nodeInfo")
deployBridgeIfAbsent(queueName, address.hostAndPort)
} else {
log.error("Don't know how to deal with $address for queue $queueName")
}
}
private fun deployBridgeFromNewQueue(queueName: SimpleString) {
log.debug { "Queue created: $queueName, deploying bridge(s)" }
when {
queueName.startsWith(PEERS_PREFIX) -> try {
val identity = CompositeKey.parseFromBase58(queueName.substring(PEERS_PREFIX.length))
val nodeInfo = networkMapCache.getNodeByLegalIdentityKey(identity)
if (nodeInfo != null) {
maybeDeployBridgeForNode(queueName, nodeInfo)
} else {
log.error("Queue created for a peer that we don't know from the network map: $queueName")
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse peer queue name as Base 58: $queueName")
}
queueName.startsWith(SERVICES_PREFIX) -> try {
val identity = CompositeKey.parseFromBase58(queueName.substring(SERVICES_PREFIX.length))
val nodeInfos = networkMapCache.getNodesByAdvertisedServiceIdentityKey(identity)
// Create a bridge for each node advertising the service.
for (nodeInfo in nodeInfos) {
maybeDeployBridgeForNode(queueName, nodeInfo)
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse service queue name as Base 58: $queueName")
}
}
}
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply { private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {
val artemisDir = config.basedir / "artemis" val artemisDir = config.basedir / "artemis"
bindingsDirectory = (artemisDir / "bindings").toString() bindingsDirectory = (artemisDir / "bindings").toString()
journalDirectory = (artemisDir / "journal").toString() journalDirectory = (artemisDir / "journal").toString()
largeMessagesDirectory = (artemisDir / "large-messages").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString()
acceptorConfigurations = setOf(tcpTransport(INBOUND, "0.0.0.0", myHostPort.port)) acceptorConfigurations = setOf(tcpTransport(Inbound, "0.0.0.0", myHostPort.port))
// Enable built in message deduplication. Note we still have to do our own as the delayed commits // Enable built in message deduplication. Note we still have to do our own as the delayed commits
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates. // and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
@ -215,42 +156,32 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS)
// Artemis allows multiple servers to be grouped together into a cluster for load balancing purposes. The cluster // Artemis allows multiple servers to be grouped together into a cluster for load balancing purposes. The cluster
// user is used for connecting the nodes together. It has super-user privileges and so it's imperative that its // user is used for connecting the nodes together. It has super-user privileges and so it's imperative that its
// password is changed from the default (as warned in the docs). Since we don't need this feature we turn it off // password be changed from the default (as warned in the docs). Since we don't need this feature we turn it off
// by having its password be an unknown securely random 128-bit value. // by having its password be an unknown securely random 128-bit value.
clusterPassword = BigInteger(128, newSecureRandom()).toString(16) clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
queueConfigurations = listOf(
queueConfigurations.addAll(listOf( queueConfig(NETWORK_MAP_QUEUE, durable = true),
CoreQueueConfiguration().apply { queueConfig(P2P_QUEUE, durable = true),
address = NETWORK_MAP_ADDRESS
name = NETWORK_MAP_ADDRESS
isDurable = true
},
CoreQueueConfiguration().apply {
address = P2P_QUEUE
name = P2P_QUEUE
isDurable = true
},
// Create an RPC queue: this will service locally connected clients only (not via a bridge) and those // Create an RPC queue: this will service locally connected clients only (not via a bridge) and those
// clients must have authenticated. We could use a single consumer for everything and perhaps we should, // clients must have authenticated. We could use a single consumer for everything and perhaps we should,
// but these queues are not worth persisting. // but these queues are not worth persisting.
CoreQueueConfiguration().apply { queueConfig(RPC_REQUESTS_QUEUE, durable = false),
name = RPC_REQUESTS_QUEUE
address = RPC_REQUESTS_QUEUE
isDurable = false
},
// The custom name for the queue is intentional - we may wish other things to subscribe to the // The custom name for the queue is intentional - we may wish other things to subscribe to the
// NOTIFICATIONS_ADDRESS with different filters in future // NOTIFICATIONS_ADDRESS with different filters in future
CoreQueueConfiguration().apply { queueConfig(RPC_QUEUE_REMOVALS_QUEUE, address = NOTIFICATIONS_ADDRESS, filter = "_AMQ_NotifType = 1", durable = false)
name = RPC_QUEUE_REMOVALS_QUEUE )
address = NOTIFICATIONS_ADDRESS
isDurable = false
filterString = "_AMQ_NotifType = 1"
}
))
configureAddressSecurity() configureAddressSecurity()
} }
private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
return CoreQueueConfiguration().apply {
this.name = name
this.address = address
filterString = filter
isDurable = durable
}
}
/** /**
* Authenticated clients connecting to us fall in one of three groups: * Authenticated clients connecting to us fall in one of three groups:
* 1. The node hosting us and any of its logically connected components. These are given full access to all valid queues. * 1. The node hosting us and any of its logically connected components. These are given full access to all valid queues.
@ -279,45 +210,114 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
} }
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
val ourRootCAPublicKey = X509Utilities val rootCAPublicKey = X509Utilities
.loadCertificateFromKeyStore(config.trustStorePath, config.trustStorePassword, CORDA_ROOT_CA) .loadCertificateFromKeyStore(config.trustStorePath, config.trustStorePassword, CORDA_ROOT_CA)
.publicKey .publicKey
val ourPublicKey = X509Utilities val ourCertificate = X509Utilities
.loadCertificateFromKeyStore(config.keyStorePath, config.keyStorePassword, CORDA_CLIENT_CA) .loadCertificateFromKeyStore(config.keyStorePath, config.keyStorePassword, CORDA_CLIENT_CA)
.publicKey val ourSubjectDN = X500Name(ourCertificate.subjectDN.name)
// This is a sanity check and should not fail unless things have been misconfigured
require(ourSubjectDN.commonName == config.myLegalName) {
"Legal name does not match with our subject CN: $ourSubjectDN"
}
val securityConfig = object : SecurityConfiguration() { val securityConfig = object : SecurityConfiguration() {
// Override to make it work with our login module // Override to make it work with our login module
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf( val options = mapOf(
RPCUserService::class.java.name to userService, RPCUserService::class.java.name to userService,
CORDA_ROOT_CA to ourRootCAPublicKey, CORDA_ROOT_CA to rootCAPublicKey,
CORDA_CLIENT_CA to ourPublicKey) CORDA_CLIENT_CA to ourCertificate.publicKey)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options)) return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
} }
} }
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig) return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
} }
private fun connectorExists(hostAndPort: HostAndPort) = hostAndPort.toString() in activeMQServer.configuration.connectorConfigurations private fun deployBridgesFromNewQueue(queueName: String) {
log.debug { "Queue created: $queueName, deploying bridge(s)" }
private fun addConnector(hostAndPort: HostAndPort) = activeMQServer.configuration.addConnectorConfiguration( fun deployBridgeToPeer(nodeInfo: NodeInfo) {
hostAndPort.toString(), log.debug("Deploying bridge for $queueName to $nodeInfo")
tcpTransport(OUTBOUND, hostAndPort.hostText, hostAndPort.port) val address = nodeInfo.address
) if (address is ArtemisPeerAddress) {
deployBridge(queueName, address.hostAndPort, nodeInfo.legalIdentity.name)
private fun bridgeExists(name: String) = activeMQServer.clusterManager.bridges.containsKey(name) } else {
log.error("Don't know how to deal with $address for queue $queueName")
fun deployBridgeIfAbsent(queueName: SimpleString, hostAndPort: HostAndPort) {
if (!connectorExists(hostAndPort)) {
addConnector(hostAndPort)
}
val bridgeName = getBridgeName(queueName, hostAndPort)
if (!bridgeExists(bridgeName)) {
deployBridge(bridgeName, queueName, hostAndPort)
} }
} }
private fun getBridgeName(queueName: SimpleString, hostAndPort: HostAndPort) = "$queueName -> $hostAndPort" when {
queueName.startsWith(PEERS_PREFIX) -> try {
val identity = CompositeKey.parseFromBase58(queueName.substring(PEERS_PREFIX.length))
val nodeInfo = networkMapCache.getNodeByLegalIdentityKey(identity)
if (nodeInfo != null) {
deployBridgeToPeer(nodeInfo)
} else {
log.error("Queue created for a peer that we don't know from the network map: $queueName")
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse peer queue name as Base 58: $queueName")
}
queueName.startsWith(SERVICES_PREFIX) -> try {
val identity = CompositeKey.parseFromBase58(queueName.substring(SERVICES_PREFIX.length))
val nodeInfos = networkMapCache.getNodesByAdvertisedServiceIdentityKey(identity)
// Create a bridge for each node advertising the service.
for (nodeInfo in nodeInfos) {
deployBridgeToPeer(nodeInfo)
}
} catch (e: AddressFormatException) {
log.error("Flow violation: Could not parse service queue name as Base 58: $queueName")
}
}
}
/**
* The bridge will be created automatically when the queues are created, however, this is not the case when the network map restarted.
* The queues are restored from the journal, and because the queues are added before we register the callback handler, this method will never get called for existing queues.
* This results in message queues up and never get send out. (https://github.com/corda/corda/issues/37)
*
* We create the bridges indirectly now because the network map is not persisted and there are no ways to obtain host and port information on startup.
* TODO : Create the bridge directly from the list of queues on start up when we have a persisted network map service.
*/
private fun updateBridgesOnNetworkChange(change: MapChange) {
fun gatherAddresses(node: NodeInfo): Sequence<ArtemisPeerAddress> {
val peerAddress = node.address as ArtemisPeerAddress
val addresses = mutableListOf(peerAddress)
node.advertisedServices.mapTo(addresses) { NodeAddress.asService(it.identity.owningKey, peerAddress.hostAndPort) }
return addresses.asSequence()
}
fun deployBridges(node: NodeInfo) {
gatherAddresses(node)
.filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) }
.forEach { deployBridge(it, node.legalIdentity.name) }
}
fun destroyBridges(node: NodeInfo) {
gatherAddresses(node).forEach {
activeMQServer.destroyBridge(it.bridgeName)
}
}
when (change) {
is MapChange.Added -> {
deployBridges(change.node)
}
is MapChange.Removed -> {
destroyBridges(change.node)
}
is MapChange.Modified -> {
// TODO Figure out what has actually changed and only destroy those bridges that need to be.
destroyBridges(change.previousNode)
deployBridges(change.node)
}
}
}
private fun deployBridge(address: ArtemisPeerAddress, legalName: String) {
deployBridge(address.queueName, address.hostAndPort, legalName)
}
/** /**
* All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
@ -325,14 +325,25 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
* P2P address. * P2P address.
*/ */
private fun deployBridge(bridgeName: String, queueName: SimpleString, hostAndPort: HostAndPort) { private fun deployBridge(queueName: String, target: HostAndPort, legalName: String) {
val tcpTransport = tcpTransport(Outbound(expectedCommonName = legalName), target.hostText, target.port)
tcpTransport.params[ArtemisMessagingServer::class.java.name] = this
// We intentionally overwrite any previous connector config in case the peer legal name changed
activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport)
activeMQServer.deployBridge(BridgeConfiguration().apply { activeMQServer.deployBridge(BridgeConfiguration().apply {
name = bridgeName name = getBridgeName(queueName, target)
this.queueName = queueName.toString() this.queueName = queueName
forwardingAddress = P2P_QUEUE forwardingAddress = P2P_QUEUE
staticConnectors = listOf(hostAndPort.toString()) staticConnectors = listOf(target.toString())
confirmationWindowSize = 100000 // a guess confirmationWindowSize = 100000 // a guess
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
// We keep trying until the network map deems the node unreachable and tells us it's been removed at which
// point we destroy the bridge
// TODO Give some thought to the retry settings
retryInterval = 5.seconds.toMillis()
retryIntervalMultiplier = 1.5 // Exponential backoff
maxRetryInterval = 3.minutes.toMillis()
// As a peer of the target node we must connect to it using the peer user. Actual authentication is done using // As a peer of the target node we must connect to it using the peer user. Actual authentication is done using
// our TLS certificate. // our TLS certificate.
user = PEER_USER user = PEER_USER
@ -340,6 +351,83 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
}) })
} }
private fun queueExists(queueName: String): Boolean = activeMQServer.queueQuery(SimpleString(queueName)).isExists
private fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName)
private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort)
private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort"
// This is called on one of Artemis' background threads
internal fun hostVerificationFail(peerLegalName: String, expectedCommonName: String) {
log.error("Peer has wrong CN - expected $expectedCommonName but got $peerLegalName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!")
if (expectedCommonName == config.networkMapService?.legalName) {
// If the peer that failed host verification was the network map node then we're in big trouble and need to bail!
_networkMapConnectionFuture!!.setException(IOException("${config.networkMapService} failed host verification check"))
}
}
// This is called on one of Artemis' background threads
internal fun onTcpConnection(peerLegalName: String) {
if (peerLegalName == config.networkMapService?.legalName) {
_networkMapConnectionFuture!!.set(Unit)
}
}
}
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
override fun createConnector(configuration: MutableMap<String, Any>?,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?): Connector {
return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool,
protocolManager)
}
}
private class VerifyingNettyConnector(configuration: MutableMap<String, Any>?,
handler: BufferHandler?,
listener: ClientConnectionLifeCycleListener?,
closeExecutor: Executor?,
threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?) :
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager)
{
private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer
private val expectedCommonName = configuration?.get(ArtemisMessagingComponent.VERIFY_PEER_COMMON_NAME) as? String
override fun createConnection(): Connection? {
val connection = super.createConnection() as NettyConnection?
if (connection != null && expectedCommonName != null) {
val peerLegalName = connection
.channel
.pipeline()
.get(SslHandler::class.java)
.engine()
.session
.peerPrincipal
.name
.let(::X500Name)
.commonName
// TODO Verify on the entire principle (subject)
if (peerLegalName != expectedCommonName) {
connection.close()
server!!.hostVerificationFail(peerLegalName, expectedCommonName)
return null // Artemis will keep trying to reconnect until it's told otherwise
} else {
server!!.onTcpConnection(peerLegalName)
}
}
return connection
}
}
/** /**
* Clients must connect to us with a username and password and must use TLS. If a someone connects with * 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 * [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
@ -352,7 +440,6 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
* valid RPC users. RPC clients are given permission to perform RPC and nothing else. * valid RPC users. RPC clients are given permission to perform RPC and nothing else.
*/ */
class NodeLoginModule : LoginModule { class NodeLoginModule : LoginModule {
companion object { companion object {
// Include forbidden username character to prevent name clash with any RPC usernames // Include forbidden username character to prevent name clash with any RPC usernames
const val PEER_ROLE = "SystemRoles/Peer" const val PEER_ROLE = "SystemRoles/Peer"
@ -394,6 +481,18 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
val validatedUser = if (username == PEER_USER || username == NODE_USER) { val validatedUser = if (username == PEER_USER || username == NODE_USER) {
val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?") val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?")
authenticateNode(certificates, username)
} else {
// Otherwise assume they're an RPC user
authenticateRpcUser(password, username)
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
}
private fun authenticateNode(certificates: Array<X509Certificate>, username: String): String {
val peerCertificate = certificates.first() val peerCertificate = certificates.first()
val role = if (username == NODE_USER) { val role = if (username == NODE_USER) {
if (peerCertificate.publicKey != ourPublicKey) { if (peerCertificate.publicKey != ourPublicKey) {
@ -408,9 +507,10 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
PEER_ROLE // This enables the peer to send to our P2P address PEER_ROLE // This enables the peer to send to our P2P address
} }
principals += RolePrincipal(role) principals += RolePrincipal(role)
peerCertificate.subjectDN.name return peerCertificate.subjectDN.name
} else { }
// Otherwise assume they're an RPC user
private fun authenticateRpcUser(password: String, username: String): String {
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist") val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
if (password != rpcUser.password) { if (password != rpcUser.password) {
// TODO Switch to hashed passwords // TODO Switch to hashed passwords
@ -419,12 +519,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
} }
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
principals += RolePrincipal("$CLIENTS_PREFIX$username") // This enables the RPC client to receive responses principals += RolePrincipal("$CLIENTS_PREFIX$username") // This enables the RPC client to receive responses
username return username
}
principals += UserPrincipal(validatedUser)
loginSucceeded = true
return loginSucceeded
} }
override fun commit(): Boolean { override fun commit(): Boolean {
@ -450,4 +545,3 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
loginSucceeded = false loginSucceeded = false
} }
} }
}

View File

@ -4,6 +4,7 @@ import com.google.common.net.HostAndPort
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.node.services.config.NodeSSLConfiguration import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
import org.apache.activemq.artemis.api.core.ActiveMQException import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientSession import org.apache.activemq.artemis.api.core.client.ClientSession
@ -35,7 +36,7 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
state.locked { state.locked {
check(!running) check(!running)
checkStorePasswords() checkStorePasswords()
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.OUTBOUND, host.hostText, host.port)) val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(Outbound(), host.hostText, host.port))
serverLocator.threadPoolMaxSize = 1 serverLocator.threadPoolMaxSize = 1
// TODO: Configure session reconnection, confirmation window sizes and other Artemis features. // TODO: Configure session reconnection, confirmation window sizes and other Artemis features.
// This will allow reconnection in case of server restart/network outages/IP address changes, etc. // This will allow reconnection in case of server restart/network outages/IP address changes, etc.

View File

@ -14,6 +14,7 @@ import net.corda.core.utilities.trace
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
import net.corda.node.services.api.MessagingServiceInternal import net.corda.node.services.api.MessagingServiceInternal
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
import net.corda.node.utilities.* import net.corda.node.utilities.*
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
@ -66,13 +67,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
// confusion. // confusion.
const val TOPIC_PROPERTY = "platform-topic" const val TOPIC_PROPERTY = "platform-topic"
const val SESSION_ID_PROPERTY = "session-id" const val SESSION_ID_PROPERTY = "session-id"
/**
* This should be the only way to generate an ArtemisAddress and that only of the remote NetworkMapService node.
* All other addresses come from the NetworkMapCache, or myAddress below.
* The node will populate with their own identity based address when they register with the NetworkMapService.
*/
fun makeNetworkMapAddress(hostAndPort: HostAndPort): SingleMessageRecipient = NetworkMapAddress(hostAndPort)
} }
private class InnerState { private class InnerState {
@ -118,7 +112,8 @@ class NodeMessagingClient(override val config: NodeConfiguration,
started = true started = true
log.info("Connecting to server: $serverHostPort") log.info("Connecting to server: $serverHostPort")
val tcpTransport = tcpTransport(ConnectionDirection.OUTBOUND, serverHostPort.hostText, serverHostPort.port) // TODO Add broker CN to config for host verification in case the embedded broker isn't used
val tcpTransport = tcpTransport(Outbound(), serverHostPort.hostText, serverHostPort.port)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport)
clientFactory = locator.createSessionFactory() clientFactory = locator.createSessionFactory()
@ -375,10 +370,10 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
} }
private fun getMQAddress(target: MessageRecipients): SimpleString { private fun getMQAddress(target: MessageRecipients): String {
return if (target == myAddress) { return if (target == myAddress) {
// If we are sending to ourselves then route the message directly to our P2P queue. // If we are sending to ourselves then route the message directly to our P2P queue.
SimpleString(P2P_QUEUE) P2P_QUEUE
} else { } else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the // Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue. // broker's job to route the message to the target's P2P queue.
@ -391,9 +386,9 @@ class NodeMessagingClient(override val config: NodeConfiguration,
} }
/** Attempts to create a durable queue on the broker which is bound to an address of the same name. */ /** Attempts to create a durable queue on the broker which is bound to an address of the same name. */
private fun createQueueIfAbsent(queueName: SimpleString) { private fun createQueueIfAbsent(queueName: String) {
state.alreadyLocked { state.alreadyLocked {
val queueQuery = session!!.queueQuery(queueName) val queueQuery = session!!.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) { if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address") log.info("Create fresh queue $queueName bound on same address")
session!!.createQueue(queueName, queueName, true) session!!.createQueue(queueName, queueName, true)

View File

@ -32,6 +32,7 @@ import net.corda.flows.CashFlowResult
import net.corda.node.internal.AbstractNode import net.corda.node.internal.AbstractNode
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
@ -197,18 +198,8 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
register(NetworkMapCache.MapChange.Added::class.java) register(NetworkMapCache.MapChange.Added::class.java)
register(NetworkMapCache.MapChange.Removed::class.java) register(NetworkMapCache.MapChange.Removed::class.java)
register(NetworkMapCache.MapChange.Modified::class.java) register(NetworkMapCache.MapChange.Modified::class.java)
register(ArtemisMessagingComponent.NodeAddress::class.java, register(ArtemisMessagingComponent.NodeAddress::class.java)
read = { kryo, input -> register(NetworkMapAddress::class.java)
ArtemisMessagingComponent.NodeAddress(
kryo.readObject(input, SimpleString::class.java),
kryo.readObject(input, HostAndPort::class.java))
},
write = { kryo, output, nodeAddress ->
kryo.writeObject(output, nodeAddress.queueName)
kryo.writeObject(output, nodeAddress.hostAndPort)
}
)
register(NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString("localhost:0")).javaClass)
register(ServiceInfo::class.java) register(ServiceInfo::class.java)
register(ServiceType.getServiceType("ab", "ab").javaClass) register(ServiceType.getServiceType("ab", "ab").javaClass)
register(ServiceType.parse("ab").javaClass) register(ServiceType.parse("ab").javaClass)

View File

@ -208,6 +208,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
serviceHub.networkService.addMessageHandler(sessionTopic) { message, reg -> serviceHub.networkService.addMessageHandler(sessionTopic) { message, reg ->
executor.checkOnThread() executor.checkOnThread()
val sessionMessage = message.data.deserialize<SessionMessage>() val sessionMessage = message.data.deserialize<SessionMessage>()
// TODO Look up the party with the full X.500 name instead of just the legal name
val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peer.commonName)?.legalIdentity val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peer.commonName)?.legalIdentity
if (otherParty != null) { if (otherParty != null) {
when (sessionMessage) { when (sessionMessage) {

View File

@ -23,6 +23,7 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
import net.corda.testing.TestNodeConfiguration
import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -35,7 +36,6 @@ import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import java.io.Closeable import java.io.Closeable
import java.net.ServerSocket import java.net.ServerSocket
import java.nio.file.Path
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -68,17 +68,10 @@ class ArtemisMessagingTests {
@Before @Before
fun setUp() { fun setUp() {
userService = RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.empty())) userService = RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.empty()))
// TODO: create a base class that provides a default implementation config = TestNodeConfiguration(
config = object : NodeConfiguration { basedir = temporaryFolder.newFolder().toPath(),
override val basedir: Path = temporaryFolder.newFolder().toPath() myLegalName = "me",
override val myLegalName: String = "me" networkMapService = null)
override val nearestCity: String = "London"
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "testpass"
override val trustStorePassword: String = "trustpass"
}
LogHelper.setLevel(PersistentUniquenessProvider::class) LogHelper.setLevel(PersistentUniquenessProvider::class)
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
dataSource = dataSourceAndDatabase.first dataSource = dataSourceAndDatabase.first

View File

@ -8,11 +8,10 @@ import net.corda.core.crypto.X509Utilities
import net.corda.core.div import net.corda.core.div
import net.corda.core.exists import net.corda.core.exists
import net.corda.core.readLines import net.corda.core.readLines
import net.corda.node.services.config.NodeConfiguration import net.corda.testing.TestNodeConfiguration
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -20,11 +19,10 @@ import kotlin.test.assertTrue
class CertificateSignerTest { class CertificateSignerTest {
@Rule @Rule
@JvmField @JvmField
val tempFolder: TemporaryFolder = TemporaryFolder() val tempFolder = TemporaryFolder()
@Test @Test
fun buildKeyStore() { fun buildKeyStore() {
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
val certs = arrayOf(X509Utilities.createSelfSignedCACert("CORDA_CLIENT_CA").certificate, val certs = arrayOf(X509Utilities.createSelfSignedCACert("CORDA_CLIENT_CA").certificate,
@ -36,17 +34,10 @@ class CertificateSignerTest {
on { retrieveCertificates(eq(id)) }.then { certs } on { retrieveCertificates(eq(id)) }.then { certs }
} }
val config = TestNodeConfiguration(
val config = object : NodeConfiguration { basedir = tempFolder.root.toPath(),
override val basedir: Path = tempFolder.root.toPath() myLegalName = "me",
override val myLegalName: String = "me" networkMapService = null)
override val nearestCity: String = "London"
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "testpass"
override val trustStorePassword: String = "trustpass"
}
assertFalse(config.keyStorePath.exists()) assertFalse(config.keyStorePath.exists())
assertFalse(config.trustStorePath.exists()) assertFalse(config.trustStorePath.exists())
@ -76,5 +67,4 @@ class CertificateSignerTest {
assertEquals(id, (config.certificatesPath / "certificate-request-id.txt").readLines { it.findFirst().get() }) assertEquals(id, (config.certificatesPath / "certificate-request-id.txt").readLines { it.findFirst().get() })
} }
} }

View File

@ -1 +1 @@
gradlePluginsVersion=0.6.2 gradlePluginsVersion=0.6.3

View File

@ -18,10 +18,13 @@ import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
import net.corda.testing.node.* import net.corda.testing.TestNodeConfiguration
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.TestClock
import net.corda.testing.node.setTo
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -47,7 +50,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
// This puts together a mock network of SimulatedNodes. // This puts together a mock network of SimulatedNodes.
open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?, open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?,
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, keyPair) { advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?)
: MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, keyPair) {
override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity] override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
} }
@ -59,19 +63,12 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
val letter = 'A' + counter val letter = 'A' + counter
val city = bankLocations[counter++ % bankLocations.size] val city = bankLocations[counter++ % bankLocations.size]
// TODO: create a base class that provides a default implementation val cfg = TestNodeConfiguration(
val cfg = object : NodeConfiguration { basedir = config.basedir,
override val basedir: Path = config.basedir
// TODO: Set this back to "Bank of $city" after video day. // TODO: Set this back to "Bank of $city" after video day.
override val myLegalName: String = "Bank $letter" myLegalName = "Bank $letter",
override val nearestCity: String = city nearestCity = city,
override val emailAddress: String = "" networkMapService = null)
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties = makeTestDataSourceProperties()
}
return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair)
} }
@ -88,20 +85,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override fun create(config: NodeConfiguration, network: MockNetwork, override fun create(config: NodeConfiguration, network: MockNetwork,
networkMapAddr: SingleMessageRecipient?, advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode { networkMapAddr: SingleMessageRecipient?, advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
require(advertisedServices.containsType(NetworkMapService.type)) require(advertisedServices.containsType(NetworkMapService.type))
val cfg = TestNodeConfiguration(
// TODO: create a base class that provides a default implementation basedir = config.basedir,
val cfg = object : NodeConfiguration { myLegalName = "Network coordination center",
override val basedir: Path = config.basedir nearestCity = "Amsterdam",
override val myLegalName: String = "Network coordination center" networkMapService = null)
override val nearestCity: String = "Amsterdam"
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties = makeTestDataSourceProperties()
}
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {} return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {}
} }
} }
@ -110,19 +98,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode { advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
require(advertisedServices.containsType(SimpleNotaryService.type)) require(advertisedServices.containsType(SimpleNotaryService.type))
val cfg = TestNodeConfiguration(
// TODO: create a base class that provides a default implementation basedir = config.basedir,
val cfg = object : NodeConfiguration { myLegalName = "Notary Service",
override val basedir: Path = config.basedir nearestCity = "Zurich",
override val myLegalName: String = "Notary Service" networkMapService = null)
override val nearestCity: String = "Zurich"
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties = makeTestDataSourceProperties()
}
return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair)
} }
} }
@ -131,20 +111,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode { advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
require(advertisedServices.containsType(NodeInterestRates.type)) require(advertisedServices.containsType(NodeInterestRates.type))
val cfg = TestNodeConfiguration(
// TODO: create a base class that provides a default implementation basedir = config.basedir,
val cfg = object : NodeConfiguration { myLegalName = "Rates Service Provider",
override val basedir: Path = config.basedir nearestCity = "Madrid",
override val myLegalName: String = "Rates Service Provider" networkMapService = null)
override val nearestCity: String = "Madrid"
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties = makeTestDataSourceProperties()
}
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) { return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
override fun start(): MockNetwork.MockNode { override fun start(): MockNetwork.MockNode {
super.start() super.start()
@ -162,26 +133,16 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
object RegulatorFactory : MockNetwork.Factory { object RegulatorFactory : MockNetwork.Factory {
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode { advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
val cfg = TestNodeConfiguration(
// TODO: create a base class that provides a default implementation basedir = config.basedir,
val cfg = object : NodeConfiguration { myLegalName = "Regulator A",
override val basedir: Path = config.basedir nearestCity = "Paris",
override val myLegalName: String = "Regulator A" networkMapService = null)
override val nearestCity: String = "Paris" return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties = makeTestDataSourceProperties()
}
val n = object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
// But that's fine for visualisation purposes. // But that's fine for visualisation purposes.
} }
return n
} }
} }

View File

@ -15,14 +15,20 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.node.internal.AbstractNode import net.corda.node.internal.AbstractNode
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager.Change import net.corda.node.services.statemachine.StateMachineManager.Change
import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.ADD
import net.corda.testing.node.MockIdentityService import net.corda.testing.node.MockIdentityService
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties
import rx.Subscriber import rx.Subscriber
import java.net.ServerSocket import java.net.ServerSocket
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.time.Duration
import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -160,3 +166,22 @@ inline fun <reified P : FlowLogic<*>> AbstractNode.initiateSingleShotFlow(
} }
fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name)) fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name))
inline fun elapsedTime(block: () -> Unit): Duration {
val start = System.nanoTime()
block()
val end = System.nanoTime()
return Duration.ofNanos(end-start)
}
data class TestNodeConfiguration(
override val basedir: Path,
override val myLegalName: String,
override val networkMapService: NetworkMapInfo?,
override val keyStorePassword: String = "cordacadevpass",
override val trustStorePassword: String = "trustpass",
override val dataSourceProperties: Properties = makeTestDataSourceProperties(myLegalName),
override val nearestCity: String = "Null Island",
override val emailAddress: String = "",
override val exportJMXto: String = "",
override val devMode: Boolean = true) : NodeConfiguration

View File

@ -4,20 +4,20 @@ import com.google.common.net.HostAndPort
import net.corda.node.services.config.NodeSSLConfiguration import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.config.configureTestSSL import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.OUTBOUND import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.*
/** /**
* As the name suggests this is a simple client for connecting to MQ brokers. * As the name suggests this is a simple client for connecting to MQ brokers.
*/ */
class SimpleMQClient(val target: HostAndPort) : ArtemisMessagingComponent() { class SimpleMQClient(val target: HostAndPort,
override val config: NodeSSLConfiguration = configureTestSSL() override val config: NodeSSLConfiguration = configureTestSSL("SimpleMQClient")) : ArtemisMessagingComponent() {
lateinit var sessionFactory: ClientSessionFactory lateinit var sessionFactory: ClientSessionFactory
lateinit var session: ClientSession lateinit var session: ClientSession
lateinit var producer: ClientProducer lateinit var producer: ClientProducer
fun start(username: String? = null, password: String? = null) { fun start(username: String? = null, password: String? = null) {
val tcpTransport = tcpTransport(OUTBOUND, target.hostText, target.port) val tcpTransport = tcpTransport(Outbound(), target.hostText, target.port)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
isBlockOnNonDurableSend = true isBlockOnNonDurableSend = true
threadPoolMaxSize = 1 threadPoolMaxSize = 1

View File

@ -3,6 +3,7 @@ package net.corda.testing.node
import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Configuration.unix
import com.google.common.jimfs.Jimfs import com.google.common.jimfs.Jimfs
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.* import net.corda.core.*
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
@ -24,9 +25,9 @@ import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.testing.TestNodeConfiguration
import org.slf4j.Logger import org.slf4j.Logger
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -47,7 +48,7 @@ import java.util.concurrent.atomic.AtomicInteger
*/ */
class MockNetwork(private val networkSendManuallyPumped: Boolean = false, class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
private val threadPerNode: Boolean = false, private val threadPerNode: Boolean = false,
private val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy =
InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(),
private val defaultFactory: Factory = MockNetwork.DefaultFactory) { private val defaultFactory: Factory = MockNetwork.DefaultFactory) {
private var nextNodeId = 0 private var nextNodeId = 0
@ -105,8 +106,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
} }
} }
open class MockNode(config: NodeConfiguration, val mockNet: MockNetwork, networkMapAddr: SingleMessageRecipient?, open class MockNode(config: NodeConfiguration,
advertisedServices: Set<ServiceInfo>, val id: Int, val keyPair: KeyPair?) : AbstractNode(config, networkMapAddr, advertisedServices, TestClock()) { val mockNet: MockNetwork,
override val networkMapAddress: SingleMessageRecipient?,
advertisedServices: Set<ServiceInfo>,
val id: Int,
val keyPair: KeyPair?) : AbstractNode(config, advertisedServices, TestClock()) {
override val log: Logger = loggerFor<MockNode>() override val log: Logger = loggerFor<MockNode>()
override val serverThread: AffinityExecutor = override val serverThread: AffinityExecutor =
if (mockNet.threadPerNode) if (mockNet.threadPerNode)
@ -140,7 +145,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
override fun generateKeyPair(): KeyPair = keyPair ?: super.generateKeyPair() override fun generateKeyPair(): KeyPair = keyPair ?: super.generateKeyPair()
// It's OK to not have a network map service in the mock network. // It's OK to not have a network map service in the mock network.
override fun noNetworkMapConfigured() = Futures.immediateFuture(Unit) override fun noNetworkMapConfigured(): ListenableFuture<Unit> = Futures.immediateFuture(Unit)
// There is no need to slow down the unit tests by initialising CityDatabase // There is no need to slow down the unit tests by initialising CityDatabase
override fun findMyLocation(): PhysicalLocation? = null override fun findMyLocation(): PhysicalLocation? = null
@ -193,18 +198,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
if (newNode) if (newNode)
(path / "attachments").createDirectories() (path / "attachments").createDirectories()
// TODO: create a base class that provides a default implementation val config = TestNodeConfiguration(
val config = object : NodeConfiguration { basedir = path,
override val basedir: Path = path myLegalName = legalName ?: "Mock Company $id",
override val myLegalName: String = legalName ?: "Mock Company $id" networkMapService = null,
override val nearestCity: String = "Atlantis" dataSourceProperties = makeTestDataSourceProperties("node_${id}_net_$networkId"))
override val emailAddress: String = ""
override val devMode: Boolean = true
override val exportJMXto: String = ""
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
override val dataSourceProperties: Properties get() = makeTestDataSourceProperties("node_${id}_net_$networkId")
}
val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair) val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair)
if (start) { if (start) {
node.setup().start() node.setup().start()

View File

@ -1,8 +1,10 @@
package net.corda.testing.node package net.corda.testing.node
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.createDirectories import net.corda.core.createDirectories
import net.corda.core.div import net.corda.core.div
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.map
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.User import net.corda.node.services.User
@ -17,7 +19,7 @@ import kotlin.concurrent.thread
/** /**
* Extend this class if you need to run nodes in a test. You could use the driver DSL but it's extremely slow for testing * Extend this class if you need to run nodes in a test. You could use the driver DSL but it's extremely slow for testing
* purposes. * purposes. Use the DSL if you need to run the nodes in separate processes otherwise this class will suffice.
*/ */
// TODO Some of the logic here duplicates what's in the driver // TODO Some of the logic here duplicates what's in the driver
abstract class NodeBasedTest { abstract class NodeBasedTest {
@ -50,7 +52,7 @@ abstract class NodeBasedTest {
rpcUsers: List<User> = emptyList(), rpcUsers: List<User> = emptyList(),
configOverrides: Map<String, Any> = emptyMap()): Node { configOverrides: Map<String, Any> = emptyMap()): Node {
check(_networkMapNode == null) check(_networkMapNode == null)
return startNodeInternal(legalName, advertisedServices, rpcUsers, configOverrides).apply { return startNodeInternal(legalName, advertisedServices, rpcUsers, configOverrides).getOrThrow().apply {
_networkMapNode = this _networkMapNode = this
} }
} }
@ -58,25 +60,28 @@ abstract class NodeBasedTest {
fun startNode(legalName: String, fun startNode(legalName: String,
advertisedServices: Set<ServiceInfo> = emptySet(), advertisedServices: Set<ServiceInfo> = emptySet(),
rpcUsers: List<User> = emptyList(), rpcUsers: List<User> = emptyList(),
configOverrides: Map<String, Any> = emptyMap()): Node { configOverrides: Map<String, Any> = emptyMap()): ListenableFuture<Node> {
return startNodeInternal( return startNodeInternal(
legalName, legalName,
advertisedServices, advertisedServices,
rpcUsers, rpcUsers,
configOverrides + mapOf( mapOf(
"networkMapAddress" to networkMapNode.configuration.artemisAddress.toString() "networkMapService" to mapOf(
"address" to networkMapNode.configuration.artemisAddress.toString(),
"legalName" to networkMapNode.info.legalIdentity.name
) )
) + configOverrides
) )
} }
private fun startNodeInternal(legalName: String, private fun startNodeInternal(legalName: String,
advertisedServices: Set<ServiceInfo>, advertisedServices: Set<ServiceInfo>,
rpcUsers: List<User>, rpcUsers: List<User>,
configOverrides: Map<String, Any>): Node { configOverrides: Map<String, Any>): ListenableFuture<Node> {
val config = ConfigHelper.loadConfig( val config = ConfigHelper.loadConfig(
baseDirectoryPath = (tempFolder.root.toPath() / legalName).createDirectories(), baseDirectoryPath = (tempFolder.root.toPath() / legalName).createDirectories(),
allowMissingConfig = true, allowMissingConfig = true,
configOverrides = configOverrides + mapOf( configOverrides = mapOf(
"myLegalName" to legalName, "myLegalName" to legalName,
"artemisAddress" to freeLocalHostAndPort().toString(), "artemisAddress" to freeLocalHostAndPort().toString(),
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","), "extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
@ -87,7 +92,7 @@ abstract class NodeBasedTest {
"permissions" to it.permissions "permissions" to it.permissions
) )
} }
) ) + configOverrides
) )
val node = FullNodeConfiguration(config).createNode() val node = FullNodeConfiguration(config).createNode()
@ -96,7 +101,6 @@ abstract class NodeBasedTest {
thread(name = legalName) { thread(name = legalName) {
node.run() node.run()
} }
node.networkMapRegistrationFuture.getOrThrow() return node.networkMapRegistrationFuture.map { node }
return node
} }
} }

View File

@ -0,0 +1,61 @@
package net.corda.testing.node
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.crypto.composite
import net.corda.core.crypto.generateKeyPair
import net.corda.core.messaging.RPCOps
import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.network.InMemoryNetworkMapCache
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.databaseTransaction
import net.corda.testing.freeLocalHostAndPort
import org.jetbrains.exposed.sql.Database
import java.io.Closeable
import java.security.KeyPair
import kotlin.concurrent.thread
/**
* This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or
* any other such task that would make it functionable in a network and thus left to the user to do so manually.
*/
class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort()) : AutoCloseable {
private val databaseWithCloseable: Pair<Closeable, Database> = configureDatabase(config.dataSourceProperties)
val database: Database get() = databaseWithCloseable.second
val userService = RPCUserServiceImpl(config)
val identity: KeyPair = generateKeyPair()
val executor = ServiceAffinityExecutor(config.myLegalName, 1)
val broker = ArtemisMessagingServer(config, address, InMemoryNetworkMapCache(), userService)
val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>()
val net = databaseTransaction(database) {
NodeMessagingClient(
config,
address,
identity.public.composite,
executor,
database,
networkMapRegistrationFuture)
}
fun start() {
broker.start()
net.start(
object : RPCOps { override val protocolVersion = 0 },
userService)
thread(name = config.myLegalName) {
net.run()
}
}
override fun close() {
net.stop()
broker.stop()
databaseWithCloseable.first.close()
executor.shutdownNow()
}
}