diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a5893b7cb8..84b4d41817 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1678,27 +1678,6 @@ public @interface net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowHandle startFlow(net.corda.core.flows.FlowLogic) @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowProgressHandle startTrackedFlow(net.corda.core.flows.FlowLogic) ## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.NetworkParameters extends java.lang.Object - public (int, List, java.time.Duration, int, int, java.time.Instant, int) - public final int component1() - @org.jetbrains.annotations.NotNull public final List component2() - @org.jetbrains.annotations.NotNull public final java.time.Duration component3() - public final int component4() - public final int component5() - @org.jetbrains.annotations.NotNull public final java.time.Instant component6() - public final int component7() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters copy(int, List, java.time.Duration, int, int, java.time.Instant, int) - public boolean equals(Object) - public final int getEpoch() - @org.jetbrains.annotations.NotNull public final java.time.Duration getEventHorizon() - public final int getMaxMessageSize() - public final int getMaxTransactionSize() - public final int getMinimumPlatformVersion() - @org.jetbrains.annotations.NotNull public final java.time.Instant getModifiedTime() - @org.jetbrains.annotations.NotNull public final List getNotaries() - public int hashCode() - public String toString() -## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.NodeInfo extends java.lang.Object public (List, List, int, long) @org.jetbrains.annotations.NotNull public final List component1() @@ -1717,17 +1696,6 @@ public @interface net.corda.core.messaging.RPCReturnsObservables public final boolean isLegalIdentity(net.corda.core.identity.Party) public String toString() ## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.NotaryInfo extends java.lang.Object - public (net.corda.core.identity.Party, boolean) - @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() - public final boolean component2() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.NotaryInfo copy(net.corda.core.identity.Party, boolean) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getIdentity() - public final boolean getValidating() - public int hashCode() - public String toString() -## @net.corda.core.DoNotImplement public interface net.corda.core.node.ServiceHub extends net.corda.core.node.ServicesForResolution @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt deleted file mode 100644 index d5a035175a..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.core.node - -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import java.time.Duration -import java.time.Instant - -/** - * @property minimumPlatformVersion - * @property notaries - * @property eventHorizon - * @property maxMessageSize Maximum P2P message sent over the wire in bytes. - * @property maxTransactionSize Maximum permitted transaction size in bytes. - * @property modifiedTime - * @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set - * of parameters. - */ -// TODO Wire up the parameters -@CordaSerializable -data class NetworkParameters( - val minimumPlatformVersion: Int, - val notaries: List, - val eventHorizon: Duration, - val maxMessageSize: Int, - val maxTransactionSize: Int, - val modifiedTime: Instant, - val epoch: Int -) { - init { - require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } - require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } - require(epoch > 0) { "epoch must be at least 1" } - } -} - -/** - * - */ -@CordaSerializable -data class NotaryInfo(val identity: Party, val validating: Boolean) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt new file mode 100644 index 0000000000..871b1e514f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt @@ -0,0 +1,75 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.verify +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import java.security.SignatureException +import java.security.cert.CertPathValidatorException +import java.security.cert.X509Certificate +import java.time.Duration +import java.time.Instant + +// TODO: Need more discussion on rather we should move this class out of internal. +/** + * Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes. + */ +@CordaSerializable +data class NetworkMap(val nodeInfoHashes: List, val networkParameterHash: SecureHash) + +/** + * @property minimumPlatformVersion + * @property notaries + * @property eventHorizon + * @property maxMessageSize Maximum P2P message sent over the wire in bytes. + * @property maxTransactionSize Maximum permitted transaction size in bytes. + * @property modifiedTime + * @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set + * of parameters. + */ +// TODO Wire up the parameters +@CordaSerializable +data class NetworkParameters( + val minimumPlatformVersion: Int, + val notaries: List, + val eventHorizon: Duration, + val maxMessageSize: Int, + val maxTransactionSize: Int, + val modifiedTime: Instant, + val epoch: Int +) { + init { + require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } + require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } + require(epoch > 0) { "epoch must be at least 1" } + } +} + +@CordaSerializable +data class NotaryInfo(val identity: Party, val validating: Boolean) + +/** + * A serialized [NetworkMap] and its signature and certificate. Enforces signature validity in order to deserialize the data + * contained within. + */ +@CordaSerializable +class SignedNetworkMap(val raw: SerializedBytes, val sig: DigitalSignatureWithCert) { + /** + * Return the deserialized NetworkMap if the signature and certificate can be verified. + * + * @throws CertPathValidatorException if the certificate path is invalid. + * @throws SignatureException if the signature is invalid. + */ + @Throws(SignatureException::class) + fun verified(): NetworkMap { + sig.by.publicKey.verify(raw.bytes, sig) + return raw.deserialize() + } +} + +// TODO: This class should reside in the [DigitalSignature] class. +/** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */ +class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt index 8a5cdbcaa3..c7c8e07232 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt @@ -6,6 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification import rx.exceptions.OnErrorNotImplementedException +import sun.security.x509.X509CertImpl import java.util.* /** @@ -58,6 +59,9 @@ object DefaultWhitelist : SerializationWhitelist { java.util.LinkedHashMap::class.java, BitSet::class.java, OnErrorNotImplementedException::class.java, - StackTraceElement::class.java - ) + StackTraceElement::class.java, + + // Implementation of X509Certificate. + X509CertImpl::class.java + ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt index 7d69264a43..5470dd2cf0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt @@ -44,6 +44,7 @@ import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger import sun.security.ec.ECPublicKeyImpl import sun.security.provider.certpath.X509CertPath +import sun.security.x509.X509CertImpl import java.io.BufferedInputStream import java.io.ByteArrayOutputStream import java.io.FileInputStream @@ -75,6 +76,7 @@ object DefaultKryoCustomizer { addDefaultSerializer(InputStream::class.java, InputStreamSerializer) addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) addDefaultSerializer(Logger::class.java, LoggerSerializer) + addDefaultSerializer(X509Certificate::class.java, X509CertificateSerializer) // WARNING: reordering the registrations here will cause a change in the serialized form, since classes // with custom serializers get written as registration ids. This will break backwards-compatibility. @@ -108,7 +110,6 @@ object DefaultKryoCustomizer { register(FileInputStream::class.java, InputStreamSerializer) register(CertPath::class.java, CertPathSerializer) register(X509CertPath::class.java, CertPathSerializer) - register(X509Certificate::class.java, X509CertificateSerializer) register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPublicKey::class.java, publicKeySerializer) register(BCRSAPrivateCrtKey::class.java, PrivateKeySerializer) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index e7d05ff80e..441cb780e5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -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 diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index c428422eb5..d82386ab5b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -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) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 0b6ef924de..2066b43a94 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -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() - 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 diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 02f4310061..9842d6776e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -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) { @@ -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() + 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>() + 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, 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) } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 10086d5108..2fccc140ba 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -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 diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index fc4b037de9..04df884285 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -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()) } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 3390d32968..12086491ca 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -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 = 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 = 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() } } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 729f68fe92..85526280af 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.minutes +import net.corda.core.utilities.seconds import net.corda.node.internal.NodeStartup import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY @@ -63,7 +64,7 @@ class DriverTests { @Test fun `node registration`() { val handler = RegistrationHandler() - NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), handler).use { + NetworkMapServer(1.seconds, portAllocation.nextHostAndPort(), handler).use { val (host, port) = it.start() driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) { // Wait for the node to have started. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index dae08d3c7d..3a61e18f2b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -17,7 +17,6 @@ import net.corda.core.internal.* import net.corda.core.internal.concurrent.* import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo -import net.corda.core.node.NotaryInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NotaryService import net.corda.core.toFuture @@ -34,6 +33,7 @@ import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.NodeInfoFilesCopier import net.corda.nodeapi.User import net.corda.nodeapi.config.toConfig +import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.* import net.corda.testing.common.internal.NetworkParametersCopier diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 9ae8a956b7..ed5806098f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -17,7 +17,6 @@ import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient 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.KeyManagementService import net.corda.core.serialization.SerializationWhitelist @@ -39,12 +38,13 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.internal.NotaryInfo import net.corda.testing.DUMMY_NOTARY import net.corda.testing.common.internal.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.setGlobalSerialization import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.setGlobalSerialization import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.sshd.common.util.security.SecurityUtils @@ -159,29 +159,32 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete * Returns the single notary node on the network. Throws if there are none or more than one. * @see notaryNodes */ - val defaultNotaryNode: StartedNode get() { - return when (notaryNodes.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryNodes[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") + val defaultNotaryNode: StartedNode + get() { + return when (notaryNodes.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryNodes[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } } - } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentity: Party get() { - return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentity: Party + get() { + return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentityAndCert: PartyAndCertificate get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentityAndCert: PartyAndCertificate + get() { + return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Because this executor is shared, we need to be careful about nodes shutting it down. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt index 94ebdcb199..1e3df5b434 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/network/NetworkMapServer.kt @@ -1,13 +1,25 @@ package net.corda.testing.node.network -import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData import net.corda.core.crypto.sha256 +import net.corda.core.internal.cert +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.hours +import net.corda.nodeapi.internal.DigitalSignatureWithCert +import net.corda.nodeapi.internal.NetworkMap +import net.corda.nodeapi.internal.NetworkParameters +import net.corda.nodeapi.internal.SignedNetworkMap +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.ROOT_CA +import org.bouncycastle.asn1.x500.X500Name import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.HandlerCollection @@ -19,15 +31,34 @@ import java.io.Closeable import java.io.InputStream import java.net.InetSocketAddress import java.time.Duration +import java.time.Instant import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response import javax.ws.rs.core.Response.ok -class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, vararg additionalServices: Any) : Closeable { - private val server: Server +class NetworkMapServer(cacheTimeout: Duration, + hostAndPort: NetworkHostAndPort, + vararg additionalServices: Any) : Closeable { + companion object { + val stubNetworkParameter = NetworkParameters(1, emptyList(), 1.hours, 10, 10, Instant.now(), 10) - private val service = InMemoryNetworkMapService(cacheTimeout) + private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair { + val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val networkMapCert = X509Utilities.createCertificate( + CertificateType.IDENTITY, + rootCAKeyAndCert.certificate, + rootCAKeyAndCert.keyPair, + X500Name("CN=Corda Network Map,L=London"), + networkMapKey.public).cert + return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey) + } + } + + private val server: Server + // Default to ROOT_CA for testing. + // TODO: make this configurable? + private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(ROOT_CA)) init { server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { @@ -68,8 +99,9 @@ class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, } @Path("network-map") - class InMemoryNetworkMapService(private val cacheTimeout: Duration) { - private val nodeInfoMap = mutableMapOf() + class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) { + private val nodeInfoMap = mutableMapOf>() + @POST @Path("publish") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @@ -77,39 +109,48 @@ class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, val registrationData = input.readBytes().deserialize>() val nodeInfo = registrationData.verified() val nodeInfoHash = nodeInfo.serialize().sha256() - nodeInfoMap.put(nodeInfoHash, nodeInfo) + nodeInfoMap.put(nodeInfoHash, registrationData) return ok().build() } @GET - @Produces(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_OCTET_STREAM) fun getNetworkMap(): Response { - return Response.ok(ObjectMapper().writeValueAsString(nodeInfoMap.keys.map { it.toString() })) - .header("Cache-Control", "max-age=${cacheTimeout.seconds}") - .build() + val networkMap = NetworkMap(nodeInfoMap.keys.map { it }, SecureHash.randomSHA256()) + val serializedNetworkMap = networkMap.serialize() + val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes) + val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate.cert, signature)) + return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build() + } + + // Remove nodeInfo for testing. + fun removeNodeInfo(nodeInfo: NodeInfo) { + nodeInfoMap.remove(nodeInfo.serialize().hash) } @GET - @Path("{var}") + @Path("node-info/{var}") @Produces(MediaType.APPLICATION_OCTET_STREAM) fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response { - val nodeInfo = nodeInfoMap[SecureHash.parse(nodeInfoHash)] - return if (nodeInfo != null) { - Response.ok(nodeInfo.serialize().bytes) + val signedNodeInfo = nodeInfoMap[SecureHash.parse(nodeInfoHash)] + return if (signedNodeInfo != null) { + Response.ok(signedNodeInfo.serialize().bytes) } else { Response.status(Response.Status.NOT_FOUND) }.build() } + @GET + @Path("network-parameter/{var}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + fun getNetworkParameter(@PathParam("var") networkParameterHash: String): Response { + return Response.ok(stubNetworkParameter.serialize().bytes).build() + } + @GET @Path("my-hostname") fun getHostName(): Response { return Response.ok("test.host.name").build() } - - // Remove nodeInfo for testing. - fun removeNodeInfo(nodeInfo: NodeInfo) { - nodeInfoMap.remove(nodeInfo.serialize().hash) - } } } diff --git a/testing/test-common/build.gradle b/testing/test-common/build.gradle index dd2e2285b5..df37173fc1 100644 --- a/testing/test-common/build.gradle +++ b/testing/test-common/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':core') + compile project(':node-api') } jar { diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/NetworkParametersCopier.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/NetworkParametersCopier.kt index 25ba327a73..f8a8d26952 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/NetworkParametersCopier.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/NetworkParametersCopier.kt @@ -5,8 +5,8 @@ import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.sign import net.corda.core.internal.copyTo import net.corda.core.internal.div -import net.corda.core.node.NetworkParameters import net.corda.core.serialization.serialize +import net.corda.nodeapi.internal.NetworkParameters import java.math.BigInteger import java.nio.file.FileAlreadyExistsException import java.nio.file.Path diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index da40ba6ac3..ea84cd88c8 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -1,8 +1,8 @@ package net.corda.testing.common.internal -import net.corda.core.node.NetworkParameters -import net.corda.core.node.NotaryInfo import net.corda.core.utilities.days +import net.corda.nodeapi.internal.NetworkParameters +import net.corda.nodeapi.internal.NotaryInfo import java.time.Instant fun testNetworkParameters(notaries: List): NetworkParameters { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index 96f452e74c..4f3eb0f66e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -70,6 +70,12 @@ val DEV_CA: CertificateAndKeyPair by lazy { val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") } + +val ROOT_CA: CertificateAndKeyPair by lazy { + // TODO: Should be identity scheme + val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, "cordacadevkeypass") +} val DEV_TRUST_ROOT: X509CertificateHolder by lazy { // TODO: Should be identity scheme val caKeyStore = loadKeyStore(ClassLoader.getSystemResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")