diff --git a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt index b9dee73411..8ce0228f31 100644 --- a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt +++ b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt @@ -12,10 +12,10 @@ import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.node.internal.Node import net.corda.node.services.User -import net.corda.node.services.config.configureTestSSL import net.corda.node.services.messaging.CordaRPCClient import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.testing.configureTestSSL import net.corda.testing.node.NodeBasedTest import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -37,7 +37,7 @@ class CordaRPCClientTest : NodeBasedTest() { @Before fun setUp() { node = startNode("Alice", rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow() - client = CordaRPCClient(node.configuration.artemisAddress, configureTestSSL()) + client = CordaRPCClient(node.configuration.rpcAddress!!) } @After diff --git a/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt b/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt index d72dd4342b..b6adeb68eb 100644 --- a/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt +++ b/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt @@ -25,8 +25,6 @@ import net.corda.flows.CashPaymentFlow import net.corda.node.driver.DriverBasedTest import net.corda.node.driver.driver import net.corda.node.services.User -import net.corda.node.services.config.configureTestSSL -import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.network.NetworkMapService import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService @@ -57,9 +55,10 @@ class NodeMonitorModelTest : DriverBasedTest() { ) val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser)) val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) - - aliceNode = aliceNodeFuture.getOrThrow().nodeInfo - notaryNode = notaryNodeFuture.getOrThrow().nodeInfo + val aliceNodeHandle = aliceNodeFuture.getOrThrow() + val notaryNodeHandle = notaryNodeFuture.getOrThrow() + aliceNode = aliceNodeHandle.nodeInfo + notaryNode = notaryNodeHandle.nodeInfo newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo } val monitor = NodeMonitorModel() @@ -70,7 +69,7 @@ class NodeMonitorModelTest : DriverBasedTest() { vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() - monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password) + monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) rpc = monitor.proxyObservable.value!! runTest() } diff --git a/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt b/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt index 8569835beb..0bb3561979 100644 --- a/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt +++ b/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt @@ -52,8 +52,8 @@ class NodeMonitorModel { * Register for updates to/from a given vault. * TODO provide an unsubscribe mechanism */ - fun register(nodeHostAndPort: HostAndPort, sslConfig: SSLConfiguration, username: String, password: String) { - val client = CordaRPCClient(nodeHostAndPort, sslConfig){ + fun register(nodeHostAndPort: HostAndPort, username: String, password: String) { + val client = CordaRPCClient(nodeHostAndPort){ maxRetryInterval = 10.seconds.toMillis() } client.start(username, password) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 2a871e0315..dee57da61a 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -6,6 +6,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC 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.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test @@ -14,6 +15,9 @@ import org.junit.Test * Runs the security tests with the attacker pretending to be a node on the network. */ class MQSecurityAsNodeTest : MQSecurityTest() { + override fun createAttacker(): SimpleMQClient { + return clientTo(alice.configuration.artemisAddress) + } override fun startAttacker(attacker: SimpleMQClient) { attacker.start(PEER_USER, PEER_USER) // Login as a peer @@ -47,4 +51,20 @@ class MQSecurityAsNodeTest : MQSecurityTest() { attacker.start() } } + + @Test + fun `login to a non ssl port as a node user`() { + val attacker = clientTo(alice.configuration.rpcAddress!!, sslConfiguration = null) + assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + attacker.start(NODE_USER, NODE_USER) + } + } + + @Test + fun `login to a non ssl port as a peer user`() { + val attacker = clientTo(alice.configuration.rpcAddress!!, sslConfiguration = null) + assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + attacker.start(PEER_USER, PEER_USER) // Login as a peer + } + } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt index f2b2bbc5db..266cd6d3bd 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt @@ -2,14 +2,35 @@ package net.corda.services.messaging import net.corda.node.services.User import net.corda.testing.messaging.SimpleMQClient +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 being a valid RPC user of Alice. */ class MQSecurityAsRPCTest : MQSecurityTest() { + override fun createAttacker(): SimpleMQClient { + return clientTo(alice.configuration.rpcAddress!!) + } + + @Test + fun `send message on logged in user's RPC address`() { + val user1Queue = loginToRPCAndGetClientQueue() + assertSendAttackFails(user1Queue) + } + override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet())) override fun startAttacker(attacker: SimpleMQClient) { attacker.loginToRPC(extraRPCUsers[0]) } -} \ No newline at end of file + + @Test + fun `login to a ssl port as a RPC user`() { + val attacker = clientTo(alice.configuration.artemisAddress) + assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { + attacker.loginToRPC(extraRPCUsers[0], enableSSL = true) + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index c0df706bcc..e1eb0e80f1 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -14,7 +14,6 @@ import net.corda.core.utilities.unwrap import net.corda.node.internal.Node import net.corda.node.services.User import net.corda.node.services.config.SSLConfiguration -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.INTERNAL_PREFIX import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_QUEUE @@ -24,6 +23,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_QUEUE_REMOVALS_QUEUE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE import net.corda.node.services.messaging.CordaRPCClientImpl +import net.corda.testing.configureTestSSL import net.corda.testing.messaging.SimpleMQClient import net.corda.testing.node.NodeBasedTest import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException @@ -49,12 +49,14 @@ abstract class MQSecurityTest : NodeBasedTest() { @Before fun start() { alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser).getOrThrow() - attacker = clientTo(alice.configuration.artemisAddress) + attacker = createAttacker() startAttacker(attacker) } open val extraRPCUsers: List get() = emptyList() + abstract fun createAttacker(): SimpleMQClient + abstract fun startAttacker(attacker: SimpleMQClient) @After @@ -112,12 +114,6 @@ abstract class MQSecurityTest : NodeBasedTest() { assertConsumeAttackFails(user1Queue) } - @Test - fun `send message on logged in user's RPC address`() { - val user1Queue = loginToRPCAndGetClientQueue() - assertSendAttackFails(user1Queue) - } - @Test fun `create queue for valid RPC user`() { val user1Queue = "$CLIENTS_PREFIX${rpcUser.username}.rpc.${random63BitValue()}" @@ -152,26 +148,26 @@ abstract class MQSecurityTest : NodeBasedTest() { assertAllQueueCreationAttacksFail(randomQueue) } - fun clientTo(target: HostAndPort, config: SSLConfiguration = configureTestSSL()): SimpleMQClient { - val client = SimpleMQClient(target, config) + fun clientTo(target: HostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient { + val client = SimpleMQClient(target, sslConfiguration) clients += client return client } fun loginToRPC(target: HostAndPort, rpcUser: User): SimpleMQClient { - val client = clientTo(target) + val client = clientTo(target, null) client.loginToRPC(rpcUser) return client } - fun SimpleMQClient.loginToRPC(rpcUser: User): CordaRPCOps { - start(rpcUser.username, rpcUser.password) + fun SimpleMQClient.loginToRPC(rpcUser: User, enableSSL: Boolean = false): CordaRPCOps { + start(rpcUser.username, rpcUser.password, enableSSL) val clientImpl = CordaRPCClientImpl(session, ReentrantLock(), rpcUser.username) return clientImpl.proxyFor(CordaRPCOps::class.java, timeout = 1.seconds) } fun loginToRPCAndGetClientQueue(): String { - val rpcClient = loginToRPC(alice.configuration.artemisAddress, rpcUser) + val rpcClient = loginToRPC(alice.configuration.rpcAddress!!, rpcUser) val clientQueueQuery = SimpleString("$CLIENTS_PREFIX${rpcUser.username}.rpc.*") return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString() } diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 8dfab7a2e7..5bb6f8028a 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -105,7 +105,7 @@ data class NodeHandle( val configuration: FullNodeConfiguration, val process: Process ) { - fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.artemisAddress, configuration) + fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!) } sealed class PortAllocation { @@ -343,7 +343,8 @@ open class DriverDSL( override fun startNode(providedName: String?, advertisedServices: Set, rpcUsers: List, customOverrides: Map): ListenableFuture { val messagingAddress = portAllocation.nextHostAndPort() - val apiAddress = portAllocation.nextHostAndPort() + val rpcAddress = portAllocation.nextHostAndPort() + val webAddress = portAllocation.nextHostAndPort() val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val name = providedName ?: "${pickA(name)}-${messagingAddress.port}" @@ -351,7 +352,8 @@ open class DriverDSL( val configOverrides = mapOf( "myLegalName" to name, "artemisAddress" to messagingAddress.toString(), - "webAddress" to apiAddress.toString(), + "rpcAddress" to rpcAddress.toString(), + "webAddress" to webAddress.toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "networkMapService" to mapOf( "address" to networkMapAddress.toString(), diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index bb0ecab4d8..43984c4466 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -22,11 +22,7 @@ import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient -import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.services.transactions.RaftUniquenessProvider -import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.node.services.transactions.BFTSmartUniquenessProvider -import net.corda.node.services.transactions.BFTValidatingNotaryService +import net.corda.node.services.transactions.* import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.databaseTransaction import org.jetbrains.exposed.sql.Database @@ -130,7 +126,7 @@ class Node(override val configuration: FullNodeConfiguration, val serverAddress = with(configuration) { messagingServerAddress ?: { - messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService) + messageBroker = ArtemisMessagingServer(this, artemisAddress, rpcAddress, services.networkMapCache, userService) artemisAddress }() } diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 9ad88565d5..6d552dae09 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -15,7 +15,6 @@ import net.corda.core.div import net.corda.core.exists import net.corda.core.utilities.loggerFor import java.net.URL -import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.time.Instant @@ -49,6 +48,9 @@ object ConfigHelper { @Suppress("UNCHECKED_CAST") operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { + if (metadata.returnType.isMarkedNullable && !hasPath(metadata.name)) { + return null as T + } return when (metadata.returnType.javaType) { String::class.java -> getString(metadata.name) as T Int::class.java -> getInt(metadata.name) as T @@ -100,7 +102,7 @@ inline fun Config.getListOrElse(path: String, default: Config. */ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName) -private fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) { +fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) { certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) @@ -112,15 +114,3 @@ private fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) } } - -// TODO Move this to CoreTestUtils.kt once we can pry this from the explorer -@JvmOverloads -fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration { - override val certificatesDirectory = Files.createTempDirectory("certs") - override val keyStorePassword: String get() = "cordacadevpass" - override val trustStorePassword: String get() = "trustpass" - - init { - configureDevKeyAndTrustStores(legalName) - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 44ae971d6a..6a76ddae34 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -65,13 +65,14 @@ class FullNodeConfiguration(override val baseDirectory: Path, val config: Config } val useHTTPS: Boolean by config val artemisAddress: HostAndPort by config + val rpcAddress: HostAndPort? by config val webAddress: HostAndPort by config // TODO This field is slightly redundant as artemisAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one - val messagingServerAddress: HostAndPort? by config.getOrElse { null } + val messagingServerAddress: HostAndPort? by config val extraAdvertisedServiceIds: List = config.getListOrElse("extraAdvertisedServiceIds") { emptyList() } val useTestClock: Boolean by config.getOrElse { false } - val notaryNodeAddress: HostAndPort? by config.getOrElse { null } + val notaryNodeAddress: HostAndPort? by config val notaryClusterAddresses: List = config .getListOrElse("notaryClusterAddresses") { emptyList() } .map { HostAndPort.fromString(it) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt index bb5dee0754..7aae91dc7e 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt @@ -21,7 +21,7 @@ import java.security.KeyStore /** * The base class for Artemis services that defines shared data structures and SSL transport configuration. */ -abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { +abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { companion object { init { System.setProperty("org.jboss.logging.provider", "slf4j") @@ -85,6 +85,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) } + fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort) } @@ -134,7 +135,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { } } - protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int): TransportConfiguration { + protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true): TransportConfiguration { val config = config val options = mutableMapOf( // Basic TCP target details @@ -148,7 +149,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP" ) - if (config != null) { + if (config != null && enableSSL) { config.keyStoreFile.expectedOnDefaultFileSystem() config.trustStoreFile.expectedOnDefaultFileSystem() val tlsOptions = mapOf( diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 9fb9eacbc5..fe17d4ccad 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -81,7 +81,8 @@ import javax.security.cert.X509Certificate */ @ThreadSafe class ArtemisMessagingServer(override val config: NodeConfiguration, - val myHostPort: HostAndPort, + val artemisHostPort: HostAndPort, + val rpcHostPort: HostAndPort?, val networkMapCache: NetworkMapCache, val userService: RPCUserService) : ArtemisMessagingComponent() { companion object { @@ -139,7 +140,10 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } } activeMQServer.start() - printBasicNodeInfo("Node listening on address", myHostPort.toString()) + printBasicNodeInfo("Node listening on address", artemisHostPort.toString()) + if (rpcHostPort != null) { + printBasicNodeInfo("Node RPC service listening on address", rpcHostPort.toString()) + } } private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply { @@ -147,7 +151,11 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, bindingsDirectory = (artemisDir / "bindings").toString() journalDirectory = (artemisDir / "journal").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString() - acceptorConfigurations = setOf(tcpTransport(Inbound, "0.0.0.0", myHostPort.port)) + val acceptors = mutableSetOf(tcpTransport(Inbound, "0.0.0.0", artemisHostPort.port)) + if (rpcHostPort != null) { + acceptors.add(tcpTransport(Inbound, "0.0.0.0", rpcHostPort.port, enableSSL = false)) + } + acceptorConfigurations = acceptors // Enable built in message deduplication. Note we still have to do our own as the delayed commits // and our own definition of commit mean that the built in deduplication cannot remove all duplicates. idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess @@ -160,15 +168,15 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, // by having its password be an unknown securely random 128-bit value. clusterPassword = BigInteger(128, newSecureRandom()).toString(16) queueConfigurations = listOf( - queueConfig(NETWORK_MAP_QUEUE, durable = true), - queueConfig(P2P_QUEUE, durable = true), - // 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, - // but these queues are not worth persisting. - queueConfig(RPC_REQUESTS_QUEUE, durable = false), - // The custom name for the queue is intentional - we may wish other things to subscribe to the - // NOTIFICATIONS_ADDRESS with different filters in future - queueConfig(RPC_QUEUE_REMOVALS_QUEUE, address = NOTIFICATIONS_ADDRESS, filter = "_AMQ_NotifType = 1", durable = false) + queueConfig(NETWORK_MAP_QUEUE, durable = true), + queueConfig(P2P_QUEUE, durable = true), + // 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, + // but these queues are not worth persisting. + queueConfig(RPC_REQUESTS_QUEUE, durable = false), + // The custom name for the queue is intentional - we may wish other things to subscribe to the + // NOTIFICATIONS_ADDRESS with different filters in future + queueConfig(RPC_QUEUE_REMOVALS_QUEUE, address = NOTIFICATIONS_ADDRESS, filter = "_AMQ_NotifType = 1", durable = false) ) configureAddressSecurity() } @@ -290,8 +298,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, fun deployBridges(node: NodeInfo) { gatherAddresses(node) - .filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) } - .forEach { deployBridge(it, node.legalIdentity.name) } + .filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) } + .forEach { deployBridge(it, node.legalIdentity.name) } } fun destroyBridges(node: NodeInfo) { @@ -397,8 +405,7 @@ private class VerifyingNettyConnector(configuration: MutableMap?, threadPool: Executor?, scheduledThreadPool: ScheduledExecutorService?, protocolManager: ClientProtocolManager?) : - NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) -{ + 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 @@ -480,15 +487,15 @@ class NodeLoginModule : LoginModule { val username = nameCallback.name ?: throw FailedLoginException("Username not provided") val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided")) + val certificates = certificateCallback.certificates log.info("Processing login for $username") - val validatedUser = if (username == PEER_USER || username == NODE_USER) { - val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?") - authenticateNode(certificates, username) - } else { - // Otherwise assume they're an RPC user - authenticateRpcUser(password, username) + val validatedUser = when (determineUserRole(certificates, username)) { + PEER_ROLE -> authenticatePeer(certificates) + NODE_ROLE -> authenticateNode(certificates) + RPC_ROLE -> authenticateRpcUser(password, username) + else -> throw FailedLoginException("Peer does not belong on our network") } principals += UserPrincipal(validatedUser) @@ -496,24 +503,24 @@ class NodeLoginModule : LoginModule { return loginSucceeded } - private fun authenticateNode(certificates: Array, username: String): String { + private fun authenticateNode(certificates: Array): String { val peerCertificate = certificates.first() - val role = if (username == NODE_USER) { - if (peerCertificate.publicKey != ourPublicKey) { - throw FailedLoginException("Only the node can login as $NODE_USER") - } - NODE_ROLE - } else { - val theirRootCAPublicKey = certificates.last().publicKey - if (theirRootCAPublicKey != ourRootCAPublicKey) { - throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey") - } - PEER_ROLE // This enables the peer to send to our P2P address + if (peerCertificate.publicKey != ourPublicKey) { + throw FailedLoginException("Only the node can login as $NODE_USER") } - principals += RolePrincipal(role) + principals += RolePrincipal(NODE_ROLE) return peerCertificate.subjectDN.name } + private fun authenticatePeer(certificates: Array): String { + val theirRootCAPublicKey = certificates.last().publicKey + if (theirRootCAPublicKey != ourRootCAPublicKey) { + throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey") + } + principals += RolePrincipal(PEER_ROLE) + return certificates.first().subjectDN.name + } + private fun authenticateRpcUser(password: String, username: String): String { val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist") if (password != rpcUser.password) { @@ -526,6 +533,18 @@ class NodeLoginModule : LoginModule { return username } + private fun determineUserRole(certificates: Array?, username: String): String? { + return if (username == PEER_USER || username == NODE_USER) { + certificates ?: throw FailedLoginException("No TLS?") + if (username == PEER_USER) PEER_ROLE else NODE_ROLE + } else if (certificates == null) { + // Assume they're an RPC user if its from a non-ssl connection + RPC_ROLE + } else { + null + } + } + override fun commit(): Boolean { val result = loginSucceeded if (result) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt index 7c68bd24b3..dabdcc2244 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt @@ -24,10 +24,10 @@ import javax.annotation.concurrent.ThreadSafe * useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works. * * @param host The hostname and messaging port of the node. - * @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will not be authenticated, nor will RPC traffic be encrypted. + * @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will only be authenticated on non-SSL RPC port, the RPC traffic with not be encrypted when SSL is disabled. */ @ThreadSafe -class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration?, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() { +class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() { private companion object { val log = loggerFor() } diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index cb3f8365ca..ee0a271848 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -47,6 +47,7 @@ class ArtemisMessagingTests { @Rule @JvmField val temporaryFolder = TemporaryFolder() val hostAndPort = freeLocalHostAndPort() + val rpcHostAndPort = freeLocalHostAndPort() val topic = "platform.self" val identity = generateKeyPair() @@ -230,8 +231,8 @@ class ArtemisMessagingTests { } } - private fun createMessagingServer(local: HostAndPort = hostAndPort): ArtemisMessagingServer { - return ArtemisMessagingServer(config, local, networkMapCache, userService).apply { + private fun createMessagingServer(local: HostAndPort = hostAndPort, rpc: HostAndPort = rpcHostAndPort): ArtemisMessagingServer { + return ArtemisMessagingServer(config, local, rpc, networkMapCache, userService).apply { config.configureWithDevSSLCertificate() messagingServer = this } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 5d4b01a4f2..b641f212cc 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -9,7 +9,6 @@ import net.corda.core.messaging.startFlow import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.node.services.config.configureTestSSL import net.corda.node.services.messaging.CordaRPCClient import net.corda.testing.http.HttpApi @@ -26,11 +25,12 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) { val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) return api.postJson("issue-asset-request", params) } + /** * RPC API */ fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { - val client = CordaRPCClient(hostAndPort, configureTestSSL()) + val client = CordaRPCClient(hostAndPort) // TODO: privileged security controls required client.start("bankUser", "test") val proxy = client.proxy() diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index ab739ac11e..083abef10e 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -55,7 +55,7 @@ class IRSDemoTest : IntegrationTestCategory { } fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable { - val client = CordaRPCClient(config.artemisAddress, config) + val client = CordaRPCClient(config.rpcAddress!!) client.start("user", "password") val proxy = client.proxy() val vaultUpdates = proxy.vaultAndUpdates().second diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 7e71e87780..d77e705425 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -27,7 +27,7 @@ class TraderDemoTest : NodeBasedTest() { ).getOrThrow() val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { - val client = CordaRPCClient(it.configuration.artemisAddress, it.configuration) + val client = CordaRPCClient(it.configuration.rpcAddress!!) client.start(demoUser[0].username, demoUser[0].password).proxy() } diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 08de15a6d9..09caba82ee 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -18,6 +18,8 @@ import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.node.internal.AbstractNode import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.SSLConfiguration +import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.AddOrRemove.ADD import net.corda.testing.node.MockIdentityService @@ -25,6 +27,7 @@ import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import java.net.ServerSocket import java.net.URL +import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.util.* @@ -161,3 +164,14 @@ data class TestNodeConfiguration( override val certificateSigningService: URL = URL("http://localhost")) : NodeConfiguration fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name)) + +@JvmOverloads +fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration { + override val certificatesDirectory = Files.createTempDirectory("certs") + override val keyStorePassword: String get() = "cordacadevpass" + override val trustStorePassword: String get() = "trustpass" + + init { + configureDevKeyAndTrustStores(legalName) + } +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index ccce09c277..d12d557e92 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -2,22 +2,22 @@ package net.corda.testing.messaging import com.google.common.net.HostAndPort import net.corda.node.services.config.SSLConfiguration -import net.corda.node.services.config.configureTestSSL import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound +import net.corda.testing.configureTestSSL import org.apache.activemq.artemis.api.core.client.* /** * As the name suggests this is a simple client for connecting to MQ brokers. */ class SimpleMQClient(val target: HostAndPort, - override val config: SSLConfiguration = configureTestSSL("SimpleMQClient")) : ArtemisMessagingComponent() { + override val config: SSLConfiguration? = configureTestSSL("SimpleMQClient")) : ArtemisMessagingComponent() { lateinit var sessionFactory: ClientSessionFactory lateinit var session: ClientSession lateinit var producer: ClientProducer - fun start(username: String? = null, password: String? = null) { - val tcpTransport = tcpTransport(Outbound(), target.hostText, target.port) + fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) { + val tcpTransport = tcpTransport(Outbound(), target.hostText, target.port, enableSSL) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { isBlockOnNonDurableSend = true threadPoolMaxSize = 1 diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 0056a816d5..7b595560e1 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -121,6 +121,7 @@ abstract class NodeBasedTest { configOverrides = mapOf( "myLegalName" to legalName, "artemisAddress" to freeLocalHostAndPort().toString(), + "rpcAddress" to freeLocalHostAndPort().toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "rpcUsers" to rpcUsers.map { mapOf( diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 3794a3a189..e198d2aa6e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -23,14 +23,14 @@ 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 { +class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), rpcAddress: HostAndPort = freeLocalHostAndPort()) : AutoCloseable { private val databaseWithCloseable: Pair = 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 broker = ArtemisMessagingServer(config, address, rpcAddress, InMemoryNetworkMapCache(), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create() val net = databaseTransaction(database) { NodeMessagingClient( diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt index e42537292e..81dd895b94 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt @@ -14,7 +14,6 @@ import net.corda.client.model.Models import net.corda.client.model.observableValue import net.corda.core.contracts.GBP import net.corda.core.contracts.USD -import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.explorer.model.CordaViewModel @@ -28,7 +27,6 @@ import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.node.driver.PortAllocation import net.corda.node.driver.driver import net.corda.node.services.User -import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import org.apache.commons.lang.SystemUtils @@ -141,7 +139,7 @@ fun main(args: Array) { val issuerNodeUSD = issuerUSD.get() arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach { - println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}") + println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}") } val parser = OptionParser("S") diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt index e41d3f629f..d437e28136 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt @@ -30,9 +30,6 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable { private var username: String by config private var reportingCurrency: Currency by config private var fullscreen: Boolean by config - private var certificatesDir: Path by config - private var keyStorePassword: String by config - private var trustStorePassword: String by config // Create observable Properties. val reportingCurrencyProperty = writableConfigProperty(SettingsModel::reportingCurrency) @@ -41,10 +38,6 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable { val portProperty = writableConfigProperty(SettingsModel::port) val usernameProperty = writableConfigProperty(SettingsModel::username) val fullscreenProperty = writableConfigProperty(SettingsModel::fullscreen) - val certificatesDirProperty = writableConfigProperty(SettingsModel::certificatesDir) - // TODO : We should encrypt all passwords in config file. - val keyStorePasswordProperty = writableConfigProperty(SettingsModel::keyStorePassword) - val trustStorePasswordProperty = writableConfigProperty(SettingsModel::trustStorePassword) init { load() diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt index 4ba675da68..341bbbee70 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt @@ -5,16 +5,16 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.SimpleIntegerProperty import javafx.scene.control.* +import javafx.stage.FileChooser import net.corda.client.fxutils.map import net.corda.client.model.NodeMonitorModel import net.corda.client.model.objectProperty -import net.corda.core.exists import net.corda.explorer.model.SettingsModel import net.corda.node.services.config.SSLConfiguration -import net.corda.node.services.config.configureTestSSL import org.controlsfx.dialog.ExceptionDialog import tornadofx.* import java.nio.file.Path +import java.nio.file.Paths import kotlin.system.exitProcess class LoginView : View() { @@ -26,7 +26,6 @@ class LoginView : View() { private val passwordTextField by fxid() private val rememberMeCheckBox by fxid() private val fullscreenCheckBox by fxid() - private val certificateButton by fxid