Enforce X.500 distinguished names in configuration

This commit is contained in:
Ross Nicoll
2017-04-27 18:39:46 +01:00
parent 8c3b9ac589
commit b64e7f51f6
35 changed files with 163 additions and 133 deletions

View File

@ -8,6 +8,7 @@ import net.corda.core.node.services.ServiceInfo
import net.corda.core.readLines
import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_REGULATOR
import net.corda.node.LOGS_DIRECTORY_NAME
import net.corda.node.services.api.RegulatorService
import net.corda.node.services.transactions.SimpleNotaryService
@ -42,7 +43,7 @@ class DriverTests {
fun `simple node startup and shutdown`() {
val handles = driver {
val notary = startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type)))
val regulator = startNode("CN=Regulator,O=R3,OU=corda,L=London,C=UK", setOf(ServiceInfo(RegulatorService.type)))
val regulator = startNode(DUMMY_REGULATOR.name, setOf(ServiceInfo(RegulatorService.type)))
listOf(nodeMustBeUp(notary), nodeMustBeUp(regulator))
}
handles.map { nodeMustBeDown(it) }

View File

@ -3,6 +3,7 @@ package net.corda.services.messaging
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.*
import net.corda.core.crypto.X509Utilities
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.DEFAULT_SESSION_ID
@ -19,6 +20,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.NodeBasedTest
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import java.util.*
import java.util.concurrent.CountDownLatch
@ -27,6 +29,11 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class P2PMessagingTest : NodeBasedTest() {
private companion object {
val DISTRIBUTED_SERVICE_NAME = X509Utilities.getDevX509Name("DistributedService")
val SERVICE_2_NAME = X509Utilities.getDevX509Name("Service Node 2")
}
@Test
fun `network map will work after restart`() {
val identities = listOf(DUMMY_BANK_A, DUMMY_BANK_B, DUMMY_NOTARY)
@ -55,15 +62,14 @@ class P2PMessagingTest : NodeBasedTest() {
// TODO Use a dummy distributed service
@Test
fun `communicating with a distributed service which the network map node is part of`() {
val serviceName = "DistributedService"
val root = tempFolder.root.toPath()
ServiceIdentityGenerator.generateToDisk(
listOf(root / DUMMY_MAP.name, root / "Service Node 2"),
listOf(root / DUMMY_MAP.name.toString(), root / SERVICE_2_NAME.toString()),
RaftValidatingNotaryService.type.id,
serviceName)
DISTRIBUTED_SERVICE_NAME)
val distributedService = ServiceInfo(RaftValidatingNotaryService.type, serviceName)
val distributedService = ServiceInfo(RaftValidatingNotaryService.type, DISTRIBUTED_SERVICE_NAME)
val notaryClusterAddress = freeLocalHostAndPort()
startNetworkMapNode(
DUMMY_MAP.name,
@ -71,7 +77,7 @@ class P2PMessagingTest : NodeBasedTest() {
configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString()))
val (serviceNode2, alice) = Futures.allAsList(
startNode(
"Service Node 2",
SERVICE_2_NAME,
advertisedServices = setOf(distributedService),
configOverrides = mapOf(
"notaryNodeAddress" to freeLocalHostAndPort().toString(),
@ -79,14 +85,13 @@ class P2PMessagingTest : NodeBasedTest() {
startNode(ALICE.name)
).getOrThrow()
assertAllNodesAreUsed(listOf(networkMapNode, serviceNode2), serviceName, alice)
assertAllNodesAreUsed(listOf(networkMapNode, serviceNode2), DISTRIBUTED_SERVICE_NAME, alice)
}
@Test
fun `communicating with a distributed service which we're part of`() {
val serviceName = "Distributed Service"
val distributedService = startNotaryCluster(serviceName, 2).getOrThrow()
assertAllNodesAreUsed(distributedService, serviceName, distributedService[0])
val distributedService = startNotaryCluster(DISTRIBUTED_SERVICE_NAME, 2).getOrThrow()
assertAllNodesAreUsed(distributedService, DISTRIBUTED_SERVICE_NAME, distributedService[0])
}
@Test
@ -183,13 +188,13 @@ class P2PMessagingTest : NodeBasedTest() {
return Pair(firstRequestReceived, requestsReceived)
}
private fun assertAllNodesAreUsed(participatingServiceNodes: List<Node>, serviceName: String, originatingNode: Node) {
private fun assertAllNodesAreUsed(participatingServiceNodes: List<Node>, serviceName: X500Name, originatingNode: Node) {
// Setup each node in the distributed service to return back it's NodeInfo so that we can know which node is being used
participatingServiceNodes.forEach { node ->
node.respondWith(node.info)
}
val serviceAddress = originatingNode.services.networkMapCache.run {
originatingNode.net.getAddressOfParty(getPartyInfo(getNotary(serviceName)!!)!!)
originatingNode.net.getAddressOfParty(getPartyInfo(getNotary(serviceName.toString())!!)!!)
}
val participatingNodes = HashSet<Any>()
// Try several times so that we can be fairly sure that any node not participating is not due to Artemis' selection

View File

@ -2,12 +2,15 @@ package net.corda.services.messaging
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.crypto.Party
import net.corda.core.crypto.commonName
import net.corda.core.div
import net.corda.core.getOrThrow
import net.corda.core.node.NodeInfo
import net.corda.core.random63BitValue
import net.corda.core.seconds
import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.sendRequest
@ -21,6 +24,7 @@ import net.corda.testing.node.NodeBasedTest
import net.corda.testing.node.SimpleNode
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import java.time.Instant
import java.util.concurrent.TimeoutException
@ -42,9 +46,9 @@ class P2PSecurityTest : NodeBasedTest() {
@Test
fun `register with the network map service using a legal name different from the TLS CN`() {
startSimpleNode("CN=Attacker,O=R3,OU=corda,L=London,C=UK").use {
startSimpleNode(X500Name(DUMMY_BANK_A.name)).use {
// Register with the network map using a different legal name
val response = it.registerWithNetworkMap("CN=Legit Business,O=R3,OU=corda,L=London,C=UK")
val response = it.registerWithNetworkMap(X500Name(DUMMY_BANK_B.name))
// We don't expect a response because the network map's host verification will prevent a connection back
// to the attacker as the TLS CN will not match the legal name it has just provided
assertThatExceptionOfType(TimeoutException::class.java).isThrownBy {
@ -53,16 +57,16 @@ class P2PSecurityTest : NodeBasedTest() {
}
}
private fun startSimpleNode(legalName: String): SimpleNode {
private fun startSimpleNode(legalName: X500Name): SimpleNode {
val config = TestNodeConfiguration(
baseDirectory = tempFolder.root.toPath() / legalName,
baseDirectory = tempFolder.root.toPath() / legalName.commonName,
myLegalName = legalName,
networkMapService = NetworkMapInfo(networkMapNode.configuration.p2pAddress, networkMapNode.info.legalIdentity.name))
config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name
return SimpleNode(config).apply { start() }
}
private fun SimpleNode.registerWithNetworkMap(registrationName: String): ListenableFuture<NetworkMapService.RegistrationResponse> {
private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture<NetworkMapService.RegistrationResponse> {
val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public), MOCK_VERSION_INFO.platformVersion)
val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX)
val request = RegistrationRequest(registration.toWire(identity.private), net.myAddress)

View File

@ -566,7 +566,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// the legal name is actually validated in some way.
val privKeyFile = dir / privateKeyFileName
val pubIdentityFile = dir / publicKeyFileName
val identityName = serviceName ?: configuration.myLegalName
val identityName = serviceName ?: configuration.myLegalName.toString()
val identityAndKey = if (!privKeyFile.exists()) {
log.info("Identity key not found, generating fresh key!")

View File

@ -14,6 +14,7 @@ import net.corda.core.div
import net.corda.core.exists
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.config.SSLConfiguration
import org.bouncycastle.asn1.x500.X500Name
import java.nio.file.Path
object ConfigHelper {
@ -45,7 +46,7 @@ object ConfigHelper {
*/
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) {
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: X500Name) {
certificatesDirectory.createDirectories()
if (!trustStoreFile.exists()) {
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)

View File

@ -13,6 +13,7 @@ import net.corda.node.utilities.TestClock
import net.corda.nodeapi.User
import net.corda.nodeapi.config.OldConfig
import net.corda.nodeapi.config.SSLConfiguration
import org.bouncycastle.asn1.x500.X500Name
import java.net.URL
import java.nio.file.Path
import java.util.*
@ -20,7 +21,7 @@ import java.util.*
interface NodeConfiguration : SSLConfiguration {
val baseDirectory: Path
override val certificatesDirectory: Path get() = baseDirectory / "certificates"
val myLegalName: String
val myLegalName: X500Name
val networkMapService: NetworkMapInfo?
val minimumPlatformVersion: Int
val nearestCity: String
@ -41,7 +42,7 @@ data class FullNodeConfiguration(
"This is a subsitution value which points to the baseDirectory and is manually added into the config before parsing",
ReplaceWith("baseDirectory"))
val basedir: Path,
override val myLegalName: String,
override val myLegalName: X500Name,
override val nearestCity: String,
override val emailAddress: String,
override val keyStorePassword: String,

View File

@ -238,7 +238,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
.loadCertificateFromKeyStore(config.keyStoreFile, config.keyStorePassword, CORDA_CLIENT_CA)
val ourSubjectDN = X500Name(ourCertificate.subjectDN.name)
// This is a sanity check and should not fail unless things have been misconfigured
require(ourSubjectDN.commonName == config.myLegalName) {
require(ourSubjectDN.commonName == config.myLegalName.commonName) {
"Legal name does not match with our subject CN: $ourSubjectDN"
}
val defaultCertPolicies = mapOf(
@ -398,18 +398,18 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort"
// This is called on one of Artemis' background threads
internal fun hostVerificationFail(peerLegalName: String, expectedCommonName: String) {
log.error("Peer has wrong CN - expected $expectedCommonName but got $peerLegalName. This is either a fatal " +
internal fun hostVerificationFail(peerLegalName: X500Name, expectedLegalName: X500Name) {
log.error("Peer has wrong CN - expected $expectedLegalName but got $peerLegalName. This is either a fatal " +
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!")
if (expectedCommonName == config.networkMapService?.legalName) {
if (expectedLegalName.toString() == config.networkMapService?.legalName) {
// If the peer that failed host verification was the network map node then we're in big trouble and need to bail!
_networkMapConnectionFuture!!.setException(IOException("${config.networkMapService} failed host verification check"))
}
}
// This is called on one of Artemis' background threads
internal fun onTcpConnection(peerLegalName: String) {
if (peerLegalName == config.networkMapService?.legalName) {
internal fun onTcpConnection(peerLegalName: X500Name) {
if (peerLegalName.toString() == config.networkMapService?.legalName) {
_networkMapConnectionFuture!!.set(Unit)
}
}
@ -437,7 +437,9 @@ private class VerifyingNettyConnector(configuration: MutableMap<String, Any>?,
protocolManager: ClientProtocolManager?) :
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) {
private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer
private val expectedCommonName = configuration?.get(ArtemisTcpTransport.VERIFY_PEER_COMMON_NAME) as? String
private val expectedCommonName = (configuration?.get(ArtemisTcpTransport.VERIFY_PEER_COMMON_NAME) as? String)?.let {
X500Name(it)
}
override fun createConnection(): Connection? {
val connection = super.createConnection() as NettyConnection?
@ -451,9 +453,8 @@ private class VerifyingNettyConnector(configuration: MutableMap<String, Any>?,
.peerPrincipal
.name
.let(::X500Name)
.commonName
// TODO Verify on the entire principle (subject)
if (peerLegalName != expectedCommonName) {
if (peerLegalName.commonName != expectedCommonName.commonName) {
connection.close()
server!!.hostVerificationFail(peerLegalName, expectedCommonName)
return null // Artemis will keep trying to reconnect until it's told otherwise

View File

@ -547,7 +547,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
}
}
private fun createRPCDispatcher(ops: RPCOps, userService: RPCUserService, nodeLegalName: String) =
private fun createRPCDispatcher(ops: RPCOps, userService: RPCUserService, nodeLegalName: X500Name): RPCDispatcher =
object : RPCDispatcher(ops, userService, nodeLegalName) {
override fun send(data: SerializedBytes<*>, toAddress: String) {
messagingExecutor.fetchFrom {

View File

@ -37,7 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger
* are handled, this is probably the wrong system.
*/
// TODO remove the nodeLegalName parameter once the webserver doesn't need special privileges
abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService, val nodeLegalName: String) {
abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService, val nodeLegalName: X500Name) {
// Throw an exception if there are overloaded methods
private val methodTable = ops.javaClass.declaredMethods.groupBy { it.name }.mapValues { it.value.single() }
@ -184,9 +184,14 @@ abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService, v
val rpcUser = userService.getUser(validatedUser)
if (rpcUser != null) {
return rpcUser
} else if (X500Name(validatedUser).commonName == nodeLegalName) {
return nodeUser
} else {
try {
if (X500Name(validatedUser) == nodeLegalName) {
return nodeUser
}
} catch (ex: IllegalArgumentException) {
// Just means the two can't be compared, treat as no match
}
throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user nor the NODE user")
}
}

View File

@ -100,7 +100,7 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService:
private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String {
// Retrieve request id from file if exists, else post a request to server.
return if (!requestIdStore.exists()) {
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, config.nearestCity, config.emailAddress, keyPair)
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, keyPair)
val writer = StringWriter()
JcaPEMWriter(writer).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded))

View File

@ -34,7 +34,7 @@ class InteractiveShellTest {
}
private val someCorpLegalName = MEGA_CORP.name
private val ids = InMemoryIdentityService().apply { registerIdentity(Party(MEGA_CORP.name, DUMMY_PUBKEY_1)) }
private val ids = InMemoryIdentityService().apply { registerIdentity(Party(someCorpLegalName, DUMMY_PUBKEY_1)) }
private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory())
private fun check(input: String, expected: String) {

View File

@ -4,6 +4,7 @@ import net.corda.core.utilities.ALICE
import net.corda.nodeapi.User
import net.corda.testing.testConfiguration
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import java.nio.file.Paths
@ -11,7 +12,7 @@ class FullNodeConfigurationTest {
@Test
fun `Artemis special characters not permitted in RPC usernames`() {
fun configWithRPCUsername(username: String): FullNodeConfiguration {
return testConfiguration(Paths.get("."), ALICE.name, 0).copy(
return testConfiguration(Paths.get("."), X500Name(ALICE.name), 0).copy(
rpcUsers = listOf(User(username, "pass", emptySet())))
}

View File

@ -27,6 +27,7 @@ import net.corda.testing.freeLocalHostAndPort
import net.corda.testing.node.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database
import org.junit.After
import org.junit.Before
@ -71,7 +72,7 @@ class ArtemisMessagingTests {
userService = RPCUserServiceImpl(emptyList())
config = TestNodeConfiguration(
baseDirectory = baseDirectory,
myLegalName = ALICE.name,
myLegalName = X500Name(ALICE.name),
networkMapService = null)
LogHelper.setLevel(PersistentUniquenessProvider::class)
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())

View File

@ -39,7 +39,7 @@ class NetworkRegistrationHelperTest {
val config = TestNodeConfiguration(
baseDirectory = tempFolder.root.toPath(),
myLegalName = ALICE.name,
myLegalName = X500Name(ALICE.name),
networkMapService = null)
assertFalse(config.keyStoreFile.exists())