Non-ssl artemis acceptor for RPC connection. (#271)

* New non-ssl acceptor in artemis server for RPC connection.
This commit is contained in:
Patrick Kuo 2017-02-24 15:36:36 +00:00 committed by GitHub
parent ce2da918c8
commit f0d82e4918
25 changed files with 170 additions and 180 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ServiceInfo>,
rpcUsers: List<User>, customOverrides: Map<String, Any?>): ListenableFuture<NodeHandle> {
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(),

View File

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

View File

@ -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 <T> 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 <reified T : Any> 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)
}
}

View File

@ -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<String> = config.getListOrElse<String>("extraAdvertisedServiceIds") { emptyList() }
val useTestClock: Boolean by config.getOrElse { false }
val notaryNodeAddress: HostAndPort? by config.getOrElse { null }
val notaryNodeAddress: HostAndPort? by config
val notaryClusterAddresses: List<HostAndPort> = config
.getListOrElse<String>("notaryClusterAddresses") { emptyList() }
.map { HostAndPort.fromString(it) }

View File

@ -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<String, Any?>(
// 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<String, Any?>(

View File

@ -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<String, Any>?,
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<X509Certificate>, username: String): String {
private fun authenticateNode(certificates: Array<X509Certificate>): 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<X509Certificate>): 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<X509Certificate>?, 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) {

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ class IRSDemoTest : IntegrationTestCategory {
}
fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable<LocalDate?> {
val client = CordaRPCClient(config.artemisAddress, config)
val client = CordaRPCClient(config.rpcAddress!!)
client.start("user", "password")
val proxy = client.proxy()
val vaultUpdates = proxy.vaultAndUpdates().second

View File

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

View File

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

View File

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

View File

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

View File

@ -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<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 broker = ArtemisMessagingServer(config, address, rpcAddress, InMemoryNetworkMapCache(), userService)
val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>()
val net = databaseTransaction(database) {
NodeMessagingClient(

View File

@ -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<String>) {
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")

View File

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

View File

@ -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<PasswordField>()
private val rememberMeCheckBox by fxid<CheckBox>()
private val fullscreenCheckBox by fxid<CheckBox>()
private val certificateButton by fxid<Button>()
private val portProperty = SimpleIntegerProperty()
private val rememberMe by objectProperty(SettingsModel::rememberMeProperty)
@ -34,9 +33,6 @@ class LoginView : View() {
private val host by objectProperty(SettingsModel::hostProperty)
private val port by objectProperty(SettingsModel::portProperty)
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
private val certificatesDir by objectProperty(SettingsModel::certificatesDirProperty)
private val keyStorePasswordProperty by objectProperty(SettingsModel::keyStorePasswordProperty)
private val trustStorePasswordProperty by objectProperty(SettingsModel::trustStorePasswordProperty)
fun login() {
val status = Dialog<LoginStatus>().apply {
@ -46,7 +42,7 @@ class LoginView : View() {
ButtonBar.ButtonData.OK_DONE -> try {
root.isDisable = true
// TODO : Run this async to avoid UI lockup.
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureSSL(), usernameTextField.text, passwordTextField.text)
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), usernameTextField.text, passwordTextField.text)
if (!rememberMe.value) {
username.value = ""
host.value = ""
@ -79,18 +75,6 @@ class LoginView : View() {
if (status != LoginStatus.loggedIn) login()
}
private fun configureSSL(): SSLConfiguration {
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory: Path get() = certificatesDir.get()
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
}
// TODO : Don't use dev certificates.
return if (sslConfig.keyStoreFile.exists()) sslConfig else configureTestSSL().apply {
alert(Alert.AlertType.WARNING, "", "KeyStore not found in certificates directory.\nDEV certificates will be used by default.")
}
}
init {
// Restrict text field to Integer only.
portTextField.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
@ -99,44 +83,6 @@ class LoginView : View() {
usernameTextField.textProperty().bindBidirectional(username)
hostTextField.textProperty().bindBidirectional(host)
portTextField.textProperty().bindBidirectional(port)
certificateButton.setOnAction {
Dialog<ButtonType>().apply {
title = "Certificates Settings"
initOwner(root.scene.window)
dialogPane.content = gridpane {
vgap = 10.0
hgap = 5.0
row("Certificates Directory :") {
textfield {
prefWidth = 400.0
textProperty().bind(certificatesDir.map(Path::toString))
isEditable = false
}
button {
graphic = FontAwesomeIconView(FontAwesomeIcon.FOLDER_OPEN_ALT)
maxHeight = Double.MAX_VALUE
setOnAction {
chooseDirectory(owner = dialogPane.scene.window) {
initialDirectoryProperty().bind(certificatesDir.map(Path::toFile))
}?.let {
certificatesDir.set(it.toPath())
}
}
}
}
row("KeyStore Password :") { passwordfield(keyStorePasswordProperty) }
row("TrustStore Password :") { passwordfield(trustStorePasswordProperty) }
}
dialogPane.buttonTypes.addAll(ButtonType.APPLY, ButtonType.CANCEL)
}.showAndWait().get().let {
when (it) {
ButtonType.APPLY -> getModel<SettingsModel>().commit()
// Discard changes.
else -> getModel<SettingsModel>().load()
}
}
}
certificateButton.tooltip("Certificate Configuration")
}
private enum class LoginStatus {

View File

@ -26,23 +26,15 @@
<Label text="Corda Node :" GridPane.halignment="RIGHT"/>
<TextField fx:id="hostTextField" promptText="Host" GridPane.columnIndex="1"/>
<TextField fx:id="portTextField" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
<Button id="certificateButton" fx:id="certificateButton" GridPane.columnIndex="3" styleClass="certificateButton">
<padding>
<Insets right="6"/>
</padding>
<graphic>
<FontAwesomeIconView styleClass="certificateIcon" glyphName="LOCK" glyphSize="20"/>
</graphic>
</Button>
<Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/>
<TextField fx:id="usernameTextField" promptText="Username" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="1"/>
GridPane.columnSpan="2" GridPane.rowIndex="1"/>
<Label text="Password :" GridPane.rowIndex="2" GridPane.halignment="RIGHT"/>
<PasswordField fx:id="passwordTextField" promptText="Password" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="2"/>
GridPane.columnSpan="2" GridPane.rowIndex="2"/>
<HBox spacing="20" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="3">
<HBox spacing="20" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="2">
<CheckBox fx:id="rememberMeCheckBox" text="Remember me"/>
<CheckBox fx:id="fullscreenCheckBox" text="Fullscreen mode"/>
</HBox>