Use NetworkMap and SignedNetworkMap in NetworkMapClient, and enable signature verification. (#2054)

* new network map object for network map, and verify signature and root in Signed network map and node info

* fixup after rebase

* * added certificate and key to network map server
* move DigitalSignature.WithCert back to NetworkMap.kt, as its breaking API test, will raise another PR to move it back.
* Make DigitalSignature.WithCert not extend WithKey, as per PR discussion.
* various fixes after rebase.

* move Network map back to core/node, as its breaking API test

* revert unintended changes

* move network map objects to node-api
This commit is contained in:
Patrick Kuo
2017-11-29 15:55:13 +00:00
committed by GitHub
parent cc1fba641e
commit 572c4af40c
20 changed files with 245 additions and 147 deletions

View File

@ -13,7 +13,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.deleteIfExists
import net.corda.core.internal.div
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.NotaryService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
@ -26,6 +25,7 @@ import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.nodeapi.internal.NotaryInfo
import net.corda.testing.chooseIdentity
import net.corda.testing.common.internal.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters

View File

@ -1,7 +1,6 @@
package net.corda.node.services.network
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.testing.ALICE
import net.corda.testing.BOB
@ -18,7 +17,7 @@ class NetworkMapClientTest {
@Test
fun `nodes can see each other using the http network map`() {
NetworkMapServer(1.minutes, portAllocation.nextHostAndPort()).use {
NetworkMapServer(1.seconds, portAllocation.nextHostAndPort()).use {
val (host, port) = it.start()
driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) {
val alice = startNode(providedName = ALICE.name)

View File

@ -44,15 +44,8 @@ import net.corda.node.services.events.ScheduledActivityObserver
import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.PersistentKeyManagementService
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.NetworkMapCacheImpl
import net.corda.node.services.network.NodeInfoWatcher
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.*
import net.corda.node.services.network.*
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.persistence.DBTransactionMappingStorage
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.*
import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.statemachine.*
@ -64,6 +57,7 @@ import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.nodeapi.internal.NetworkParameters
import net.corda.nodeapi.internal.crypto.*
import org.apache.activemq.artemis.utils.ReusableLatch
import org.slf4j.Logger
@ -137,7 +131,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected lateinit var network: MessagingService
protected val runOnStop = ArrayList<() -> Any?>()
protected val _nodeReadyFuture = openFuture<Unit>()
protected val networkMapClient: NetworkMapClient? by lazy { configuration.compatibilityZoneURL?.let(::NetworkMapClient) }
protected val networkMapClient: NetworkMapClient? by lazy {
configuration.compatibilityZoneURL?.let {
NetworkMapClient(it, services.identityService.trustRoot)
}
}
lateinit var userService: RPCUserService get

View File

@ -1,6 +1,5 @@
package net.corda.node.services.network
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.util.concurrent.MoreExecutors
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
@ -13,6 +12,10 @@ import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory
import net.corda.nodeapi.internal.NetworkMap
import net.corda.nodeapi.internal.NetworkParameters
import net.corda.nodeapi.internal.SignedNetworkMap
import net.corda.nodeapi.internal.crypto.X509Utilities
import okhttp3.CacheControl
import okhttp3.Headers
import rx.Subscription
@ -20,11 +23,12 @@ import java.io.BufferedReader
import java.io.Closeable
import java.net.HttpURLConnection
import java.net.URL
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class NetworkMapClient(compatibilityZoneURL: URL) {
class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) {
private val networkMapUrl = URL("$compatibilityZoneURL/network-map")
fun publish(signedNodeInfo: SignedData<NodeInfo>) {
@ -42,14 +46,30 @@ class NetworkMapClient(compatibilityZoneURL: URL) {
fun getNetworkMap(): NetworkMapResponse {
val conn = networkMapUrl.openHttpConnection()
val response = conn.inputStream.bufferedReader().use(BufferedReader::readLine)
val networkMap = ObjectMapper().readValue(response, List::class.java).map { SecureHash.parse(it.toString()) }
val signedNetworkMap = conn.inputStream.use { it.readBytes() }.deserialize<SignedNetworkMap>()
val networkMap = signedNetworkMap.verified()
// Assume network map cert is issued by the root.
X509Utilities.validateCertificateChain(trustedRoot, signedNetworkMap.sig.by, trustedRoot)
val timeout = CacheControl.parse(Headers.of(conn.headerFields.filterKeys { it != null }.mapValues { it.value.first() })).maxAgeSeconds().seconds
return NetworkMapResponse(networkMap, timeout)
}
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? {
val conn = URL("$networkMapUrl/$nodeInfoHash").openHttpConnection()
val conn = URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection()
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize<SignedData<NodeInfo>>()
val nodeInfo = signedNodeInfo.verified()
// Verify node info is signed by node identity
// TODO : Validate multiple signatures when NodeInfo supports multiple identities.
require(nodeInfo.legalIdentities.any { it.owningKey == signedNodeInfo.sig.by }) { "NodeInfo must be signed by the node owning key." }
nodeInfo
}
}
fun getNetworkParameter(networkParameterHash: SecureHash): NetworkParameters? {
val conn = URL("$networkMapUrl/network-parameter/$networkParameterHash").openHttpConnection()
return if (conn.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
@ -63,7 +83,7 @@ class NetworkMapClient(compatibilityZoneURL: URL) {
}
}
data class NetworkMapResponse(val networkMap: List<SecureHash>, val cacheMaxAge: Duration)
data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Duration)
class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val fileWatcher: NodeInfoWatcher,
@ -107,21 +127,28 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
val nextScheduleDelay = try {
val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap()
val currentNodeHashes = networkMapCache.allNodeHashes
(networkMap - currentNodeHashes).mapNotNull {
val hashesFromNetworkMap = networkMap.nodeInfoHashes
(hashesFromNetworkMap - currentNodeHashes).mapNotNull {
// Download new node info from network map
networkMapClient.getNodeInfo(it)
try {
networkMapClient.getNodeInfo(it)
} catch (t: Throwable) {
// Failure to retrieve one node info shouldn't stop the whole update, log and return null instead.
logger.warn("Error encountered when downloading node info '$it', skipping...", t)
null
}
}.forEach {
// Add new node info to the network map cache, these could be new node info or modification of node info for existing nodes.
networkMapCache.addNode(it)
}
// Remove node info from network map.
(currentNodeHashes - networkMap - fileWatcher.processedNodeInfoHashes)
(currentNodeHashes - hashesFromNetworkMap - fileWatcher.processedNodeInfoHashes)
.mapNotNull(networkMapCache::getNodeByHash)
.forEach(networkMapCache::removeNode)
// TODO: Check NetworkParameter.
cacheTimeout
} catch (t: Throwable) {
logger.warn("Error encountered while updating network map, will retry in $retryInterval", t)
logger.warn("Error encountered while updating network map, will retry in ${retryInterval.seconds} seconds", t)
retryInterval
}
// Schedule the next update.
@ -137,7 +164,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
try {
networkMapClient.publish(signedNodeInfo)
} catch (t: Throwable) {
logger.warn("Error encountered while publishing node info, will retry in $retryInterval.", t)
logger.warn("Error encountered while publishing node info, will retry in ${retryInterval.seconds} seconds.", t)
// TODO: Exponential backoff?
executor.schedule(this, retryInterval.toMillis(), TimeUnit.MILLISECONDS)
}

View File

@ -12,7 +12,6 @@ import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.schemas.NodeInfoSchemaV1
import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.PartyInfo
@ -25,6 +24,7 @@ import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.bufferUntilDatabaseCommit
import net.corda.node.utilities.wrapWithDatabaseTransaction
import net.corda.nodeapi.internal.NotaryInfo
import org.hibernate.Session
import rx.Observable
import rx.subjects.PublishSubject

View File

@ -1,11 +1,14 @@
package net.corda.node.services.network
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.cert
import net.corda.core.serialization.serialize
import net.corda.core.utilities.seconds
import net.corda.node.services.network.TestNodeInfoFactory.createNodeInfo
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.DEV_CA
import net.corda.testing.DEV_TRUST_ROOT
import net.corda.testing.ROOT_CA
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.network.NetworkMapServer
@ -16,6 +19,7 @@ import org.junit.Rule
import org.junit.Test
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class NetworkMapClientTest {
@Rule
@ -32,7 +36,7 @@ class NetworkMapClientTest {
fun setUp() {
server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort())
val hostAndPort = server.start()
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"))
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_TRUST_ROOT.cert)
}
@After
@ -50,7 +54,7 @@ class NetworkMapClientTest {
val nodeInfoHash = nodeInfo.serialize().sha256()
assertThat(networkMapClient.getNetworkMap().networkMap).containsExactly(nodeInfoHash)
assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash)
assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash))
val signedNodeInfo2 = createNodeInfo("Test2")
@ -58,13 +62,22 @@ class NetworkMapClientTest {
networkMapClient.publish(signedNodeInfo2)
val nodeInfoHash2 = nodeInfo2.serialize().sha256()
assertThat(networkMapClient.getNetworkMap().networkMap).containsExactly(nodeInfoHash, nodeInfoHash2)
assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash, nodeInfoHash2)
assertEquals(cacheTimeout, networkMapClient.getNetworkMap().cacheMaxAge)
assertEquals(nodeInfo2, networkMapClient.getNodeInfo(nodeInfoHash2))
}
@Test
fun `download NetworkParameter correctly`() {
// The test server returns same network parameter for any hash.
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())
assertNotNull(networkParameter)
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter)
}
@Test
fun `get hostname string from http response correctly`() {
assertEquals("test.host.name", networkMapClient.myPublicHostname())
assertEquals("test.host.name", networkMapClient.myPublicHostname())
}
}

View File

@ -13,6 +13,7 @@ import net.corda.core.crypto.SignedData
import net.corda.core.identity.Party
import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
import net.corda.nodeapi.internal.NetworkMap
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
@ -95,7 +96,7 @@ class NetworkMapUpdaterTest {
val signedNodeInfo: SignedData<NodeInfo> = uncheckedCast(it.arguments.first())
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then { NetworkMapResponse(nodeInfoMap.keys.toList(), 100.millis) }
on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) }
on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() }
}
@ -149,7 +150,7 @@ class NetworkMapUpdaterTest {
val signedNodeInfo: SignedData<NodeInfo> = uncheckedCast(it.arguments.first())
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then { NetworkMapResponse(nodeInfoMap.keys.toList(), 100.millis) }
on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), SecureHash.randomSHA256()), 100.millis) }
on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() }
}