diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 0db44ba841..7bd11ec661 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -22,7 +22,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). */ @Throws(InvalidKeyException::class, SignatureException::class) - fun verify(content: ByteArray) = by.verify(content, this) + fun verify(content: ByteArray): Boolean = by.verify(content, this) /** * Utility to simplify the act of verifying a signature. @@ -32,7 +32,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). */ @Throws(InvalidKeyException::class, SignatureException::class) - fun verify(content: OpaqueBytes) = by.verify(content.bytes, this) + fun verify(content: OpaqueBytes): Boolean = by.verify(content.bytes, this) /** * Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an @@ -45,7 +45,8 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { * @return whether the signature is correct for this key. */ @Throws(InvalidKeyException::class, SignatureException::class) - fun isValid(content: ByteArray) = by.isValid(content, this) - fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes) + fun isValid(content: ByteArray): Boolean = by.isValid(content, this) + + fun withoutKey(): DigitalSignature = DigitalSignature(this.bytes) } } 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 index e29e0f6ca0..a9bf3ca97b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkMap.kt @@ -4,6 +4,7 @@ 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.node.NodeInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -59,7 +60,7 @@ data class NotaryInfo(val identity: Party, val validating: Boolean) * contained within. */ @CordaSerializable -class SignedNetworkMap(val raw: SerializedBytes, val sig: DigitalSignatureWithCert) { +class SignedNetworkMap(val raw: SerializedBytes, val signature: DigitalSignatureWithCert) { /** * Return the deserialized NetworkMap if the signature and certificate can be verified. * @@ -68,13 +69,14 @@ class SignedNetworkMap(val raw: SerializedBytes, val sig: DigitalSig */ @Throws(SignatureException::class, CertPathValidatorException::class) fun verified(trustedRoot: X509Certificate): NetworkMap { - sig.by.publicKey.verify(raw.bytes, sig) + signature.by.publicKey.verify(raw.bytes, signature) // Assume network map cert is under the default trust root. - X509Utilities.validateCertificateChain(trustedRoot, sig.by, trustedRoot) + X509Utilities.validateCertificateChain(trustedRoot, signature.by, trustedRoot) return raw.deserialize() } } // TODO: This class should reside in the [DigitalSignature] class. +// TODO: Removing the val from signatureBytes causes serialisation issues /** 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 +class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt index c903938b22..6e0f5c4c40 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NetworkParametersGenerator.kt @@ -1,7 +1,6 @@ package net.corda.nodeapi.internal import com.typesafe.config.ConfigFactory -import net.corda.core.crypto.SignedData import net.corda.core.identity.Party import net.corda.core.internal.div import net.corda.core.internal.list @@ -78,7 +77,7 @@ class NetworkParametersGenerator { private fun processFile(file: Path): NodeInfo? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readAll().deserialize>() + val signedData = file.readAll().deserialize() signedData.verified() } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt new file mode 100644 index 0000000000..1b0ed15348 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt @@ -0,0 +1,44 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.verify +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import java.security.SignatureException + +/** + * A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected + * to be in the same order as the identities. + */ +// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key +// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node +// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite +// public keys. +@CordaSerializable +class SignedNodeInfo(val raw: SerializedBytes, val signatures: List) { + fun verified(): NodeInfo { + val nodeInfo = raw.deserialize() + val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey } + + if (identities.size < signatures.size) { + throw SignatureException("Extra signatures. Found ${signatures.size} expected ${identities.size}") + } + if (identities.size > signatures.size) { + throw SignatureException("Missing signatures. Found ${signatures.size} expected ${identities.size}") + } + + val rawBytes = raw.bytes // To avoid cloning the byte array multiple times + identities.zip(signatures).forEach { (identity, signature) -> + try { + identity.owningKey.verify(rawBytes, signature) + } catch (e: SignatureException) { + throw SignatureException("$identity: ${e.message}") + } + } + + return nodeInfo + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt new file mode 100644 index 0000000000..0ad99ae579 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/SignedNodeInfoTest.kt @@ -0,0 +1,80 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.Crypto +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.TestNodeInfoBuilder +import net.corda.testing.internal.signWith +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Rule +import org.junit.Test +import java.security.SignatureException + +class SignedNodeInfoTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val nodeInfoBuilder = TestNodeInfoBuilder() + + @Test + fun `verifying single identity`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() + assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) + } + + @Test + fun `verifying multiple identities`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + nodeInfoBuilder.addIdentity(BOB_NAME) + val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() + assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) + } + + @Test + fun `verifying missing signature`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + nodeInfoBuilder.addIdentity(BOB_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining("Missing signatures") + } + + @Test + fun `verifying extra signature`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining("Extra signatures") + } + + @Test + fun `verifying incorrect signature`() { + nodeInfoBuilder.addIdentity(ALICE_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining(ALICE_NAME.toString()) + } + + @Test + fun `verifying with signatures in wrong order`() { + val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) + val (_, bobKey) = nodeInfoBuilder.addIdentity(BOB_NAME) + val nodeInfo = nodeInfoBuilder.build() + val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey)) + assertThatThrownBy { signedNodeInfo.verified() } + .isInstanceOf(SignatureException::class.java) + .hasMessageContaining(ALICE_NAME.toString()) + } + + private fun generateKeyPair() = Crypto.generateKeyPair() +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index ba071c1fa1..cac6163282 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -3,14 +3,15 @@ package net.corda.node.services.network import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs import net.corda.cordform.CordformNode -import net.corda.core.crypto.SignedData import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService -import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.NodeInfoFilesCopier -import net.corda.testing.* +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.testing.ALICE_NAME +import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -27,20 +28,20 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeInfoWatcherTest { - private companion object { - val alice = TestIdentity(ALICE_NAME, 70) - val nodeInfo = NodeInfo(listOf(), listOf(alice.identity), 0, 0) - } - @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + @Rule @JvmField val tempFolder = TemporaryFolder() - private lateinit var nodeInfoPath: Path + private val scheduler = TestScheduler() private val testSubscriber = TestSubscriber() + + private lateinit var nodeInfo: NodeInfo + private lateinit var signedNodeInfo: SignedNodeInfo + private lateinit var nodeInfoPath: Path private lateinit var keyManagementService: KeyManagementService // Object under test @@ -48,8 +49,11 @@ class NodeInfoWatcherTest { @Before fun start() { + val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) + nodeInfo = nodeInfoAndSigned.first + signedNodeInfo = nodeInfoAndSigned.second val identityService = makeTestIdentityService() - keyManagementService = MockKeyManagementService(identityService, alice.keyPair) + keyManagementService = MockKeyManagementService(identityService) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) nodeInfoPath = tempFolder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY } @@ -58,7 +62,6 @@ class NodeInfoWatcherTest { fun `save a NodeInfo`() { assertEquals(0, tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size) - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo) val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) } @@ -74,7 +77,6 @@ class NodeInfoWatcherTest { fun `save a NodeInfo to JimFs`() { val jimFs = Jimfs.newFileSystem(Configuration.unix()) val jimFolder = jimFs.getPath("/nodeInfo") - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo) } @@ -82,11 +84,9 @@ class NodeInfoWatcherTest { fun `load an empty Directory`() { nodeInfoPath.createDirectories() - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) try { advanceTime() - val readNodes = testSubscriber.onNextEvents.distinct() assertEquals(0, readNodes.size) } finally { @@ -96,15 +96,13 @@ class NodeInfoWatcherTest { @Test fun `load a non empty Directory`() { - createNodeInfoFileInPath(nodeInfo) + createNodeInfoFileInPath() - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) advanceTime() try { val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(1, readNodes.size) assertEquals(nodeInfo, readNodes.first()) } finally { @@ -117,14 +115,13 @@ class NodeInfoWatcherTest { nodeInfoPath.createDirectories() // Start polling with an empty folder. - val subscription = nodeInfoWatcher.nodeInfoUpdates() - .subscribe(testSubscriber) + val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) try { // Ensure the watch service is started. advanceTime() // Check no nodeInfos are read. assertEquals(0, testSubscriber.valueCount) - createNodeInfoFileInPath(nodeInfo) + createNodeInfoFileInPath() advanceTime() @@ -143,8 +140,7 @@ class NodeInfoWatcherTest { } // Write a nodeInfo under the right path. - private fun createNodeInfoFileInPath(nodeInfo: NodeInfo) { - val signedNodeInfo = SignedData(nodeInfo.serialize(), keyManagementService.sign(nodeInfo.serialize().bytes, nodeInfo.legalIdentities.first().owningKey)) + private fun createNodeInfoFileInPath() { NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo) } } 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 220bb62259..58432c2cd2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -10,6 +10,8 @@ import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.sign import net.corda.core.flows.* @@ -32,10 +34,10 @@ import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler -import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.api.* import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration @@ -59,6 +61,7 @@ import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.internal.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.NetworkParameters +import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -173,6 +176,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, validateKeystore() } + private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes) -> DigitalSignature): SignedNodeInfo { + // For now we assume the node has only one identity (excluding any composite ones) + val owningKey = nodeInfo.legalIdentities.single { it.owningKey !is CompositeKey }.owningKey + val serialised = nodeInfo.serialize() + val signature = sign(owningKey, serialised) + return SignedNodeInfo(serialised, listOf(signature)) + } + open fun generateNodeInfo() { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") @@ -184,11 +195,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // a code smell. val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) - val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey } - val serialisedNodeInfo = info.serialize() - val signature = identityKeypair.sign(serialisedNodeInfo) - // TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature. - NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature)) + val signedNodeInfo = signNodeInfo(info) { publicKey, serialised -> + val privateKey = keyPairs.single { it.public == publicKey }.private + privateKey.sign(serialised.bytes) + } + NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) } } @@ -247,9 +258,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, runOnStop += networkMapUpdater::close networkMapUpdater.updateNodeInfo(services.myInfo) { - val serialisedNodeInfo = it.serialize() - val signature = services.keyManagementService.sign(serialisedNodeInfo.bytes, it.legalIdentities.first().owningKey) - SignedData(serialisedNodeInfo, signature) + signNodeInfo(it) { publicKey, serialised -> + services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() + } } networkMapUpdater.subscribeToNetworkMap() 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 5146c2bc53..d7a76cb008 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 @@ -15,7 +15,7 @@ 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 net.corda.nodeapi.internal.SignedNodeInfo import okhttp3.CacheControl import okhttp3.Headers import rx.Subscription @@ -31,13 +31,13 @@ import java.util.concurrent.TimeUnit class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) { private val networkMapUrl = URL("$compatibilityZoneURL/network-map") - fun publish(signedNodeInfo: SignedData) { + fun publish(signedNodeInfo: SignedNodeInfo) { val publishURL = URL("$networkMapUrl/publish") val conn = publishURL.openHttpConnection() conn.doOutput = true conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/octet-stream") - conn.outputStream.use { it.write(signedNodeInfo.serialize().bytes) } + conn.outputStream.use { signedNodeInfo.serialize().open().copyTo(it) } // This will throw IOException if the response code is not HTTP 200. // This gives a much better exception then reading the error stream. @@ -57,12 +57,8 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C 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 + val signedNodeInfo = conn.inputStream.use { it.readBytes() }.deserialize() + signedNodeInfo.verified() } } @@ -100,7 +96,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS) } - fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedData) { + fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedNodeInfo) { val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) // Compare node info without timestamp. if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return @@ -138,9 +134,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, // Download new node info from network map try { networkMapClient.getNodeInfo(it) - } catch (t: Throwable) { + } catch (e: Exception) { // 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) + logger.warn("Error encountered when downloading node info '$it', skipping...", e) null } }.forEach { @@ -163,7 +159,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, executor.submit(task) // The check may be expensive, so always run it in the background even the first time. } - private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedData, networkMapClient: NetworkMapClient) { + private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) { val task = object : Runnable { override fun run() { try { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index 771604ba6b..c19f1a2195 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -2,7 +2,6 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData import net.corda.core.internal.* import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize @@ -10,6 +9,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.NodeInfoFilesCopier +import net.corda.nodeapi.internal.SignedNodeInfo import rx.Observable import rx.Scheduler import java.io.IOException @@ -41,14 +41,14 @@ class NodeInfoWatcher(private val nodePath: Path, private val logger = contextLogger() /** * Saves the given [NodeInfo] to a path. - * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. + * The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity. * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename * is used so that one can freely copy these files without fearing to overwrite another one. * * @param path the path where to write the file, if non-existent it will be created. * @param signedNodeInfo the signed NodeInfo. */ - fun saveToFile(path: Path, signedNodeInfo: SignedData) { + fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) { try { path.createDirectories() signedNodeInfo.serialize() @@ -85,7 +85,7 @@ class NodeInfoWatcher(private val nodePath: Path, .flatMapIterable { loadFromDirectory() } } - fun saveToFile(signedNodeInfo: SignedData) = Companion.saveToFile(nodePath, signedNodeInfo) + fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo) /** * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. @@ -118,7 +118,7 @@ class NodeInfoWatcher(private val nodePath: Path, private fun processFile(file: Path): NodeInfo? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readAll().deserialize>() + val signedData = file.readAll().deserialize() signedData.verified() } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) 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 0cfa199402..608f583f89 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 @@ -5,10 +5,12 @@ 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.testing.ALICE_NAME +import net.corda.testing.BOB_NAME import net.corda.testing.DEV_TRUST_ROOT import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -23,13 +25,12 @@ class NetworkMapClientTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + + private val cacheTimeout = 100000.seconds + private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient - companion object { - private val cacheTimeout = 100000.seconds - } - @Before fun setUp() { server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort()) @@ -44,9 +45,7 @@ class NetworkMapClientTest { @Test fun `registered node is added to the network map`() { - // Create node info. - val signedNodeInfo = createNodeInfo("Test1") - val nodeInfo = signedNodeInfo.verified() + val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME) networkMapClient.publish(signedNodeInfo) @@ -55,8 +54,8 @@ class NetworkMapClientTest { assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash) assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash)) - val signedNodeInfo2 = createNodeInfo("Test2") - val nodeInfo2 = signedNodeInfo2.verified() + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned(BOB_NAME) + networkMapClient.publish(signedNodeInfo2) val nodeInfoHash2 = nodeInfo2.serialize().sha256() 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 ffee923ea9..98dbb96415 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 @@ -1,69 +1,70 @@ package net.corda.node.services.network -import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify -import net.corda.cordform.CordformNode -import net.corda.core.crypto.Crypto +import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData +import net.corda.core.identity.CordaX500Name 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 import net.corda.core.utilities.millis import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.NetworkMap +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.testing.ALICE_NAME import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.TestNodeInfoBuilder +import net.corda.testing.internal.createNodeInfoAndSigned +import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Rule import org.junit.Test import rx.schedulers.TestScheduler import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals class NetworkMapUpdaterTest { - companion object { - val NETWORK_PARAMS_HASH = SecureHash.randomSHA256() - } - @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val jimFs = Jimfs.newFileSystem(Configuration.unix()) - private val baseDir = jimFs.getPath("/node") + + private val fs = Jimfs.newFileSystem(unix()) + private val baseDir = fs.getPath("/node") + private val networkMapCache = createMockNetworkMapCache() + private val nodeInfoMap = ConcurrentHashMap() + private val cacheExpiryMs = 100 + private val networkMapClient = createMockNetworkMapClient() + private val scheduler = TestScheduler() + private val networkParametersHash = SecureHash.randomSHA256() + private val fileWatcher = NodeInfoWatcher(baseDir, scheduler) + private val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, networkParametersHash) + private val nodeInfoBuilder = TestNodeInfoBuilder() + + @After + fun cleanUp() { + updater.close() + fs.close() + } @Test fun `publish node info`() { - val keyPair = Crypto.generateKeyPair() + nodeInfoBuilder.addIdentity(ALICE_NAME) - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1").verified() - val signedNodeInfo = TestNodeInfoFactory.sign(keyPair, nodeInfo1) - - val sameNodeInfoDifferentTime = nodeInfo1.copy(serial = System.currentTimeMillis()) - val signedSameNodeInfoDifferentTime = TestNodeInfoFactory.sign(keyPair, sameNodeInfoDifferentTime) - - val differentNodeInfo = nodeInfo1.copy(addresses = listOf(NetworkHostAndPort("my.new.host.com", 1000))) - val signedDifferentNodeInfo = TestNodeInfoFactory.sign(keyPair, differentNodeInfo) - - val networkMapCache = getMockNetworkMapCache() - - val networkMapClient = mock() - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, NETWORK_PARAMS_HASH) + val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned() + val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) // Publish node info for the first time. - updater.updateNodeInfo(nodeInfo1) { signedNodeInfo } + updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 } // Sleep as publish is asynchronous. // TODO: Remove sleep in unit test - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapClient, times(1)).publish(any()) networkMapCache.addNode(nodeInfo1) @@ -71,167 +72,144 @@ class NetworkMapUpdaterTest { // Publish the same node info, but with different serial. updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime } // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // Same node info should not publish twice verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime) + val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob") + // Publish different node info. updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo } // TODO: Remove sleep in unit test. Thread.sleep(200) verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo) - - updater.close() } @Test fun `process add node updates from network map, with additional node infos from dir`() { - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1") - val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2") - val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3") - val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4") - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - val networkMapCache = getMockNetworkMapCache() - - val nodeInfoMap = ConcurrentHashMap>() - val networkMapClient = mock { - on { publish(any()) }.then { - val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) - nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) - } - on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), NETWORK_PARAMS_HASH), 100.millis) } - on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } - } - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, NETWORK_PARAMS_HASH) + val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1") + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") + val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") + val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Test adding new node. - networkMapClient.publish(nodeInfo1) + networkMapClient.publish(signedNodeInfo1) // Not subscribed yet. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - networkMapClient.publish(nodeInfo2) + networkMapClient.publish(signedNodeInfo2) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, times(2)).addNode(any()) - verify(networkMapCache, times(1)).addNode(nodeInfo1.verified()) - verify(networkMapCache, times(1)).addNode(nodeInfo2.verified()) + verify(networkMapCache, times(1)).addNode(nodeInfo1) + verify(networkMapCache, times(1)).addNode(nodeInfo2) - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) - networkMapClient.publish(nodeInfo3) - networkMapClient.publish(nodeInfo4) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + networkMapClient.publish(signedNodeInfo3) + networkMapClient.publish(signedNodeInfo4) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // 4 node info from network map, and 1 from file. verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(nodeInfo3.verified()) - verify(networkMapCache, times(1)).addNode(nodeInfo4.verified()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) - - updater.close() + verify(networkMapCache, times(1)).addNode(nodeInfo3) + verify(networkMapCache, times(1)).addNode(nodeInfo4) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) } @Test fun `process remove node updates from network map, with additional node infos from dir`() { - val nodeInfo1 = TestNodeInfoFactory.createNodeInfo("Info 1") - val nodeInfo2 = TestNodeInfoFactory.createNodeInfo("Info 2") - val nodeInfo3 = TestNodeInfoFactory.createNodeInfo("Info 3") - val nodeInfo4 = TestNodeInfoFactory.createNodeInfo("Info 4") - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - val networkMapCache = getMockNetworkMapCache() - - val nodeInfoMap = ConcurrentHashMap>() - val networkMapClient = mock { - on { publish(any()) }.then { - val signedNodeInfo: SignedData = uncheckedCast(it.arguments.first()) - nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) - } - on { getNetworkMap() }.then { NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), NETWORK_PARAMS_HASH), 100.millis) } - on { getNodeInfo(any()) }.then { nodeInfoMap[it.arguments.first()]?.verified() } - } - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, NETWORK_PARAMS_HASH) + val (nodeInfo1, signedNodeInfo1) = createNodeInfoAndSigned("Info 1") + val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") + val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") + val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Add all nodes. - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) - networkMapClient.publish(nodeInfo1) - networkMapClient.publish(nodeInfo2) - networkMapClient.publish(nodeInfo3) - networkMapClient.publish(nodeInfo4) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + networkMapClient.publish(signedNodeInfo1) + networkMapClient.publish(signedNodeInfo2) + networkMapClient.publish(signedNodeInfo3) + networkMapClient.publish(signedNodeInfo4) updater.subscribeToNetworkMap() scheduler.advanceTimeBy(10, TimeUnit.SECONDS) // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) // 4 node info from network map, and 1 from file. - assertEquals(4, nodeInfoMap.size) + assertThat(nodeInfoMap).hasSize(4) verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) // Test remove node. nodeInfoMap.clear() // TODO: Remove sleep in unit test. - Thread.sleep(200) + Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, times(4)).removeNode(any()) - verify(networkMapCache, times(1)).removeNode(nodeInfo1.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo2.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo3.verified()) - verify(networkMapCache, times(1)).removeNode(nodeInfo4.verified()) + verify(networkMapCache, times(1)).removeNode(nodeInfo1) + verify(networkMapCache, times(1)).removeNode(nodeInfo2) + verify(networkMapCache, times(1)).removeNode(nodeInfo3) + verify(networkMapCache, times(1)).removeNode(nodeInfo4) // Node info from file should not be deleted - assertEquals(1, networkMapCache.allNodeHashes.size) - assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first()) - - updater.close() + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) } @Test fun `receive node infos from directory, without a network map`() { - val fileNodeInfo = TestNodeInfoFactory.createNodeInfo("Info from file") - - val networkMapCache = getMockNetworkMapCache() - - val scheduler = TestScheduler() - val fileWatcher = NodeInfoWatcher(baseDir, scheduler) - val updater = NetworkMapUpdater(networkMapCache, fileWatcher, null, NETWORK_PARAMS_HASH) + val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") // Not subscribed yet. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - NodeInfoWatcher.saveToFile(baseDir / CordformNode.NODE_INFO_DIRECTORY, fileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) verify(networkMapCache, times(1)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo.verified()) + verify(networkMapCache, times(1)).addNode(fileNodeInfo) - assertEquals(1, networkMapCache.allNodeHashes.size) - assertEquals(fileNodeInfo.verified().serialize().hash, networkMapCache.allNodeHashes.first()) - - updater.close() + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) } - private fun getMockNetworkMapCache() = mock { - val data = ConcurrentHashMap() - on { addNode(any()) }.then { - val nodeInfo = it.arguments.first() as NodeInfo - data.put(nodeInfo.legalIdentities.first(), nodeInfo) + private fun createMockNetworkMapClient(): NetworkMapClient { + return mock { + on { publish(any()) }.then { + val signedNodeInfo: SignedNodeInfo = uncheckedCast(it.arguments[0]) + nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo) + } + on { getNetworkMap() }.then { + NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), networkParametersHash), cacheExpiryMs.millis) + } + on { getNodeInfo(any()) }.then { + nodeInfoMap[it.arguments[0]]?.verified() + } } - on { removeNode(any()) }.then { data.remove((it.arguments.first() as NodeInfo).legalIdentities.first()) } - on { getNodeByLegalIdentity(any()) }.then { data[it.arguments.first()] } - on { allNodeHashes }.then { data.values.map { it.serialize().hash } } - on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments.first() } } + } + + private fun createMockNetworkMapCache(): NetworkMapCacheInternal { + return mock { + val data = ConcurrentHashMap() + on { addNode(any()) }.then { + val nodeInfo = it.arguments[0] as NodeInfo + data.put(nodeInfo.legalIdentities[0], nodeInfo) + } + on { removeNode(any()) }.then { data.remove((it.arguments[0] as NodeInfo).legalIdentities[0]) } + on { getNodeByLegalIdentity(any()) }.then { data[it.arguments[0]] } + on { allNodeHashes }.then { data.values.map { it.serialize().hash } } + on { getNodeByHash(any()) }.then { mock -> data.values.single { it.serialize().hash == mock.arguments[0] } } + } + } + + private fun createNodeInfoAndSigned(org: String): Pair { + return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt b/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt deleted file mode 100644 index 975bd65bf4..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.node.services.network - -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.SignedData -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.NodeInfo -import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509CertificateFactory -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder -import java.security.KeyPair -import java.security.cert.CertPath -import java.security.cert.Certificate -import java.security.cert.X509Certificate - -object TestNodeInfoFactory { - private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", organisation = "R3 LTD", locality = "London", country = "GB"), rootCAKey) - private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) - - fun createNodeInfo(organisation: String): SignedData { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.$organisation.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - return sign(keyPair, nodeInfo) - } - - fun sign(keyPair: KeyPair, t: T): SignedData { - // Create digital signature. - val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, t.serialize().bytes)) - return SignedData(t.serialize(), digitalSignature) - } - - private fun buildCertPath(vararg certificates: Certificate): CertPath { - return X509CertificateFactory().generateCertPath(*certificates) - } - - private fun X509CertificateHolder.toX509Certificate(): X509Certificate { - return X509CertificateFactory().generateCertificate(encoded.inputStream()) - } - -} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index 9d88561798..e704357863 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -7,10 +7,7 @@ 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.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.* import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -95,7 +92,7 @@ class NetworkMapServer(cacheTimeout: Duration, @Path("network-map") class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) { - private val nodeInfoMap = mutableMapOf>() + private val nodeInfoMap = mutableMapOf() private val parametersHash = serializedParameters.hash private val signedParameters = SignedData( serializedParameters, @@ -106,7 +103,7 @@ class NetworkMapServer(cacheTimeout: Duration, @Path("publish") @Consumes(MediaType.APPLICATION_OCTET_STREAM) fun publishNodeInfo(input: InputStream): Response { - val registrationData = input.readBytes().deserialize>() + val registrationData = input.readBytes().deserialize() val nodeInfo = registrationData.verified() val nodeInfoHash = nodeInfo.serialize().sha256() nodeInfoMap.put(nodeInfoHash, registrationData) @@ -116,7 +113,7 @@ class NetworkMapServer(cacheTimeout: Duration, @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) fun getNetworkMap(): Response { - val networkMap = NetworkMap(nodeInfoMap.keys.map { it }, parametersHash) + val networkMap = NetworkMap(nodeInfoMap.keys.toList(), parametersHash) val serializedNetworkMap = networkMap.serialize() val signature = Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedNetworkMap.bytes) val signedNetworkMap = SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(networkMapKeyAndCert.certificate.cert, signature)) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index f133a9557d..aa2dc08533 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -86,18 +86,29 @@ fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLC configureDevKeyAndTrustStores(legalName) } } + fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT val intermediate: CertificateAndKeyPair = DEV_CA val nodeCaName = party.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()) - val issuerKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) - val issuerCertificate = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediate.certificate, intermediate.keyPair, nodeCaName, issuerKeyPair.public, - nameConstraints = nameConstraints) + val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + intermediate.certificate, + intermediate.keyPair, + nodeCaName, + nodeCaKeyPair.public, + nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf())) - val certHolder = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCertificate, issuerKeyPair, party.name, party.owningKey) - val pathElements = listOf(certHolder, issuerCertificate, intermediate.certificate, trustRoot) + val identityCert = X509Utilities.createCertificate( + CertificateType.WELL_KNOWN_IDENTITY, + nodeCaCert, + nodeCaKeyPair, + party.name, + party.owningKey) + + val pathElements = listOf(identityCert, nodeCaCert, intermediate.certificate, trustRoot) val certPath = X509CertificateFactory().generateCertPath(pathElements.map(X509CertificateHolder::cert)) return PartyAndCertificate(certPath) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt new file mode 100644 index 0000000000..a367cff7e6 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -0,0 +1,55 @@ +package net.corda.testing.internal + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.sign +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.testing.getTestPartyAndCertificate +import java.security.PrivateKey + +class TestNodeInfoBuilder { + private val identitiesAndPrivateKeys = ArrayList>() + + fun addIdentity(name: CordaX500Name): Pair { + val identityKeyPair = Crypto.generateKeyPair() + val identity = getTestPartyAndCertificate(name, identityKeyPair.public) + return Pair(identity, identityKeyPair.private).also { + identitiesAndPrivateKeys += it + } + } + + fun build(serial: Long = 1): NodeInfo { + return NodeInfo( + listOf(NetworkHostAndPort("my.${identitiesAndPrivateKeys[0].first.party.name.organisation}.com", 1234)), + identitiesAndPrivateKeys.map { it.first }, + 1, + serial + ) + } + + fun buildWithSigned(serial: Long = 1): Pair { + val nodeInfo = build(serial) + val privateKeys = identitiesAndPrivateKeys.map { it.second } + return Pair(nodeInfo, nodeInfo.signWith(privateKeys)) + } + + fun reset() { + identitiesAndPrivateKeys.clear() + } +} + +fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1): Pair { + val nodeInfoBuilder = TestNodeInfoBuilder() + names.forEach { nodeInfoBuilder.addIdentity(it) } + return nodeInfoBuilder.buildWithSigned(serial) +} + +fun NodeInfo.signWith(keys: List): SignedNodeInfo { + val serialized = serialize() + val signatures = keys.map { it.sign(serialized.bytes) } + return SignedNodeInfo(serialized, signatures) +}