mirror of
https://github.com/corda/corda.git
synced 2025-06-17 22:58:19 +00:00
[CORDA:936]: Enable RPC layer to work with SSL
This commit is contained in:
committed by
GitHub
parent
70f1fdeb2b
commit
142f52fa82
@ -94,7 +94,7 @@ class AuthDBTests : NodeBasedTest() {
|
||||
)
|
||||
|
||||
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = securityConfig)
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
|
||||
client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -161,7 +161,6 @@ class AMQPBridgeTest {
|
||||
artemisServer.stop()
|
||||
artemisLegacyClient.stop()
|
||||
artemisLegacyServer.stop()
|
||||
|
||||
}
|
||||
|
||||
private fun createArtemis(sourceQueueName: String?): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||
@ -170,6 +169,7 @@ class AMQPBridgeTest {
|
||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(artemisAddress).whenever(it).p2pAddress
|
||||
doReturn("").whenever(it).exportJMXto
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(true).whenever(it).useAMQPBridges
|
||||
@ -180,7 +180,7 @@ class AMQPBridgeTest {
|
||||
doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
|
||||
}
|
||||
val userService = rigorousMock<RPCSecurityManager>()
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
|
||||
artemisServer.start()
|
||||
artemisClient.start()
|
||||
@ -198,6 +198,7 @@ class AMQPBridgeTest {
|
||||
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(artemisAddress).whenever(it).p2pAddress
|
||||
doReturn("").whenever(it).exportJMXto
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(false).whenever(it).useAMQPBridges
|
||||
@ -209,7 +210,7 @@ class AMQPBridgeTest {
|
||||
doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
|
||||
}
|
||||
val userService = rigorousMock<RPCSecurityManager>()
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress2, MAX_MESSAGE_SIZE)
|
||||
artemisServer.start()
|
||||
artemisClient.start()
|
||||
|
@ -226,6 +226,7 @@ class ProtonWrapperTests {
|
||||
doReturn(CHARLIE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress
|
||||
doReturn("").whenever(it).exportJMXto
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(true).whenever(it).useAMQPBridges
|
||||
@ -236,7 +237,7 @@ class ProtonWrapperTests {
|
||||
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||
}
|
||||
val userService = rigorousMock<RPCSecurityManager>()
|
||||
val server = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val server = ArtemisMessagingServer(artemisConfig, artemisPort, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||
val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), MAX_MESSAGE_SIZE)
|
||||
server.start()
|
||||
client.start()
|
||||
|
@ -35,7 +35,7 @@ class NetworkMapTest {
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val cacheTimeout = 1.seconds
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val portAllocation = PortAllocation.RandomFree
|
||||
|
||||
private lateinit var networkMapServer: NetworkMapServer
|
||||
private lateinit var compatibilityZone: CompatibilityZoneParams
|
||||
|
@ -0,0 +1,64 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.node.testsupport.withCertificates
|
||||
import net.corda.node.testsupport.withKeyStores
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.useSslRpcOverrides
|
||||
import net.corda.testing.node.User
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class RpcSslTest {
|
||||
@Test
|
||||
fun rpc_client_using_ssl() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
driver(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
node.rpcClientToNode(clientSslOptions).start(user.username, user.password).use { connection ->
|
||||
connection.proxy.apply {
|
||||
nodeInfo()
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_client_not_using_ssl() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
var successful = false
|
||||
driver(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree) {
|
||||
startNode(rpcUsers = listOf(user)).getOrThrow().use { node ->
|
||||
node.rpcClientToNode().start(user.username, user.password).use { connection ->
|
||||
connection.proxy.apply {
|
||||
nodeInfo()
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import org.junit.Test
|
||||
@ -69,7 +70,7 @@ class LargeTransactionsTest {
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 1)
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 2)
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 3)
|
||||
driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts")) {
|
||||
driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), portAllocation = PortAllocation.RandomFree) {
|
||||
val rpcUser = User("admin", "admin", setOf("ALL"))
|
||||
val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow()
|
||||
alice.rpcClientToNode().use(rpcUser.username, rpcUser.password) {
|
||||
|
@ -27,7 +27,7 @@ import java.nio.file.Files
|
||||
/**
|
||||
* Runs the security tests with the attacker pretending to be a node on the network.
|
||||
*/
|
||||
class MQSecurityAsNodeTest : MQSecurityTest() {
|
||||
class MQSecurityAsNodeTest : P2PMQSecurityTest() {
|
||||
override fun createAttacker(): SimpleMQClient {
|
||||
return clientTo(alice.internals.configuration.p2pAddress)
|
||||
}
|
||||
@ -67,7 +67,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
||||
|
||||
@Test
|
||||
fun `login to a non ssl port as a node user`() {
|
||||
val attacker = clientTo(alice.internals.configuration.rpcAddress!!, sslConfiguration = null)
|
||||
val attacker = clientTo(alice.internals.configuration.rpcOptions.address!!, sslConfiguration = null)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start(NODE_USER, NODE_USER, enableSSL = false)
|
||||
}
|
||||
@ -75,7 +75,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
|
||||
|
||||
@Test
|
||||
fun `login to a non ssl port as a peer user`() {
|
||||
val attacker = clientTo(alice.internals.configuration.rpcAddress!!, sslConfiguration = null)
|
||||
val attacker = clientTo(alice.internals.configuration.rpcOptions.address!!, sslConfiguration = null)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start(PEER_USER, PEER_USER, enableSSL = false) // Login as a peer
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ import org.junit.Test
|
||||
/**
|
||||
* Runs the security tests with the attacker being a valid RPC user of Alice.
|
||||
*/
|
||||
class MQSecurityAsRPCTest : MQSecurityTest() {
|
||||
class MQSecurityAsRPCTest : RPCMQSecurityTest() {
|
||||
override fun createAttacker(): SimpleMQClient {
|
||||
return clientTo(alice.internals.configuration.rpcAddress!!)
|
||||
return clientTo(alice.internals.configuration.rpcOptions.address!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -21,8 +21,6 @@ import net.corda.node.internal.StartedNode
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
@ -69,46 +67,6 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
clients.forEach { it.stop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from P2P queue`() {
|
||||
assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from peer queue`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to address of peer which has been communicated with`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for peer which has not been communicated with`() {
|
||||
val bob = startNode(BOB_NAME)
|
||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for unknown peer`() {
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
|
||||
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from RPC requests queue`() {
|
||||
assertConsumeAttackFails(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from logged in user's RPC queue`() {
|
||||
val user1Queue = loginToRPCAndGetClientQueue()
|
||||
assertConsumeAttackFails(user1Queue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for valid RPC user`() {
|
||||
val user1Queue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.${rpcUser.username}.${random63BitValue()}"
|
||||
@ -155,9 +113,9 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
|
||||
fun loginToRPCAndGetClientQueue(): String {
|
||||
loginToRPC(alice.internals.configuration.rpcAddress!!, rpcUser)
|
||||
loginToRPC(alice.internals.configuration.rpcOptions.address!!, rpcUser)
|
||||
val clientQueueQuery = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.${rpcUser.username}.*")
|
||||
val client = clientTo(alice.internals.configuration.rpcAddress!!)
|
||||
val client = clientTo(alice.internals.configuration.rpcOptions.address!!)
|
||||
client.start(rpcUser.username, rpcUser.password, false)
|
||||
return client.session.addressQuery(clientQueueQuery).queueNames.single().toString()
|
||||
}
|
||||
@ -178,6 +136,12 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
}
|
||||
|
||||
fun assertAttackFailsNonexistent(queue: String, attack: () -> Unit) {
|
||||
assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java)
|
||||
.isThrownBy(attack)
|
||||
.withMessageContaining(queue)
|
||||
}
|
||||
|
||||
fun assertNonTempQueueCreationAttackFails(queue: String, durable: Boolean) {
|
||||
val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE"
|
||||
assertAttackFails(queue, permission) {
|
||||
@ -208,6 +172,15 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
}
|
||||
|
||||
fun assertConsumeAttackFailsNonexistent(queue: String) {
|
||||
assertAttackFailsNonexistent(queue) {
|
||||
attacker.session.createConsumer(queue)
|
||||
}
|
||||
assertAttackFailsNonexistent(queue) {
|
||||
attacker.session.createConsumer(queue, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertAttackFails(queue: String, permission: String, attack: () -> Unit) {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java)
|
||||
.isThrownBy(attack)
|
||||
@ -215,7 +188,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
.withMessageContaining(permission)
|
||||
}
|
||||
|
||||
private fun startBobAndCommunicateWithAlice(): Party {
|
||||
protected fun startBobAndCommunicateWithAlice(): Party {
|
||||
val bob = startNode(BOB_NAME)
|
||||
bob.registerInitiatedFlow(ReceiveFlow::class.java)
|
||||
val bobParty = bob.info.chooseIdentity()
|
||||
|
@ -0,0 +1,56 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.chooseIdentity
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Runs a series of MQ-related attacks against a node. Subclasses need to call [startAttacker] to connect
|
||||
* the attacker to [alice].
|
||||
*/
|
||||
abstract class P2PMQSecurityTest : MQSecurityTest() {
|
||||
@Test
|
||||
fun `consume message from P2P queue`() {
|
||||
assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from peer queue`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to address of peer which has been communicated with`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for peer which has not been communicated with`() {
|
||||
val bob = startNode(BOB_NAME)
|
||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for unknown peer`() {
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
|
||||
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from RPC requests queue`() {
|
||||
assertConsumeAttackFailsNonexistent(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from logged in user's RPC queue`() {
|
||||
val user1Queue = loginToRPCAndGetClientQueue()
|
||||
assertConsumeAttackFailsNonexistent(user1Queue)
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.chooseIdentity
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Runs a series of MQ-related attacks against a node. Subclasses need to call [startAttacker] to connect
|
||||
* the attacker to [alice].
|
||||
*/
|
||||
abstract class RPCMQSecurityTest : MQSecurityTest() {
|
||||
@Test
|
||||
fun `consume message from P2P queue`() {
|
||||
assertConsumeAttackFailsNonexistent("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from peer queue`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertConsumeAttackFailsNonexistent("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to address of peer which has been communicated with`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertConsumeAttackFailsNonexistent("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for peer which has not been communicated with`() {
|
||||
val bob = startNode(BOB_NAME)
|
||||
assertConsumeAttackFailsNonexistent("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for unknown peer`() {
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}"
|
||||
assertConsumeAttackFailsNonexistent(invalidPeerQueue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from RPC requests queue`() {
|
||||
assertConsumeAttackFails(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from logged in user's RPC queue`() {
|
||||
val user1Queue = loginToRPCAndGetClientQueue()
|
||||
assertConsumeAttackFails(user1Queue)
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.core.chooseIdentity
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import org.junit.Assume.assumeFalse
|
||||
import org.junit.Test
|
||||
@ -40,7 +41,7 @@ class NodeStatePersistenceTests {
|
||||
|
||||
val user = User("mark", "dadada", setOf(startFlow<SendMessageFlow>(), invokeRpc("vaultQuery")))
|
||||
val message = Message("Hello world!")
|
||||
val stateAndRef: StateAndRef<MessageState>? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) {
|
||||
val stateAndRef: StateAndRef<MessageState>? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree) {
|
||||
val nodeName = {
|
||||
val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeName = nodeHandle.nodeInfo.chooseIdentity().name
|
||||
@ -74,7 +75,7 @@ class NodeStatePersistenceTests {
|
||||
|
||||
val user = User("mark", "dadada", setOf(startFlow<SendMessageFlow>(), invokeRpc("vaultQuery")))
|
||||
val message = Message("Hello world!")
|
||||
val stateAndRef: StateAndRef<MessageState>? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) {
|
||||
val stateAndRef: StateAndRef<MessageState>? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree) {
|
||||
val nodeName = {
|
||||
val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeName = nodeHandle.nodeInfo.chooseIdentity().name
|
||||
|
@ -0,0 +1,13 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
interface LifecycleSupport : Startable, Stoppable
|
||||
|
||||
interface Stoppable {
|
||||
fun stop()
|
||||
}
|
||||
|
||||
interface Startable {
|
||||
fun start()
|
||||
|
||||
val started: Boolean
|
||||
}
|
@ -4,6 +4,7 @@ import com.codahale.metrics.JmxReporter
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -15,6 +16,8 @@ import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
@ -23,6 +26,7 @@ import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.SecurityConfiguration
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.rpc.ArtemisRpcBroker
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
@ -36,6 +40,7 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Scheduler
|
||||
import rx.schedulers.Schedulers
|
||||
import java.nio.file.Path
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -132,6 +137,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
override lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
|
||||
|
||||
private var messageBroker: ArtemisMessagingServer? = null
|
||||
private var rpcBroker: ArtemisBroker? = null
|
||||
|
||||
private var shutdownHook: ShutdownHook? = null
|
||||
|
||||
@ -139,15 +145,16 @@ open class Node(configuration: NodeConfiguration,
|
||||
// Construct security manager reading users data either from the 'security' config section
|
||||
// if present or from rpcUsers list if the former is missing from config.
|
||||
val securityManagerConfig = configuration.security?.authService ?:
|
||||
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
|
||||
securityManager = RPCSecurityManagerImpl(securityManagerConfig)
|
||||
|
||||
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
|
||||
val advertisedAddress = info.addresses.single()
|
||||
val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) else startLocalRpcBroker()
|
||||
val advertisedAddress = info.addresses.first()
|
||||
|
||||
printBasicNodeInfo("Incoming connection address", advertisedAddress.toString())
|
||||
rpcMessagingClient = RPCMessagingClient(configuration, serverAddress, networkParameters.maxMessageSize)
|
||||
rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, rpcServerAddresses.admin, networkParameters.maxMessageSize)
|
||||
verifierMessagingClient = when (configuration.verifierType) {
|
||||
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
|
||||
VerifierType.InMemory -> null
|
||||
@ -166,15 +173,38 @@ open class Node(configuration: NodeConfiguration,
|
||||
networkParameters.maxMessageSize)
|
||||
}
|
||||
|
||||
private fun startLocalRpcBroker(): BrokerAddresses {
|
||||
with(configuration) {
|
||||
require(rpcOptions.address != null) { "RPC address needs to be specified for local RPC broker." }
|
||||
val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc"
|
||||
with(rpcOptions) {
|
||||
rpcBroker = if (useSsl) {
|
||||
ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory)
|
||||
} else {
|
||||
ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory)
|
||||
}
|
||||
}
|
||||
return rpcBroker!!.addresses
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeLocalMessageBroker(): NetworkHostAndPort {
|
||||
with(configuration) {
|
||||
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize)
|
||||
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize)
|
||||
return NetworkHostAndPort("localhost", p2pAddress.port)
|
||||
}
|
||||
}
|
||||
|
||||
override fun myAddresses(): List<NetworkHostAndPort> {
|
||||
return listOf(configuration.messagingServerAddress ?: getAdvertisedAddress())
|
||||
val addresses = mutableListOf<NetworkHostAndPort>()
|
||||
addresses.add(configuration.messagingServerAddress ?: getAdvertisedAddress())
|
||||
rpcBroker?.addresses?.let {
|
||||
addresses.add(it.primary)
|
||||
if (it.admin != it.primary) {
|
||||
addresses.add(it.admin)
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
private fun getAdvertisedAddress(): NetworkHostAndPort {
|
||||
@ -220,12 +250,16 @@ open class Node(configuration: NodeConfiguration,
|
||||
override fun startMessagingService(rpcOps: RPCOps) {
|
||||
// Start up the embedded MQ server
|
||||
messageBroker?.apply {
|
||||
runOnStop += this::stop
|
||||
runOnStop += this::close
|
||||
start()
|
||||
}
|
||||
rpcBroker?.apply {
|
||||
runOnStop += this::close
|
||||
start()
|
||||
}
|
||||
// Start up the MQ clients.
|
||||
rpcMessagingClient.run {
|
||||
runOnStop += this::stop
|
||||
runOnStop += this::close
|
||||
start(rpcOps, securityManager)
|
||||
}
|
||||
verifierMessagingClient?.run {
|
||||
@ -331,7 +365,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
private var verifierMessagingClient: VerifierMessagingClient? = null
|
||||
/** Starts a blocking event loop for message dispatch. */
|
||||
fun run() {
|
||||
rpcMessagingClient.start2(messageBroker!!.serverControl)
|
||||
rpcMessagingClient.start2(rpcBroker!!.serverControl)
|
||||
verifierMessagingClient?.start2()
|
||||
(network as P2PMessagingClient).run()
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package net.corda.node.internal.artemis
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.LifecycleSupport
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
|
||||
interface ArtemisBroker : LifecycleSupport, AutoCloseable {
|
||||
val addresses: BrokerAddresses
|
||||
|
||||
val serverControl: ActiveMQServerControl
|
||||
|
||||
override fun close() = stop()
|
||||
}
|
||||
|
||||
data class BrokerAddresses(val primary: NetworkHostAndPort, private val adminArg: NetworkHostAndPort?) {
|
||||
val admin = adminArg ?: primary
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package net.corda.node.internal.artemis
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import java.security.KeyStore
|
||||
import javax.security.cert.CertificateException
|
||||
import javax.security.cert.X509Certificate
|
||||
|
||||
sealed class CertificateChainCheckPolicy {
|
||||
@FunctionalInterface
|
||||
interface Check {
|
||||
fun checkCertificateChain(theirChain: Array<X509Certificate>)
|
||||
}
|
||||
|
||||
abstract fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check
|
||||
|
||||
object Any : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<X509Certificate>) {
|
||||
// nothing to do here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RootMustMatch : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val rootPublicKey = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<X509Certificate>) {
|
||||
val theirRoot = theirChain.last().publicKey
|
||||
if (rootPublicKey != theirRoot) {
|
||||
throw CertificateException("Root certificate mismatch, their root = $theirRoot")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LeafMustMatch : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val ourPublicKey = keyStore.getCertificate(X509Utilities.CORDA_CLIENT_TLS).publicKey
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<X509Certificate>) {
|
||||
val theirLeaf = theirChain.first().publicKey
|
||||
if (ourPublicKey != theirLeaf) {
|
||||
throw CertificateException("Leaf certificate mismatch, their leaf = $theirLeaf")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MustContainOneOf(private val trustedAliases: Set<String>) : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val trustedPublicKeys = trustedAliases.map { trustStore.getCertificate(it).publicKey }.toSet()
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<X509Certificate>) {
|
||||
if (!theirChain.any { it.publicKey in trustedPublicKeys }) {
|
||||
throw CertificateException("Their certificate chain contained none of the trusted ones")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UsernameMustMatchCommonName : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
return UsernameMustMatchCommonNameCheck()
|
||||
}
|
||||
}
|
||||
|
||||
class UsernameMustMatchCommonNameCheck : Check {
|
||||
lateinit var username: String
|
||||
override fun checkCertificateChain(theirChain: Array<X509Certificate>) {
|
||||
if (!theirChain.any { certificate -> CordaX500Name.parse(certificate.subjectDN.name).commonName == username }) {
|
||||
throw CertificateException("Client certificate does not match login username.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package net.corda.node.internal.artemis
|
||||
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import java.math.BigInteger
|
||||
|
||||
internal open class SecureArtemisConfiguration : ConfigurationImpl() {
|
||||
init {
|
||||
// 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
|
||||
// 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.
|
||||
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
||||
}
|
||||
}
|
@ -3,10 +3,14 @@ package net.corda.node.services.config
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.messaging.CertificateChainCheckPolicy
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
@ -34,7 +38,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val activeMQServer: ActiveMqServerConfiguration
|
||||
val additionalNodeInfoPollingFrequencyMsec: Long
|
||||
val p2pAddress: NetworkHostAndPort
|
||||
val rpcAddress: NetworkHostAndPort?
|
||||
val rpcOptions: NodeRpcOptions
|
||||
val messagingServerAddress: NetworkHostAndPort?
|
||||
// TODO Move into DevModeOptions
|
||||
val useTestClock: Boolean get() = false
|
||||
@ -46,7 +50,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val attachmentContentCacheSizeBytes: Long get() = defaultAttachmentContentCacheSize
|
||||
val attachmentCacheBound: Long get() = defaultAttachmentCacheBound
|
||||
|
||||
|
||||
companion object {
|
||||
// default to at least 8MB and a bit extra for larger heap sizes
|
||||
val defaultTransactionCacheSize: Long = 8.MB + getAdditionalCacheMemory()
|
||||
@ -100,7 +103,7 @@ data class BridgeConfiguration(val retryIntervalMs: Long,
|
||||
|
||||
data class ActiveMqServerConfiguration(val bridge: BridgeConfiguration)
|
||||
|
||||
fun Config.parseAsNodeConfiguration(): NodeConfiguration = this.parseAs<NodeConfigurationImpl>()
|
||||
fun Config.parseAsNodeConfiguration(): NodeConfiguration = parseAs<NodeConfigurationImpl>()
|
||||
|
||||
data class NodeConfigurationImpl(
|
||||
/** This is not retrieved from the config file but rather from a command line argument. */
|
||||
@ -118,7 +121,8 @@ data class NodeConfigurationImpl(
|
||||
// Then rename this to messageRedeliveryDelay and make it of type Duration
|
||||
override val messageRedeliveryDelaySeconds: Int = 30,
|
||||
override val p2pAddress: NetworkHostAndPort,
|
||||
override val rpcAddress: NetworkHostAndPort?,
|
||||
private val rpcAddress: NetworkHostAndPort? = null,
|
||||
private val rpcSettings: NodeRpcSettings,
|
||||
// TODO This field is slightly redundant as p2pAddress 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
|
||||
override val messagingServerAddress: NetworkHostAndPort?,
|
||||
@ -137,7 +141,23 @@ data class NodeConfigurationImpl(
|
||||
private val transactionCacheSizeMegaBytes: Int? = null,
|
||||
private val attachmentContentCacheSizeMegaBytes: Int? = null,
|
||||
override val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound
|
||||
) : NodeConfiguration {
|
||||
) : NodeConfiguration {
|
||||
companion object {
|
||||
private val logger = loggerFor<NodeConfigurationImpl>()
|
||||
}
|
||||
|
||||
override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword))
|
||||
|
||||
private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: SSLConfiguration): NodeRpcOptions {
|
||||
return when {
|
||||
explicitAddress != null -> {
|
||||
require(settings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." }
|
||||
logger.warn("Top-level declaration of property 'rpcAddress' is deprecated. Please use 'rpcSettings.address' instead.")
|
||||
settings.copy(address = explicitAddress)
|
||||
}
|
||||
else -> settings
|
||||
}.asOptions(fallbackSslOptions)
|
||||
}
|
||||
|
||||
override val exportJMXto: String get() = "http"
|
||||
override val transactionCacheSizeBytes: Long
|
||||
@ -156,6 +176,28 @@ data class NodeConfigurationImpl(
|
||||
}
|
||||
}
|
||||
|
||||
data class NodeRpcSettings(
|
||||
val address: NetworkHostAndPort?,
|
||||
val adminAddress: NetworkHostAndPort?,
|
||||
val standAloneBroker: Boolean = false,
|
||||
val useSsl: Boolean = false,
|
||||
val ssl: SslOptions?
|
||||
) {
|
||||
fun asOptions(fallbackSslOptions: SSLConfiguration): NodeRpcOptions {
|
||||
return object : NodeRpcOptions {
|
||||
override val address = this@NodeRpcSettings.address
|
||||
override val adminAddress = this@NodeRpcSettings.adminAddress
|
||||
override val standAloneBroker = this@NodeRpcSettings.standAloneBroker
|
||||
override val useSsl = this@NodeRpcSettings.useSsl
|
||||
override val sslConfig = this@NodeRpcSettings.ssl ?: fallbackSslOptions
|
||||
|
||||
override fun toString(): String {
|
||||
return "address: $address, adminAddress: $adminAddress, standAloneBroker: $standAloneBroker, useSsl: $useSsl, sslConfig: $sslConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class VerifierType {
|
||||
InMemory,
|
||||
OutOfProcess
|
||||
@ -165,7 +207,8 @@ enum class CertChainPolicyType {
|
||||
Any,
|
||||
RootMustMatch,
|
||||
LeafMustMatch,
|
||||
MustContainOneOf
|
||||
MustContainOneOf,
|
||||
UsernameMustMatch
|
||||
}
|
||||
|
||||
data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set<String>) {
|
||||
@ -176,6 +219,7 @@ data class CertChainPolicyConfig(val role: String, private val policy: CertChain
|
||||
CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch
|
||||
CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch
|
||||
CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases)
|
||||
CertChainPolicyType.UsernameMustMatch -> CertificateChainCheckPolicy.UsernameMustMatchCommonName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package net.corda.node.services.config
|
||||
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
data class SslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration {
|
||||
constructor(certificatesDirectory: String, keyStorePassword: String, trustStorePassword: String) : this(certificatesDirectory.toAbsolutePath(), keyStorePassword, trustStorePassword)
|
||||
|
||||
fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(), keyStorePassword: String = this.keyStorePassword, trustStorePassword: String = this.trustStorePassword): SslOptions = copy(certificatesDirectory = certificatesDirectory.toAbsolutePath(), keyStorePassword = keyStorePassword, trustStorePassword = trustStorePassword)
|
||||
}
|
||||
|
||||
private fun String.toAbsolutePath() = Paths.get(this).toAbsolutePath()
|
@ -0,0 +1,12 @@
|
||||
package net.corda.node.services.config.rpc
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
|
||||
interface NodeRpcOptions {
|
||||
val address: NetworkHostAndPort?
|
||||
val adminAddress: NetworkHostAndPort?
|
||||
val standAloneBroker: Boolean
|
||||
val useSsl: Boolean
|
||||
val sslConfig: SSLConfiguration
|
||||
}
|
@ -37,7 +37,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s
|
||||
isUseGlobalPools = nodeSerializationEnv != null
|
||||
}
|
||||
val sessionFactory = locator.createSessionFactory()
|
||||
// Login using the node username. The broker will authentiate us as its node (as opposed to another peer)
|
||||
// Login using the node username. The broker will authenticate us as its node (as opposed to another peer)
|
||||
// using our TLS certificate.
|
||||
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
|
||||
// size of 1MB is acknowledged.
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.core.crypto.AddressFormatException
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.div
|
||||
@ -14,17 +13,18 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.node.internal.artemis.SecureArtemisConfiguration
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.VerifierApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
@ -34,32 +34,23 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.apache.activemq.artemis.core.config.Configuration
|
||||
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin
|
||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
||||
import org.apache.activemq.artemis.core.settings.HierarchicalRepository
|
||||
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
|
||||
import org.apache.activemq.artemis.core.settings.impl.AddressSettings
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.Principal
|
||||
import java.util.*
|
||||
@ -67,14 +58,12 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.security.auth.Subject
|
||||
import javax.security.auth.callback.CallbackHandler
|
||||
import javax.security.auth.callback.NameCallback
|
||||
import javax.security.auth.callback.PasswordCallback
|
||||
import javax.security.auth.callback.UnsupportedCallbackException
|
||||
import javax.security.auth.login.AppConfigurationEntry
|
||||
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import javax.security.auth.login.LoginException
|
||||
import javax.security.auth.spi.LoginModule
|
||||
import javax.security.cert.CertificateException
|
||||
|
||||
// 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)
|
||||
@ -92,10 +81,9 @@ import javax.security.cert.CertificateException
|
||||
@ThreadSafe
|
||||
class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private val p2pPort: Int,
|
||||
val rpcPort: Int?,
|
||||
val networkMapCache: NetworkMapCacheInternal,
|
||||
val securityManager: RPCSecurityManager,
|
||||
val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||
val maxMessageSize: Int) : ArtemisBroker, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
@ -105,7 +93,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
private lateinit var activeMQServer: ActiveMQServer
|
||||
val serverControl: ActiveMQServerControl get() = activeMQServer.activeMQServerControl
|
||||
override val serverControl: ActiveMQServerControl get() = activeMQServer.activeMQServerControl
|
||||
private var networkChangeHandle: Subscription? = null
|
||||
private lateinit var bridgeManager: BridgeManager
|
||||
|
||||
@ -117,8 +105,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
* 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.
|
||||
*/
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
fun start() = mutex.locked {
|
||||
override fun start() = mutex.locked {
|
||||
if (!running) {
|
||||
configureAndStartServer()
|
||||
networkChangeHandle = networkMapCache.changed.subscribe { updateBridgesOnNetworkChange(it) }
|
||||
@ -126,7 +113,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() = mutex.locked {
|
||||
override fun stop() = mutex.locked {
|
||||
bridgeManager.close()
|
||||
networkChangeHandle?.unsubscribe()
|
||||
networkChangeHandle = null
|
||||
@ -134,6 +121,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
running = false
|
||||
}
|
||||
|
||||
override val addresses = config.p2pAddress.let { BrokerAddresses(it, it) }
|
||||
|
||||
override val started: Boolean
|
||||
get() = activeMQServer.isStarted
|
||||
|
||||
// TODO: Maybe wrap [IOException] on a key store load error so that it's clearly splitting key store loading from
|
||||
// Artemis IO errors
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
@ -157,12 +149,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
activeMQServer.start()
|
||||
bridgeManager.start()
|
||||
Node.printBasicNodeInfo("Listening on port", p2pPort.toString())
|
||||
if (rpcPort != null) {
|
||||
Node.printBasicNodeInfo("RPC service listening on port", rpcPort.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createArtemisConfig() = ConfigurationImpl().apply {
|
||||
private fun createArtemisConfig() = SecureArtemisConfiguration().apply {
|
||||
val artemisDir = config.baseDirectory / "artemis"
|
||||
bindingsDirectory = (artemisDir / "bindings").toString()
|
||||
journalDirectory = (artemisDir / "journal").toString()
|
||||
@ -171,9 +160,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
acceptorFactoryClassName = NettyAcceptorFactory::class.java.name
|
||||
)
|
||||
val acceptors = mutableSetOf(createTcpTransport(connectionDirection, "0.0.0.0", p2pPort))
|
||||
if (rpcPort != null) {
|
||||
acceptors.add(createTcpTransport(connectionDirection, "0.0.0.0", rpcPort, 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.
|
||||
@ -184,35 +170,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
|
||||
journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB.
|
||||
managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS)
|
||||
// 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
|
||||
// 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.
|
||||
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
||||
queueConfigurations = listOf(
|
||||
// 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(RPCApi.RPC_SERVER_QUEUE_NAME, durable = false),
|
||||
queueConfig(
|
||||
name = RPCApi.RPC_CLIENT_BINDING_REMOVALS,
|
||||
address = NOTIFICATIONS_ADDRESS,
|
||||
filter = RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION,
|
||||
durable = false
|
||||
),
|
||||
queueConfig(
|
||||
name = RPCApi.RPC_CLIENT_BINDING_ADDITIONS,
|
||||
address = NOTIFICATIONS_ADDRESS,
|
||||
filter = RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION,
|
||||
durable = false
|
||||
)
|
||||
)
|
||||
addressesSettings = mapOf(
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply {
|
||||
maxSizeBytes = 10L * maxMessageSize
|
||||
addressFullMessagePolicy = AddressFullMessagePolicy.FAIL
|
||||
}
|
||||
)
|
||||
|
||||
// JMX enablement
|
||||
if (config.exportJMXto.isNotEmpty()) {
|
||||
isJMXManagementEnabled = true
|
||||
@ -221,16 +179,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
|
||||
}.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 the following groups:
|
||||
* 1. The node itself. It is given full access to all valid queues.
|
||||
@ -242,24 +190,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
|
||||
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
|
||||
securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
|
||||
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
|
||||
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
|
||||
// and stealing RPC responses.
|
||||
val rolesAdderOnLogin = RolesAdderOnLogin { username ->
|
||||
Pair(
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#",
|
||||
setOf(
|
||||
nodeInternalRole,
|
||||
restrictedRole(
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username",
|
||||
consume = true,
|
||||
createNonDurableQueue = true,
|
||||
deleteNonDurableQueue = true)))
|
||||
}
|
||||
securitySettingPlugins.add(rolesAdderOnLogin)
|
||||
securityRoles[VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(VERIFIER_ROLE, consume = true))
|
||||
securityRoles["${VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX}.#"] = setOf(nodeInternalRole, restrictedRole(VERIFIER_ROLE, send = true))
|
||||
val onLoginListener = { username: String -> rolesAdderOnLogin.onLogin(username) }
|
||||
val onLoginListener = { _: String -> }
|
||||
return Pair(this, onLoginListener)
|
||||
}
|
||||
|
||||
@ -369,66 +302,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
|
||||
}
|
||||
|
||||
sealed class CertificateChainCheckPolicy {
|
||||
|
||||
@FunctionalInterface
|
||||
interface Check {
|
||||
fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>)
|
||||
}
|
||||
|
||||
abstract fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check
|
||||
|
||||
object Any : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RootMustMatch : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val rootPublicKey = trustStore.getCertificate(CORDA_ROOT_CA).publicKey
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) {
|
||||
val theirRoot = theirChain.last().publicKey
|
||||
if (rootPublicKey != theirRoot) {
|
||||
throw CertificateException("Root certificate mismatch, their root = $theirRoot")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LeafMustMatch : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val ourPublicKey = keyStore.getCertificate(CORDA_CLIENT_TLS).publicKey
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) {
|
||||
val theirLeaf = theirChain.first().publicKey
|
||||
if (ourPublicKey != theirLeaf) {
|
||||
throw CertificateException("Leaf certificate mismatch, their leaf = $theirLeaf")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MustContainOneOf(val trustedAliases: Set<String>) : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
val trustedPublicKeys = trustedAliases.map { trustStore.getCertificate(it).publicKey }.toSet()
|
||||
return object : Check {
|
||||
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) {
|
||||
if (!theirChain.any { it.publicKey in trustedPublicKeys }) {
|
||||
throw CertificateException("Their certificate chain contained none of the trusted ones")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -445,7 +318,6 @@ class NodeLoginModule : LoginModule {
|
||||
// Include forbidden username character to prevent name clash with any RPC usernames
|
||||
const val PEER_ROLE = "SystemRoles/Peer"
|
||||
const val NODE_ROLE = "SystemRoles/Node"
|
||||
const val RPC_ROLE = "SystemRoles/RPC"
|
||||
const val VERIFIER_ROLE = "SystemRoles/Verifier"
|
||||
|
||||
const val CERT_CHAIN_CHECKS_OPTION_NAME = "CertChainChecks"
|
||||
@ -475,10 +347,9 @@ class NodeLoginModule : LoginModule {
|
||||
|
||||
override fun login(): Boolean {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
val certificateCallback = CertificateCallback()
|
||||
try {
|
||||
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
|
||||
callbackHandler.handle(arrayOf(nameCallback, certificateCallback))
|
||||
} catch (e: IOException) {
|
||||
throw LoginException(e.message)
|
||||
} catch (e: UnsupportedCallbackException) {
|
||||
@ -486,7 +357,6 @@ 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.debug { "Processing login for $username" }
|
||||
@ -496,7 +366,6 @@ class NodeLoginModule : LoginModule {
|
||||
PEER_ROLE -> authenticatePeer(certificates)
|
||||
NODE_ROLE -> authenticateNode(certificates)
|
||||
VERIFIER_ROLE -> authenticateVerifier(certificates)
|
||||
RPC_ROLE -> authenticateRpcUser(username, Password(password))
|
||||
else -> throw FailedLoginException("Peer does not belong on our network")
|
||||
}
|
||||
principals += UserPrincipal(validatedUser)
|
||||
@ -527,14 +396,6 @@ class NodeLoginModule : LoginModule {
|
||||
return certificates.first().subjectDN.name
|
||||
}
|
||||
|
||||
private fun authenticateRpcUser(username: String, password: Password): String {
|
||||
securityManager.authenticate(username, password)
|
||||
loginListener(username)
|
||||
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
|
||||
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses
|
||||
return username
|
||||
}
|
||||
|
||||
private fun determineUserRole(certificates: Array<javax.security.cert.X509Certificate>?, username: String): String? {
|
||||
fun requireTls() = require(certificates != null) { "No TLS?" }
|
||||
return when (username) {
|
||||
@ -550,14 +411,7 @@ class NodeLoginModule : LoginModule {
|
||||
requireTls()
|
||||
VERIFIER_ROLE
|
||||
}
|
||||
else -> {
|
||||
// Assume they're an RPC user if its from a non-ssl connection
|
||||
if (certificates == null) {
|
||||
RPC_ROLE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,39 +439,4 @@ class NodeLoginModule : LoginModule {
|
||||
}
|
||||
}
|
||||
|
||||
typealias LoginListener = (String) -> Unit
|
||||
typealias RolesRepository = HierarchicalRepository<MutableSet<Role>>
|
||||
|
||||
/**
|
||||
* Helper class to dynamically assign security roles to RPC users
|
||||
* on their authentication. This object is plugged into the server
|
||||
* as [SecuritySettingPlugin]. It responds to authentication events
|
||||
* from [NodeLoginModule] by adding the address -> roles association
|
||||
* generated by the given [source], unless already done before.
|
||||
*/
|
||||
private class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>)
|
||||
: SecuritySettingPlugin {
|
||||
|
||||
// Artemis internal container storing roles association
|
||||
private lateinit var repository: RolesRepository
|
||||
|
||||
fun onLogin(username: String) {
|
||||
val (address, roles) = source(username)
|
||||
val entry = repository.getMatch(address)
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
repository.addMatch(address, roles.toMutableSet())
|
||||
}
|
||||
}
|
||||
|
||||
// Initializer called by the Artemis framework
|
||||
override fun setSecurityRepository(repository: RolesRepository) {
|
||||
this.repository = repository
|
||||
}
|
||||
|
||||
// Part of SecuritySettingPlugin interface which is no-op in this case
|
||||
override fun stop() = this
|
||||
|
||||
override fun init(options: MutableMap<String, String>?) = this
|
||||
|
||||
override fun getSecurityRoles() = null
|
||||
}
|
||||
typealias LoginListener = (String) -> Unit
|
@ -5,14 +5,14 @@ import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.getX509Certificate
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
|
||||
class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||
class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: NetworkHostAndPort, maxMessageSize: Int) : SingletonSerializeAsToken(), AutoCloseable {
|
||||
private val artemis = ArtemisMessagingClient(config, serverAddress, maxMessageSize)
|
||||
private var rpcServer: RPCServer? = null
|
||||
|
||||
@ -30,4 +30,6 @@ class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: Ne
|
||||
rpcServer?.close()
|
||||
artemis.stop()
|
||||
}
|
||||
|
||||
override fun close() = stop()
|
||||
}
|
||||
|
@ -0,0 +1,108 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.config.CertChainPolicyConfig
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import rx.Observable
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStoreException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.security.auth.login.AppConfigurationEntry
|
||||
|
||||
internal class ArtemisRpcBroker internal constructor(
|
||||
address: NetworkHostAndPort,
|
||||
private val adminAddressOptional: NetworkHostAndPort?,
|
||||
private val sslOptions: SSLConfiguration,
|
||||
private val useSsl: Boolean,
|
||||
private val securityManager: RPCSecurityManager,
|
||||
private val certificateChainCheckPolicies: List<CertChainPolicyConfig>,
|
||||
private val maxMessageSize: Int,
|
||||
private val jmxEnabled: Boolean = false,
|
||||
private val baseDirectory: Path) : ArtemisBroker {
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<ArtemisRpcBroker>()
|
||||
|
||||
fun withSsl(address: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker {
|
||||
return ArtemisRpcBroker(address, null, sslOptions, true, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
}
|
||||
|
||||
fun withoutSsl(address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: SSLConfiguration, securityManager: RPCSecurityManager, certificateChainCheckPolicies: List<CertChainPolicyConfig>, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path): ArtemisBroker {
|
||||
return ArtemisRpcBroker(address, adminAddress, sslOptions, false, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
logger.debug("Artemis RPC broker is starting.")
|
||||
server.start()
|
||||
logger.debug("Artemis RPC broker is started.")
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
logger.debug("Artemis RPC broker is stopping.")
|
||||
server.stop(true)
|
||||
logger.debug("Artemis RPC broker is stopped.")
|
||||
}
|
||||
|
||||
override val started get() = server.isStarted
|
||||
|
||||
override val serverControl: ActiveMQServerControl get() = server.activeMQServerControl
|
||||
|
||||
override val addresses = BrokerAddresses(address, adminAddressOptional ?: address)
|
||||
|
||||
private val server = initialiseServer()
|
||||
|
||||
private fun initialiseServer(): ActiveMQServer {
|
||||
val serverConfiguration = RpcBrokerConfiguration(baseDirectory, maxMessageSize, jmxEnabled, addresses.primary, adminAddressOptional, sslOptions, useSsl)
|
||||
val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener, sslOptions)
|
||||
|
||||
return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply {
|
||||
registerActivationFailureListener { exception -> throw exception }
|
||||
registerPostQueueDeletionCallback { address, qName -> logger.debug("Queue deleted: $qName for $address") }
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, KeyStoreException::class)
|
||||
private fun createArtemisSecurityManager(loginListener: LoginListener, sslOptions: SSLConfiguration): ActiveMQJAASSecurityManager {
|
||||
val keyStore = loadKeyStore(sslOptions.sslKeystore, sslOptions.keyStorePassword)
|
||||
val trustStore = loadKeyStore(sslOptions.trustStoreFile, sslOptions.trustStorePassword)
|
||||
|
||||
val defaultCertPolicies = mapOf(
|
||||
NodeLoginModule.NODE_ROLE to CertificateChainCheckPolicy.LeafMustMatch,
|
||||
NodeLoginModule.RPC_ROLE to CertificateChainCheckPolicy.Any
|
||||
)
|
||||
val certChecks = defaultCertPolicies.mapValues { (role, defaultPolicy) ->
|
||||
val policy = certificateChainCheckPolicies.noneOrSingle { it.role == role }?.certificateChainCheckPolicy ?: defaultPolicy
|
||||
policy.createCheck(keyStore, trustStore)
|
||||
}
|
||||
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
NodeLoginModule.LOGIN_LISTENER_ARG to loginListener,
|
||||
NodeLoginModule.SECURITY_MANAGER_ARG to securityManager,
|
||||
NodeLoginModule.USE_SSL_ARG to useSsl,
|
||||
NodeLoginModule.CERT_CHAIN_CHECKS_ARG to certChecks)
|
||||
return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options))
|
||||
}
|
||||
}
|
||||
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
|
||||
}
|
||||
}
|
||||
|
||||
typealias LoginListener = (String) -> Unit
|
||||
|
||||
private fun <RESULT> CompletableFuture<RESULT>.toObservable() = Observable.from(this)
|
@ -0,0 +1,169 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
|
||||
import java.io.IOException
|
||||
import java.security.Principal
|
||||
import java.util.*
|
||||
import javax.security.auth.Subject
|
||||
import javax.security.auth.callback.CallbackHandler
|
||||
import javax.security.auth.callback.NameCallback
|
||||
import javax.security.auth.callback.PasswordCallback
|
||||
import javax.security.auth.callback.UnsupportedCallbackException
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import javax.security.auth.login.LoginException
|
||||
import javax.security.auth.spi.LoginModule
|
||||
import javax.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
|
||||
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
|
||||
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
|
||||
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
|
||||
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
|
||||
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
|
||||
* the CN within that is their legal name.
|
||||
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
|
||||
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
|
||||
*/
|
||||
internal class NodeLoginModule : LoginModule {
|
||||
companion object {
|
||||
internal const val NODE_ROLE = "SystemRoles/Node"
|
||||
internal const val RPC_ROLE = "SystemRoles/RPC"
|
||||
|
||||
internal const val CERT_CHAIN_CHECKS_ARG = "CertChainChecks"
|
||||
internal const val USE_SSL_ARG = "useSsl"
|
||||
internal val SECURITY_MANAGER_ARG = "RpcSecurityManager"
|
||||
internal val LOGIN_LISTENER_ARG = "LoginListener"
|
||||
private val log = loggerFor<NodeLoginModule>()
|
||||
}
|
||||
|
||||
private var loginSucceeded: Boolean = false
|
||||
private lateinit var subject: Subject
|
||||
private lateinit var callbackHandler: CallbackHandler
|
||||
private lateinit var securityManager: RPCSecurityManager
|
||||
private lateinit var loginListener: LoginListener
|
||||
private var useSsl: Boolean? = null
|
||||
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
|
||||
private lateinit var rpcCertCheck: CertificateChainCheckPolicy.Check
|
||||
private val principals = ArrayList<Principal>()
|
||||
|
||||
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
|
||||
this.subject = subject
|
||||
this.callbackHandler = callbackHandler
|
||||
securityManager = uncheckedCast(options[SECURITY_MANAGER_ARG])
|
||||
loginListener = uncheckedCast(options[LOGIN_LISTENER_ARG])
|
||||
useSsl = options[USE_SSL_ARG] as Boolean
|
||||
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_ARG])
|
||||
nodeCertCheck = certChainChecks[NODE_ROLE]!!
|
||||
rpcCertCheck = certChainChecks[RPC_ROLE]!!
|
||||
}
|
||||
|
||||
override fun login(): Boolean {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
val certificateCallback = CertificateCallback()
|
||||
try {
|
||||
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
|
||||
} catch (e: IOException) {
|
||||
throw LoginException(e.message)
|
||||
} catch (e: UnsupportedCallbackException) {
|
||||
throw LoginException("${e.message} not available to obtain information from user")
|
||||
}
|
||||
|
||||
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
|
||||
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
|
||||
val certificates = certificateCallback.certificates ?: emptyArray()
|
||||
|
||||
if (rpcCertCheck is CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck) {
|
||||
(rpcCertCheck as CertificateChainCheckPolicy.UsernameMustMatchCommonNameCheck).username = username
|
||||
}
|
||||
|
||||
log.debug("Logging user in")
|
||||
|
||||
try {
|
||||
val role = determineUserRole(certificates, username, useSsl!!)
|
||||
val validatedUser = when (role) {
|
||||
NodeLoginModule.NODE_ROLE -> {
|
||||
authenticateNode(certificates)
|
||||
NODE_USER
|
||||
}
|
||||
RPC_ROLE -> {
|
||||
authenticateRpcUser(username, Password(password), certificates, useSsl!!)
|
||||
username
|
||||
}
|
||||
else -> throw FailedLoginException("Peer does not belong on our network")
|
||||
}
|
||||
principals += UserPrincipal(validatedUser)
|
||||
|
||||
loginSucceeded = true
|
||||
return loginSucceeded
|
||||
} catch (exception: FailedLoginException) {
|
||||
log.warn("$exception")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticateNode(certificates: Array<X509Certificate>) {
|
||||
nodeCertCheck.checkCertificateChain(certificates)
|
||||
principals += RolePrincipal(NodeLoginModule.NODE_ROLE)
|
||||
}
|
||||
|
||||
private fun authenticateRpcUser(username: String, password: Password, certificates: Array<X509Certificate>, useSsl: Boolean) {
|
||||
if (useSsl) {
|
||||
rpcCertCheck.checkCertificateChain(certificates)
|
||||
// no point in matching username with CN because companies wouldn't want to provide a certificate for each user
|
||||
}
|
||||
securityManager.authenticate(username, password)
|
||||
loginListener(username)
|
||||
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
|
||||
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses
|
||||
}
|
||||
|
||||
private fun determineUserRole(certificates: Array<X509Certificate>, username: String, useSsl: Boolean): String? {
|
||||
return when (username) {
|
||||
ArtemisMessagingComponent.NODE_USER -> {
|
||||
require(certificates.isNotEmpty()) { "No TLS?" }
|
||||
NodeLoginModule.NODE_ROLE
|
||||
}
|
||||
else -> {
|
||||
if (useSsl) {
|
||||
require(certificates.isNotEmpty()) { "No TLS?" }
|
||||
}
|
||||
return RPC_ROLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
val result = loginSucceeded
|
||||
if (result) {
|
||||
subject.principals.addAll(principals)
|
||||
}
|
||||
clear()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun abort(): Boolean {
|
||||
clear()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun logout(): Boolean {
|
||||
subject.principals.removeAll(principals)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
loginSucceeded = false
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin
|
||||
import org.apache.activemq.artemis.core.settings.HierarchicalRepository
|
||||
|
||||
/**
|
||||
* Helper class to dynamically assign security roles to RPC users
|
||||
* on their authentication. This object is plugged into the server
|
||||
* as [SecuritySettingPlugin]. It responds to authentication events
|
||||
* from [NodeLoginModule] by adding the address -> roles association
|
||||
* generated by the given [source], unless already done before.
|
||||
*/
|
||||
internal class RolesAdderOnLogin(val source: (String) -> Pair<String, Set<Role>>) : SecuritySettingPlugin {
|
||||
private lateinit var repository: RolesRepository
|
||||
|
||||
fun onLogin(username: String) {
|
||||
val (address, roles) = source(username)
|
||||
val entry = repository.getMatch(address)
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
repository.addMatch(address, roles.toMutableSet())
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSecurityRepository(repository: RolesRepository) {
|
||||
this.repository = repository
|
||||
}
|
||||
|
||||
override fun stop() = this
|
||||
|
||||
override fun init(options: MutableMap<String, String>?) = this
|
||||
|
||||
override fun getSecurityRoles() = null
|
||||
}
|
||||
|
||||
typealias RolesRepository = HierarchicalRepository<MutableSet<Role>>
|
@ -0,0 +1,131 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.artemis.SecureArtemisConfiguration
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
|
||||
import org.apache.activemq.artemis.core.settings.impl.AddressSettings
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: SSLConfiguration?, useSsl: Boolean) : SecureArtemisConfiguration() {
|
||||
val loginListener: (String) -> Unit
|
||||
|
||||
init {
|
||||
setDirectories(baseDirectory)
|
||||
|
||||
val acceptorConfigurationsSet = mutableSetOf(acceptorConfiguration(address, useSsl, sslOptions))
|
||||
adminAddress?.let {
|
||||
acceptorConfigurationsSet += acceptorConfiguration(adminAddress, true, sslOptions)
|
||||
}
|
||||
acceptorConfigurations = acceptorConfigurationsSet
|
||||
|
||||
queueConfigurations = queueConfigurations()
|
||||
|
||||
managementNotificationAddress = SimpleString(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS)
|
||||
addressesSettings = mapOf(
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply {
|
||||
maxSizeBytes = 10L * maxMessageSize
|
||||
addressFullMessagePolicy = AddressFullMessagePolicy.FAIL
|
||||
}
|
||||
)
|
||||
|
||||
initialiseSettings(maxMessageSize)
|
||||
|
||||
val nodeInternalRole = Role(NodeLoginModule.NODE_ROLE, true, true, true, true, true, true, true, true)
|
||||
|
||||
val rolesAdderOnLogin = RolesAdderOnLogin { username ->
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole(
|
||||
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username",
|
||||
consume = true,
|
||||
createNonDurableQueue = true,
|
||||
deleteNonDurableQueue = true)
|
||||
)
|
||||
}
|
||||
|
||||
configureAddressSecurity(nodeInternalRole, rolesAdderOnLogin)
|
||||
|
||||
if (jmxEnabled) {
|
||||
enableJmx()
|
||||
}
|
||||
|
||||
loginListener = { username: String -> rolesAdderOnLogin.onLogin(username) }
|
||||
}
|
||||
|
||||
private fun configureAddressSecurity(nodeInternalRole: Role, rolesAdderOnLogin: RolesAdderOnLogin) {
|
||||
securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole)
|
||||
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(NodeLoginModule.RPC_ROLE, send = true))
|
||||
securitySettingPlugins.add(rolesAdderOnLogin)
|
||||
}
|
||||
|
||||
private fun enableJmx() {
|
||||
isJMXManagementEnabled = true
|
||||
isJMXUseBrokerName = true
|
||||
}
|
||||
|
||||
private fun initialiseSettings(maxMessageSize: Int) {
|
||||
// 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
|
||||
isPersistIDCache = true
|
||||
isPopulateValidatedUser = true
|
||||
journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store.
|
||||
journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
|
||||
journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB.
|
||||
}
|
||||
|
||||
private fun queueConfigurations(): List<CoreQueueConfiguration> {
|
||||
return listOf(
|
||||
queueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME, durable = false),
|
||||
queueConfiguration(
|
||||
name = RPCApi.RPC_CLIENT_BINDING_REMOVALS,
|
||||
address = ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS,
|
||||
filter = RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION,
|
||||
durable = false
|
||||
),
|
||||
queueConfiguration(
|
||||
name = RPCApi.RPC_CLIENT_BINDING_ADDITIONS,
|
||||
address = ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS,
|
||||
filter = RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION,
|
||||
durable = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setDirectories(baseDirectory: Path) {
|
||||
bindingsDirectory = (baseDirectory / "bindings").toString()
|
||||
journalDirectory = (baseDirectory / "journal").toString()
|
||||
largeMessagesDirectory = (baseDirectory / "large-messages").toString()
|
||||
}
|
||||
|
||||
private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
|
||||
val configuration = CoreQueueConfiguration()
|
||||
|
||||
configuration.name = name
|
||||
configuration.address = address
|
||||
configuration.filterString = filter
|
||||
configuration.isDurable = durable
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
|
||||
private fun acceptorConfiguration(address: NetworkHostAndPort, enableSsl: Boolean, sslOptions: SSLConfiguration?): TransportConfiguration {
|
||||
return tcpTransport(ConnectionDirection.Inbound(NettyAcceptorFactory::class.java.name), address, sslOptions, enableSsl)
|
||||
}
|
||||
|
||||
private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false,
|
||||
deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false,
|
||||
deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role {
|
||||
return Role(name, send, consume, createDurableQueue, deleteDurableQueue, createNonDurableQueue, deleteNonDurableQueue, manage, browse)
|
||||
}
|
||||
}
|
@ -24,4 +24,8 @@ activeMQServer = {
|
||||
maxRetryIntervalMin = 3
|
||||
}
|
||||
}
|
||||
useAMQPBridges = true
|
||||
useAMQPBridges = true
|
||||
rpcSettings = {
|
||||
useSsl = false
|
||||
standAloneBroker = false
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package net.corda.node.services.config
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -27,24 +29,42 @@ class NodeConfigurationImplTest {
|
||||
assertFalse { configDebugOptions(true, DevModeOptions(true)).shouldCheckCheckpoints() }
|
||||
}
|
||||
|
||||
private fun configDebugOptions(devMode: Boolean, devModeOptions: DevModeOptions?) : NodeConfiguration {
|
||||
private fun configDebugOptions(devMode: Boolean, devModeOptions: DevModeOptions?): NodeConfiguration {
|
||||
return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions)
|
||||
}
|
||||
|
||||
private val testConfiguration = NodeConfigurationImpl(
|
||||
baseDirectory = Paths.get("."),
|
||||
myLegalName = ALICE_NAME,
|
||||
emailAddress = "",
|
||||
keyStorePassword = "cordacadevpass",
|
||||
trustStorePassword = "trustpass",
|
||||
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
|
||||
rpcUsers = emptyList(),
|
||||
verifierType = VerifierType.InMemory,
|
||||
p2pAddress = NetworkHostAndPort("localhost", 0),
|
||||
rpcAddress = NetworkHostAndPort("localhost", 1),
|
||||
messagingServerAddress = null,
|
||||
notary = null,
|
||||
certificateChainCheckPolicies = emptyList(),
|
||||
devMode = true,
|
||||
activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0)))
|
||||
private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl {
|
||||
return testConfiguration.copy(dataSourceProperties = dataSourceProperties)
|
||||
}
|
||||
|
||||
private val testConfiguration = testNodeConfiguration()
|
||||
|
||||
private fun testNodeConfiguration(): NodeConfigurationImpl {
|
||||
val baseDirectory = Paths.get(".")
|
||||
val keyStorePassword = "cordacadevpass"
|
||||
val trustStorePassword = "trustpass"
|
||||
val rpcSettings = NodeRpcSettings(
|
||||
address = NetworkHostAndPort("localhost", 1),
|
||||
adminAddress = NetworkHostAndPort("localhost", 2),
|
||||
standAloneBroker = false,
|
||||
useSsl = false,
|
||||
ssl = SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword))
|
||||
return NodeConfigurationImpl(
|
||||
baseDirectory = baseDirectory,
|
||||
myLegalName = ALICE_NAME,
|
||||
emailAddress = "",
|
||||
keyStorePassword = keyStorePassword,
|
||||
trustStorePassword = trustStorePassword,
|
||||
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
|
||||
rpcUsers = emptyList(),
|
||||
verifierType = VerifierType.InMemory,
|
||||
p2pAddress = NetworkHostAndPort("localhost", 0),
|
||||
messagingServerAddress = null,
|
||||
notary = null,
|
||||
certificateChainCheckPolicies = emptyList(),
|
||||
devMode = true,
|
||||
activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0)),
|
||||
rpcSettings = rpcSettings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ArtemisMessagingTests {
|
||||
class ArtemisMessagingTest {
|
||||
companion object {
|
||||
const val TOPIC = "platform.self"
|
||||
}
|
||||
@ -51,7 +51,6 @@ class ArtemisMessagingTests {
|
||||
val temporaryFolder = TemporaryFolder()
|
||||
|
||||
private val serverPort = freePort()
|
||||
private val rpcPort = freePort()
|
||||
private val identity = generateKeyPair()
|
||||
|
||||
private lateinit var config: NodeConfiguration
|
||||
@ -71,6 +70,7 @@ class ArtemisMessagingTests {
|
||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress
|
||||
doReturn("").whenever(it).exportJMXto
|
||||
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||
doReturn(5).whenever(it).messageRedeliveryDelaySeconds
|
||||
@ -183,8 +183,8 @@ class ArtemisMessagingTests {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort, maxMessageSize: Int = MAX_MESSAGE_SIZE): ArtemisMessagingServer {
|
||||
return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager, maxMessageSize).apply {
|
||||
private fun createMessagingServer(local: Int = serverPort, maxMessageSize: Int = MAX_MESSAGE_SIZE): ArtemisMessagingServer {
|
||||
return ArtemisMessagingServer(config, local, networkMapCache, securityManager, maxMessageSize).apply {
|
||||
config.configureWithDevSSLCertificate()
|
||||
messagingServer = this
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package net.corda.node.services.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.node.services.config.CertChainPolicyConfig
|
||||
import net.corda.node.services.messaging.RPCMessagingClient
|
||||
import net.corda.node.testsupport.withCertificates
|
||||
import net.corda.node.testsupport.withKeyStores
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ArtemisRpcTests {
|
||||
private val ports: PortAllocation = PortAllocation.RandomFree
|
||||
|
||||
private val user = User("mark", "dadada", setOf(all()))
|
||||
private val users = listOf(user)
|
||||
private val securityManager = RPCSecurityManagerImpl.fromUserList(AuthServiceId("test"), users)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
@Test
|
||||
fun rpc_with_ssl_enabled() {
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_ssl_disabled() {
|
||||
withCertificates { server, client, createSelfSigned, _ ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, _ ->
|
||||
// here server is told not to use SSL, and client sslOptions are null (as in, do not use SSL)
|
||||
testSslCommunication(brokerSslOptions, false, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_server_certificate_untrusted_to_client() {
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
// here the server certificate is not trusted by the client
|
||||
// client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_no_client_certificate() {
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
|
||||
// here client keystore is empty
|
||||
// client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_with_no_ssl_on_client_side_and_ssl_on_server_side() {
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["mark"] = markCertificate
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, _ ->
|
||||
// here client sslOptions are passed null (as in, do not use SSL)
|
||||
testSslCommunication(brokerSslOptions, true, null, clientConnectionSpy = expectExceptionOfType(ActiveMQConnectionTimedOutException::class))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rpc_client_certificate_untrusted_to_server() {
|
||||
withCertificates { server, client, createSelfSigned, _ ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
// here client's certificate is self-signed, otherwise Artemis allows the connection (the issuing certificate is in the truststore)
|
||||
val markCertificate = createSelfSigned(CordaX500Name("mark", "IT", "R3 London", "London", "London", "GB"))
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
// here the client certificate is not trusted by the server
|
||||
// server.trustStore["mark"] = markCertificate
|
||||
|
||||
client.keyStore["mark"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { brokerSslOptions, clientSslOptions ->
|
||||
testSslCommunication(brokerSslOptions, true, clientSslOptions, clientConnectionSpy = expectExceptionOfType(ActiveMQNotConnectedException::class))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun testSslCommunication(brokerSslOptions: SSLConfiguration, useSslForBroker: Boolean, clientSslOptions: SSLConfiguration?, address: NetworkHostAndPort = ports.nextHostAndPort(),
|
||||
adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = Files.createTempDirectory(null), clientConnectionSpy: (() -> Unit) -> Unit = {}) {
|
||||
val maxMessageSize = 10000
|
||||
val jmxEnabled = false
|
||||
val certificateChainCheckPolicies: List<CertChainPolicyConfig> = listOf()
|
||||
|
||||
val artemisBroker: ArtemisBroker = if (useSslForBroker) {
|
||||
ArtemisRpcBroker.withSsl(address, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
} else {
|
||||
ArtemisRpcBroker.withoutSsl(address, adminAddress, brokerSslOptions, securityManager, certificateChainCheckPolicies, maxMessageSize, jmxEnabled, baseDirectory)
|
||||
}
|
||||
artemisBroker.use { broker ->
|
||||
broker.start()
|
||||
RPCMessagingClient(brokerSslOptions, broker.addresses.admin, maxMessageSize).use { server ->
|
||||
server.start(TestRpcOpsImpl(), securityManager, broker.serverControl)
|
||||
|
||||
val client = RPCClient<TestRpcOps>(tcpTransport(ConnectionDirection.Outbound(), broker.addresses.primary, clientSslOptions))
|
||||
|
||||
clientConnectionSpy {
|
||||
client.start(TestRpcOps::class.java, user.username, user.password).use { connection ->
|
||||
connection.proxy.apply {
|
||||
val greeting = greet("Frodo")
|
||||
assertThat(greeting).isEqualTo("Oh, hello Frodo!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <OPS : RPCOps> RPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) {
|
||||
apply {
|
||||
start(ops, securityManager)
|
||||
start2(brokerControl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <EXCEPTION : Exception> expectExceptionOfType(exceptionType: KClass<EXCEPTION>): (() -> Unit) -> Unit {
|
||||
return { action -> assertThatThrownBy { action.invoke() }.isInstanceOf(exceptionType.java) }
|
||||
}
|
||||
|
||||
interface TestRpcOps : RPCOps {
|
||||
fun greet(name: String): String
|
||||
}
|
||||
|
||||
class TestRpcOpsImpl : TestRpcOps {
|
||||
override fun greet(name: String): String = "Oh, hello $name!"
|
||||
|
||||
override val protocolVersion: Int = 1
|
||||
}
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
package net.corda.node.testsupport
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.services.config.SslOptions
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.apache.commons.io.FileUtils
|
||||
import sun.security.tools.keytool.CertAndKeyGen
|
||||
import sun.security.x509.X500Name
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.Instant.now
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
class UnsafeCertificatesFactory(
|
||||
defaults: Defaults = defaults(),
|
||||
private val keyType: String = defaults.keyType,
|
||||
private val signatureAlgorithm: String = defaults.signatureAlgorithm,
|
||||
private val keySize: Int = defaults.keySize,
|
||||
private val certificatesValidityWindow: CertificateValidityWindow = defaults.certificatesValidityWindow,
|
||||
private val keyStoreType: String = defaults.keyStoreType) {
|
||||
|
||||
companion object {
|
||||
private const val KEY_TYPE_RSA = "RSA"
|
||||
private const val SIG_ALG_SHA_RSA = "SHA1WithRSA"
|
||||
private const val KEY_SIZE = 1024
|
||||
private val DEFAULT_DURATION = Duration.of(365, ChronoUnit.DAYS)
|
||||
private const val DEFAULT_KEYSTORE_TYPE = "JKS"
|
||||
|
||||
fun defaults() = Defaults(KEY_TYPE_RSA, SIG_ALG_SHA_RSA, KEY_SIZE, CertificateValidityWindow(now(), DEFAULT_DURATION), DEFAULT_KEYSTORE_TYPE)
|
||||
}
|
||||
|
||||
data class Defaults(
|
||||
val keyType: String,
|
||||
val signatureAlgorithm: String,
|
||||
val keySize: Int,
|
||||
val certificatesValidityWindow: CertificateValidityWindow,
|
||||
val keyStoreType: String)
|
||||
|
||||
fun createSelfSigned(name: X500Name): UnsafeCertificate = createSelfSigned(name, keyType, signatureAlgorithm, keySize, certificatesValidityWindow)
|
||||
|
||||
fun createSelfSigned(name: CordaX500Name) = createSelfSigned(name.asX500Name())
|
||||
|
||||
fun createSignedBy(subject: X500Principal, issuer: UnsafeCertificate): UnsafeCertificate = issuer.createSigned(subject, keyType, signatureAlgorithm, keySize, certificatesValidityWindow)
|
||||
|
||||
fun createSignedBy(name: CordaX500Name, issuer: UnsafeCertificate): UnsafeCertificate = issuer.createSigned(name, keyType, signatureAlgorithm, keySize, certificatesValidityWindow)
|
||||
|
||||
fun newKeyStore(password: String) = UnsafeKeyStore.create(keyStoreType, password)
|
||||
|
||||
fun newKeyStores(keyStorePassword: String, trustStorePassword: String): KeyStores = KeyStores(newKeyStore(keyStorePassword), newKeyStore(trustStorePassword))
|
||||
}
|
||||
|
||||
class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) {
|
||||
fun save(directory: Path = Files.createTempDirectory(null)): AutoClosableSSLConfiguration {
|
||||
val keyStoreFile = keyStore.toTemporaryFile("sslkeystore", directory = directory)
|
||||
val trustStoreFile = trustStore.toTemporaryFile("truststore", directory = directory)
|
||||
|
||||
val sslConfiguration = sslConfiguration(directory)
|
||||
|
||||
return object : AutoClosableSSLConfiguration {
|
||||
override val value = sslConfiguration
|
||||
|
||||
override fun close() {
|
||||
keyStoreFile.close()
|
||||
trustStoreFile.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sslConfiguration(directory: Path) = SslOptions(directory, keyStore.password, trustStore.password)
|
||||
}
|
||||
|
||||
interface AutoClosableSSLConfiguration : AutoCloseable {
|
||||
val value: SslOptions
|
||||
}
|
||||
|
||||
typealias KeyStoreEntry = Pair<String, UnsafeCertificate>
|
||||
|
||||
data class UnsafeKeyStore(private val delegate: KeyStore, val password: String) : Iterable<KeyStoreEntry> {
|
||||
companion object {
|
||||
private const val JKS_TYPE = "JKS"
|
||||
|
||||
fun create(type: String, password: String) = UnsafeKeyStore(newKeyStore(type, password), password)
|
||||
|
||||
fun createJKS(password: String) = create(JKS_TYPE, password)
|
||||
}
|
||||
|
||||
operator fun plus(entry: KeyStoreEntry) = set(entry.first, entry.second)
|
||||
|
||||
override fun iterator(): Iterator<Pair<String, UnsafeCertificate>> = delegate.aliases().toList().map { alias -> alias to get(alias) }.iterator()
|
||||
|
||||
operator fun get(alias: String): UnsafeCertificate {
|
||||
return when {
|
||||
delegate.isKeyEntry(alias) -> delegate.getCertificateAndKeyPair(alias, password).unsafe()
|
||||
else -> UnsafeCertificate(delegate.getX509Certificate(alias), null)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun set(alias: String, certificate: UnsafeCertificate) {
|
||||
delegate.setCertificateEntry(alias, certificate.value)
|
||||
delegate.setKeyEntry(alias, certificate.privateKey, password.toCharArray(), arrayOf(certificate.value))
|
||||
}
|
||||
|
||||
fun save(path: Path) = delegate.save(path, password)
|
||||
|
||||
fun toTemporaryFile(fileName: String, fileExtension: String? = delegate.type.toLowerCase(), directory: Path): TemporaryFile {
|
||||
return TemporaryFile("$fileName.$fileExtension", directory).also { save(it.path) }
|
||||
}
|
||||
}
|
||||
|
||||
class TemporaryFile(fileName: String, val directory: Path) : AutoCloseable {
|
||||
private val file = (directory / fileName).toFile()
|
||||
init {
|
||||
file.createNewFile()
|
||||
file.deleteOnExit()
|
||||
}
|
||||
|
||||
val path: Path = file.toPath().toAbsolutePath()
|
||||
|
||||
override fun close() = FileUtils.forceDelete(file)
|
||||
}
|
||||
|
||||
data class UnsafeCertificate(val value: X509Certificate, val privateKey: PrivateKey?) {
|
||||
val keyPair = KeyPair(value.publicKey, privateKey)
|
||||
|
||||
val principal: X500Principal get() = value.subjectX500Principal
|
||||
|
||||
val issuer: X500Principal get() = value.issuerX500Principal
|
||||
|
||||
fun createSigned(subject: X500Principal, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow): UnsafeCertificate {
|
||||
val keyGen = keyGen(keyType, signatureAlgorithm, keySize)
|
||||
|
||||
return UnsafeCertificate(X509Utilities.createCertificate(
|
||||
certificateType = CertificateType.TLS,
|
||||
issuer = value.subjectX500Principal,
|
||||
issuerKeyPair = keyPair,
|
||||
validityWindow = certificatesValidityWindow.datePair,
|
||||
subject = subject,
|
||||
subjectPublicKey = keyGen.publicKey
|
||||
), keyGen.privateKey)
|
||||
}
|
||||
|
||||
fun createSigned(name: CordaX500Name, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow) = createSigned(name.x500Principal, keyType, signatureAlgorithm, keySize, certificatesValidityWindow)
|
||||
}
|
||||
|
||||
data class CertificateValidityWindow(val from: Instant, val to: Instant) {
|
||||
constructor(from: Instant, duration: Duration) : this(from, from.plus(duration))
|
||||
|
||||
val duration = Duration.between(from, to)!!
|
||||
|
||||
val datePair = Date.from(from) to Date.from(to)
|
||||
}
|
||||
|
||||
private fun createSelfSigned(name: X500Name, keyType: String, signatureAlgorithm: String, keySize: Int, certificatesValidityWindow: CertificateValidityWindow): UnsafeCertificate {
|
||||
val keyGen = keyGen(keyType, signatureAlgorithm, keySize)
|
||||
return UnsafeCertificate(keyGen.getSelfCertificate(name, certificatesValidityWindow.duration.toMillis()), keyGen.privateKey)
|
||||
}
|
||||
|
||||
private fun CordaX500Name.asX500Name(): X500Name = X500Name.asX500Name(x500Principal)
|
||||
|
||||
private fun CertificateAndKeyPair.unsafe() = UnsafeCertificate(certificate, keyPair.private)
|
||||
|
||||
private fun keyGen(keyType: String, signatureAlgorithm: String, keySize: Int): CertAndKeyGen {
|
||||
val keyGen = CertAndKeyGen(keyType, signatureAlgorithm)
|
||||
keyGen.generate(keySize)
|
||||
return keyGen
|
||||
}
|
||||
|
||||
private fun newKeyStore(type: String, password: String): KeyStore {
|
||||
val keyStore = KeyStore.getInstance(type)
|
||||
// Loading creates the store, can't do anything with it until it's loaded
|
||||
keyStore.load(null, password.toCharArray())
|
||||
|
||||
return keyStore
|
||||
}
|
||||
|
||||
fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: SslOptions, clientSslOptions: SslOptions) -> Unit) {
|
||||
val serverDir = Files.createTempDirectory(null)
|
||||
FileUtils.forceDeleteOnExit(serverDir.toFile())
|
||||
|
||||
val clientDir = Files.createTempDirectory(null)
|
||||
FileUtils.forceDeleteOnExit(clientDir.toFile())
|
||||
|
||||
server.save(serverDir).use { serverSslConfiguration ->
|
||||
client.save(clientDir).use { clientSslConfiguration ->
|
||||
action(serverSslConfiguration.value, clientSslConfiguration.value)
|
||||
}
|
||||
}
|
||||
FileUtils.deleteQuietly(clientDir.toFile())
|
||||
FileUtils.deleteQuietly(serverDir.toFile())
|
||||
}
|
||||
|
||||
fun withCertificates(factoryDefaults: UnsafeCertificatesFactory.Defaults = UnsafeCertificatesFactory.defaults(), action: (server: KeyStores, client: KeyStores, createSelfSigned: (name: CordaX500Name) -> UnsafeCertificate, createSignedBy: (name: CordaX500Name, issuer: UnsafeCertificate) -> UnsafeCertificate) -> Unit) {
|
||||
val factory = UnsafeCertificatesFactory(factoryDefaults)
|
||||
val server = factory.newKeyStores("serverKeyStorePass", "serverTrustKeyStorePass")
|
||||
val client = factory.newKeyStores("clientKeyStorePass", "clientTrustKeyStorePass")
|
||||
action(server, client, factory::createSelfSigned, factory::createSignedBy)
|
||||
}
|
Reference in New Issue
Block a user