From a2d14bd20d99960fd69b4ed37aa9534840e7410f Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 21 Dec 2017 11:06:44 +0000 Subject: [PATCH 01/18] Exclude jolokia packages from Quasar instrumentation. (#2279) --- experimental/quasar-hook/README.md | 2 +- node/capsule/build.gradle | 2 +- .../kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/quasar-hook/README.md b/experimental/quasar-hook/README.md index b0b1de0c99..829c26979c 100644 --- a/experimental/quasar-hook/README.md +++ b/experimental/quasar-hook/README.md @@ -26,7 +26,7 @@ additional classes are used when the jar is invoked directly. To do this we'll u ./gradlew experimental:quasar-hook:jar ./gradlew samples:irs-demo:deployNodes cd samples/irs-demo/build/nodes/NotaryService -java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix -jar corda.jar +java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix,org.jolokia -jar corda.jar ``` Once the node is started just exit the node. diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index a9bb2bbbea..de359edc74 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -44,7 +44,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationVersion = corda_release_version appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"] // See experimental/quasar-hook/README.md for how to generate. - def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" + def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] systemProperties['visualvm.display.name'] = 'Corda' minJavaVersion = '1.8.0' diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 700d37b7f2..628d174600 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -647,7 +647,7 @@ class DriverDSLImpl( "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" + "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + - "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;)" val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath=$excludePattern" val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" } From ff8d0881b3e72b8a7607544ecf155292c127fbe1 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 21 Dec 2017 11:22:32 +0000 Subject: [PATCH 02/18] Improves CorDapp dependency docs. --- docs/source/cordapp-build-systems.rst | 37 +++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index f2561429c0..5cba71b9af 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -28,18 +28,16 @@ Setting your dependencies Choosing your Corda version ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The following two lines of the ``build.gradle`` file define the Corda version used to build your CorDapp: +``ext.corda_release_version`` and ``ext.corda_gradle_plugins_version`` are used in the ``build.gradle`` to define the +versions of Corda and the Corda Gradle Plugins that are used to build your CorDapp. + +For example, to use version 1.0 of Corda and version 1.0 of the Corda gradle plugins, you'd write: .. sourcecode:: groovy ext.corda_release_version = '1.0.0' ext.corda_gradle_plugins_version = '1.0.0' -In this case, our CorDapp will use: - -* Version 1.0 of Corda -* Version 1.0 of the Corda gradle plugins - You can find the latest published version of both here: https://bintray.com/r3/corda. ``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and @@ -50,25 +48,32 @@ In certain cases, you may also wish to build against the unstable Master branch. Corda dependencies ^^^^^^^^^^^^^^^^^^ -The ``cordformation`` plugin adds: +The ``cordformation`` plugin adds two new gradle configurations: -* ``cordaCompile`` as a new configuration that ``compile`` extends from -* ``cordaRuntime`` which ``runtime`` extends from +* ``cordaCompile``, which extends ``compile`` +* ``cordaRuntime``, which extends ``runtime`` -To build against Corda you must add the following to your ``build.gradle`` file; +To build against Corda, you must add the following to your ``build.gradle`` file: -* The ``net.corda:corda:`` JAR as a ``cordaRuntime`` dependency -* Each compile dependency (eg ``corda-core``) as a ``cordaCompile`` dependency +* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency +* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency -To use Corda's test facilities you must add ``net.corda:corda-test-utils:`` as a ``testCompile`` dependency -(i.e. a default Java/Kotlin test compile task). +You may also want to add: + +* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test + frameworks +* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's + built-in development webserver .. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency. Dependencies on other CorDapps ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Sometimes, a CorDapp you build will depend on states, contracts or flows defined in another CorDapp. You must include -the CorDapp your CorDapp depends upon as a ``cordapp`` dependency in your ``build.gradle`` file. +You CorDapp may also depend on classes defined in another CorDapp, such as states, contracts and flows. There are two +ways to add another CorDapp as a dependency in your CorDapp's ``build.gradle`` file: + +* ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project) +* ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise) Other dependencies ^^^^^^^^^^^^^^^^^^ From 6db04907503d12bff6f7b9039f6debfe0cc8e35a Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Thu, 21 Dec 2017 11:48:00 +0000 Subject: [PATCH 03/18] Fix node integration tests. (#2233) * Add tests for node registration and communication --- .../net/corda/docs/tutorial/twoparty/flow.kt | 1 - .../node/services/network/NetworkMapTest.kt | 15 +++-- .../registration/NodeRegistrationTest.kt | 64 ++++++++++++++----- .../services/network/NetworkMapClientTest.kt | 9 +-- .../kotlin/net/corda/testing/driver/Driver.kt | 3 +- .../testing/node/internal/DriverDSLImpl.kt | 41 ++++++++---- .../corda/testing/node/internal/RPCDriver.kt | 3 +- .../node/internal/network/NetworkMapServer.kt | 35 +++++++--- .../net/corda/verifier/VerifierDriver.kt | 3 +- 9 files changed, 121 insertions(+), 53 deletions(-) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt index 27f4705501..dc4825c1b1 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt @@ -10,7 +10,6 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.serialization.SerializationWhitelist import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker -import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function import javax.ws.rs.GET import javax.ws.rs.Path diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index a46ae6f378..3f9aade18c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -10,6 +10,7 @@ import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.testing.ALICE_NAME +import net.corda.testing.ROOT_CA import net.corda.testing.BOB_NAME import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.driver.NodeHandle @@ -49,20 +50,22 @@ class NetworkMapTest { @Test fun `node correctly downloads and saves network parameters file on startup`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, + initialiseSerialization = false, notarySpecs = emptyList()) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val networkParameters = alice.configuration.baseDirectory .list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() } .readAll() .deserialize>() .verified() - assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters) + assertEquals(networkMapServer.networkParameters, networkParameters) } } @Test fun `nodes can see each other using the http network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, + initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { val alice = startNode(providedName = ALICE_NAME) val bob = startNode(providedName = BOB_NAME) val notaryNode = defaultNotaryNode.get() @@ -77,7 +80,8 @@ class NetworkMapTest { @Test fun `nodes process network map add updates correctly when adding new node to network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, + initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { val alice = startNode(providedName = ALICE_NAME) val notaryNode = defaultNotaryNode.get() val aliceNode = alice.get() @@ -98,7 +102,8 @@ class NetworkMapTest { @Test fun `nodes process network map remove updates correctly`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) { + internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, + initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { val alice = startNode(providedName = ALICE_NAME) val bob = startNode(providedName = BOB_NAME) val notaryNode = defaultNotaryNode.get() diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 83526ba759..af1a44426c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -3,10 +3,12 @@ package net.corda.node.utilities.registration import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert +import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.toX509CertHolder -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.minutes +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.* +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory @@ -14,11 +16,16 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.network.NotaryInfo +import net.corda.testing.ROOT_CA import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.driver.PortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.network.NetworkMapServer +import net.corda.testing.singleIdentity +import net.corda.testing.singleIdentityAndCert import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -44,15 +51,13 @@ class NodeRegistrationTest { @JvmField val testSerialization = SerializationEnvironmentRule(true) private val portAllocation = PortAllocation.Incremental(13000) - private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate() - private val registrationHandler = RegistrationHandler(rootCertAndKeyPair) - + private val registrationHandler = RegistrationHandler(ROOT_CA) private lateinit var server: NetworkMapServer private lateinit var serverHostAndPort: NetworkHostAndPort @Before fun startServer() { - server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler) + server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), ROOT_CA, "localhost", registrationHandler) serverHostAndPort = server.start() } @@ -61,19 +66,47 @@ class NodeRegistrationTest { server.close() } - // TODO Ideally this test should be checking that two nodes that register are able to transact with each other. However - // starting a second node hangs so that needs to be fixed. @Test fun `node registration correct root cert`() { - val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert) + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = ROOT_CA.certificate.cert) internalDriver( portAllocation = portAllocation, - notarySpecs = emptyList(), compatibilityZone = compatibilityZone, - initialiseSerialization = false + initialiseSerialization = false, + notarySpecs = listOf(NotarySpec(CordaX500Name(organisation = "NotaryService", locality = "Zurich", country = "CH"), validating = false)), + extraCordappPackagesToScan = listOf("net.corda.finance"), + onNetworkParametersGeneration = { server.networkParameters = it } ) { - startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() - assertThat(registrationHandler.idsPolled).contains("Alice") + val notary = defaultNotaryNode.get() + + val ALICE_NAME = "Alice" + val GENEVIEVE_NAME = "Genevieve" + val nodesFutures = listOf(startNode(providedName = CordaX500Name(ALICE_NAME, "London", "GB")), + startNode(providedName = CordaX500Name(GENEVIEVE_NAME, "London", "GB"))) + + val (alice, genevieve) = nodesFutures.transpose().get() + val nodes = listOf(alice, genevieve, notary) + + assertThat(registrationHandler.idsPolled).contains(ALICE_NAME, GENEVIEVE_NAME) + // Notary identities are generated beforehand hence notary nodes don't go through registration. + // This test isn't specifically testing this, or relying on this behavior, though if this check fail, + // this will probably lead to the rest of the test to fail. + assertThat(registrationHandler.idsPolled).doesNotContain("NotaryService") + + // Check each node has each other identity in their network map cache. + val nodeIdentities = nodes.map { it.nodeInfo.singleIdentity() } + for (node in nodes) { + assertThat(node.rpc.networkMapSnapshot().map { it.singleIdentity() }).containsAll(nodeIdentities) + } + + // Check we nodes communicate among themselves (and the notary). + val anonymous = false + genevieve.rpc.startFlow(::CashIssueAndPaymentFlow, 1000.DOLLARS, OpaqueBytes.of(12), + alice.nodeInfo.singleIdentity(), + anonymous, + notary.nodeInfo.singleIdentity()) + .returnValue + .getOrThrow() } } @@ -85,6 +118,7 @@ class NodeRegistrationTest { portAllocation = portAllocation, notarySpecs = emptyList(), compatibilityZone = compatibilityZone, + initialiseSerialization = false, // Changing the content of the truststore makes the node fail in a number of ways if started out process. startNodesInProcess = true ) { 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 608f583f89..30819a16ef 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,7 @@ 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.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.* import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.internal.network.NetworkMapServer @@ -33,7 +30,7 @@ class NetworkMapClientTest { @Before fun setUp() { - server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort()) + server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort(), root_ca = ROOT_CA) val hostAndPort = server.start() networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_TRUST_ROOT.cert) } @@ -70,7 +67,7 @@ class NetworkMapClientTest { // The test server returns same network parameter for any hash. val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified() assertNotNull(networkParameter) - assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter) + assertEquals(server.networkParameters, networkParameter) } @Test 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 45cd567f90..cdcf9d966d 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 @@ -193,7 +193,8 @@ fun driver( notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, - compatibilityZone = null + compatibilityZone = null, + onNetworkParametersGeneration = { } ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 628d174600..c4c36fa8c7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -10,13 +10,12 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf +import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.ThreadBox +import net.corda.core.identity.Party +import net.corda.core.internal.* import net.corda.core.internal.concurrent.* -import net.corda.core.internal.copyTo -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.NetworkMapCache import net.corda.core.toFuture @@ -32,6 +31,7 @@ import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -46,6 +46,8 @@ import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME import net.corda.testing.DUMMY_BANK_A_NAME +import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.* import net.corda.testing.node.ClusterSpec @@ -89,7 +91,8 @@ class DriverDSLImpl( extraCordappPackagesToScan: List, val jmxPolicy: JmxPolicy, val notarySpecs: List, - val compatibilityZone: CompatibilityZoneParams? + val compatibilityZone: CompatibilityZoneParams?, + val onNetworkParametersGeneration: (NetworkParameters) -> Unit ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! @@ -104,7 +107,8 @@ class DriverDSLImpl( private val countObservables = mutableMapOf>() private lateinit var _notaries: List override val notaryHandles: List get() = _notaries - private var networkParameters: NetworkParametersCopier? = null + private var networkParametersCopier: NetworkParametersCopier? = null + lateinit var networkParameters: NetworkParameters class State { val processes = ArrayList() @@ -179,8 +183,8 @@ class DriverDSLImpl( val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") - - val registrationFuture = if (compatibilityZone?.rootCert != null) { + val isNotary = name in notarySpecs.map { it.name } + val registrationFuture = if (compatibilityZone?.rootCert != null && !isNotary) { nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url) } else { doneFuture(Unit) @@ -279,7 +283,9 @@ class DriverDSLImpl( notaryInfos += NotaryInfo(identity, type.validating) } - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + networkParameters = testNetworkParameters(notaryInfos) + onNetworkParametersGeneration(networkParameters) + networkParametersCopier = NetworkParametersCopier(networkParameters) return cordforms.map { val startedNode = startCordformNode(it) @@ -347,8 +353,10 @@ class DriverDSLImpl( } } val notaryInfos = generateNotaryIdentities() + networkParameters = testNetworkParameters(notaryInfos) + onNetworkParametersGeneration(networkParameters) // The network parameters must be serialised before starting any of the nodes - if (compatibilityZone == null) networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + networkParametersCopier = NetworkParametersCopier(networkParameters) val nodeHandles = startNotaries() _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } } @@ -504,7 +512,11 @@ class DriverDSLImpl( val configuration = config.parseAsNodeConfiguration() val baseDirectory = configuration.baseDirectory.createDirectories() nodeInfoFilesCopier?.addConfig(baseDirectory) - networkParameters?.install(baseDirectory) + if (configuration.myLegalName in networkParameters.notaries.map { it.identity.name } || compatibilityZone == null) { + // If a notary is being started, then its identity appears in networkParameters (generated by Driver), + // and Driver itself must pass the network parameters to the notary. + networkParametersCopier?.install(baseDirectory) + } val onNodeExit: () -> Unit = { nodeInfoFilesCopier?.removeConfig(baseDirectory) countObservables.remove(configuration.myLegalName) @@ -817,7 +829,8 @@ fun genericDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, notarySpecs = notarySpecs, - compatibilityZone = null + compatibilityZone = null, + onNetworkParametersGeneration = {} ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -855,6 +868,7 @@ fun internalDriver( extraCordappPackagesToScan: List = DriverParameters().extraCordappPackagesToScan, jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, compatibilityZone: CompatibilityZoneParams? = null, + onNetworkParametersGeneration: (NetworkParameters) -> Unit = {}, dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -870,7 +884,8 @@ fun internalDriver( notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, - compatibilityZone = compatibilityZone + compatibilityZone = compatibilityZone, + onNetworkParametersGeneration = onNetworkParametersGeneration ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index a5dff915ca..4b761f0b05 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -121,7 +121,8 @@ fun rpcDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, - compatibilityZone = null + compatibilityZone = null, + onNetworkParametersGeneration = {} ), externalTrace ), coerce = { it }, 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 cf7cb48fa0..995817c74e 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 @@ -1,9 +1,15 @@ package net.corda.testing.node.internal.network +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.identity.CordaX500Name import net.corda.core.crypto.* import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort @@ -16,7 +22,6 @@ 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 @@ -37,10 +42,10 @@ import javax.ws.rs.core.Response.ok class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing. + private val myHostNameValue: String = "test.host.name", vararg additionalServices: Any) : Closeable { companion object { - val stubNetworkParameter = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10) - private val serializedParameters = stubNetworkParameter.serialize() + private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10) private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair { val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -48,15 +53,25 @@ class NetworkMapServer(cacheTimeout: Duration, CertificateType.INTERMEDIATE_CA, rootCAKeyAndCert.certificate, rootCAKeyAndCert.keyPair, - X500Name("CN=Corda Network Map,L=London"), + CordaX500Name("Corda Network Map", "R3 Ltd", "London","GB"), networkMapKey.public).cert + // Check that the certificate validates. Nodes will perform this check upon receiving a network map, + // it's better to fail here than there. + X509Utilities.validateCertificateChain(rootCAKeyAndCert.certificate.cert, networkMapCert) return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey) } } private val server: Server + var networkParameters: NetworkParameters = stubNetworkParameters + set(networkParameters) { + check(field == stubNetworkParameters) { "Network parameters can be set only once" } + field = networkParameters + } + private val serializedParameters get() = networkParameters.serialize() private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca)) + init { server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { handler = HandlerCollection().apply { @@ -95,13 +110,13 @@ class NetworkMapServer(cacheTimeout: Duration, } @Path("network-map") - class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) { + inner class InMemoryNetworkMapService(private val cacheTimeout: Duration, + private val networkMapKeyAndCert: CertificateAndKeyPair) { private val nodeInfoMap = mutableMapOf() - private val parametersHash = serializedParameters.hash - private val signedParameters = SignedData( + private val parametersHash by lazy { serializedParameters.hash } + private val signedParameters by lazy { SignedData( serializedParameters, - DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes)) - ) + DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))) } @POST @Path("publish") @@ -151,7 +166,7 @@ class NetworkMapServer(cacheTimeout: Duration, @GET @Path("my-hostname") fun getHostName(): Response { - return Response.ok("test.host.name").build() + return Response.ok(myHostNameValue).build() } } } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 791df21211..32013bfc32 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -76,7 +76,8 @@ fun verifierDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, - compatibilityZone = null + compatibilityZone = null, + onNetworkParametersGeneration = { } ) ), coerce = { it }, From 7f1b61d34b242536d3753d565094343611736bf5 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 21 Dec 2017 14:42:18 +0000 Subject: [PATCH 04/18] Adds a caveat regarding the inability to record a transaction twice. --- docs/source/tutorial-observer-nodes.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/source/tutorial-observer-nodes.rst b/docs/source/tutorial-observer-nodes.rst index ecb5f04089..52c7a5e67c 100644 --- a/docs/source/tutorial-observer-nodes.rst +++ b/docs/source/tutorial-observer-nodes.rst @@ -35,8 +35,14 @@ the transaction to the regulator. There are two important aspects to note here: If the states define a relational mapping (see :doc:`api-persistence`) then the regulator will be able to query the reports from their database and observe new transactions coming in via RPC. -.. warning:: Nodes which act as both observers and which directly take part in the ledger are not supported at this - time. In particular, coin selection may return states which you do not have the private keys to be able to sign - for. Future versions of Corda may address this issue, but for now, if you wish to both participate in the ledger - and also observe transactions that you can't sign for you will need to run two nodes and have two separate - identities. \ No newline at end of file +Caveats +------- + +* Nodes which act as both observers and direct participants in the ledger are not supported at this time. In + particular, coin selection may return states which you do not have the private keys to be able to sign for. Future + versions of Corda may address this issue, but for now, if you wish to both participate in the ledger and also observe + transactions that you can't sign for you will need to run two nodes and have two separate identities + +* Nodes only record each transaction once. If a node has already recorded a transaction in non-observer mode, it cannot + later re-record the same transaction as an observer. This issue is tracked here: + https://r3-cev.atlassian.net/browse/CORDA-883 \ No newline at end of file From 693cac2a1112052389377b950152a242d91d8237 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Thu, 21 Dec 2017 15:59:01 +0000 Subject: [PATCH 05/18] Fix debug option in Cordformation (#2276) --- .../src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index c49bd847f7..bb308a8191 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -90,8 +90,12 @@ private abstract class JavaCommand( add(getJavaPath()) addAll(jvmArgs) add("-Dname=$nodeName") - null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") - null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort") + val jvmArgs: MutableList = mutableListOf() + null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") + null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort") + if (jvmArgs.isNotEmpty()) { + add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}") + } add("-jar") add(jarName) init() From 5a50f564c958c5acaf39f0f0b78a4c550bd53d7d Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 21 Dec 2017 16:57:03 +0000 Subject: [PATCH 06/18] Improvements to the shell docs. --- docs/source/shell.rst | 293 +++++++++++++++++++++++++++--------------- 1 file changed, 186 insertions(+), 107 deletions(-) diff --git a/docs/source/shell.rst b/docs/source/shell.rst index d993c5e3bf..86833f002b 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -7,25 +7,32 @@ Shell ===== -The Corda shell is an embedded command line that allows an administrator to control and monitor the node. -Some of its features include: +.. contents:: -* Invoking any of the RPCs the node exposes to applications. -* Starting flows. -* View a dashboard of threads, heap usage, VM properties. -* Uploading and downloading zips from the attachment store. -* Issue SQL queries to the underlying database. -* View JMX metrics and monitoring exports. -* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data. +The Corda shell is an embedded command line that allows an administrator to control and monitor a node. It is based on +the `CRaSH`_ shell and supports many of the same features. These features include: -It is based on the popular `CRaSH`_ shell used in various other projects and supports many of the same features. +* Invoking any of the node's RPC methods +* Viewing a dashboard of threads, heap usage, VM properties +* Uploading and downloading attachments +* Issuing SQL queries to the underlying database +* Viewing JMX metrics and monitoring exports +* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data -Local terminal shell runs only in development mode. It may be disabled by passing the ``--no-local-shell`` flag to the node. +The shell via the local terminal +-------------------------------- -SSH server ----------- +In development mode, the shell will display in the node's terminal window. It may be disabled by passing the +``--no-local-shell`` flag when running the node. -Shell can also be accessible via SSH. By default SSH server is *disabled*. To enable it port must be configured - in ``node.conf`` file +The shell via SSH +----------------- +The shell is also accessible via SSH. + +Enabling SSH access +******************* + +By default, the SSH server is *disabled*. To enable it, a port must be configured in the node's ``node.conf`` file: .. code:: bash @@ -33,80 +40,159 @@ Shell can also be accessible via SSH. By default SSH server is *disabled*. To en port = 2222 } -Authentication and authorization --------------------------------- -SSH requires users to login first - using the same users as RPC system. In fact, the shell serves as a proxy to RPC and communicates -with the node using RPC calls. This also means that RPC permissions are enforced. No permissions are required to allow the connection -and log in. -Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` while starting flows requires -``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows`` in addition to a permission for a particular flow. +Authentication +************** +Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates +with the node using RPC calls. No RPC permissions are required to allow the connection and log in. -Host key --------- +The host key is loaded from the ``/sshkey/hostkey.pem`` file. If this file does not exist, it is +generated automatically. In development mode, the seed may be specified to give the same results on the same computer +in order to avoid host-checking errors. -The host key is loaded from ``sshkey/hostkey.pem`` file. If the file does not exist, it will be generated randomly, however -in the development mode seed may be tuned to give the same results on the same computer - in order to avoid host checking -errors. +Connecting to the shell +*********************** -Connecting ----------- +Linux and MacOS +^^^^^^^^^^^^^^^ -Linux and MacOS computers usually come with SSH client preinstalled. On Windows it usually requires extra download. -Usual connection syntax is ``ssh user@host -p 2222`` - where ``user`` is a RPC username, and ``-p`` specifies a port parameters - -it's the same as setup in ``node.conf`` file. ``host`` should point to a node hostname, usually ``localhost`` if connecting and -running node on the same computer. Password will be asked after establishing connection. +Run the following command from the terminal: -:note: While developing, checking multiple samples or simply restarting a node frequently host key may be regenerated. SSH usually - saved once trusted hosts and will refuse to connect in case of a change. Then check may be disabled with extra options - ``ssh -o StrictHostKeyChecking=no user@host -p2222``. This option should never be used in production environment! +.. code:: bash -Getting help ------------- + ssh -p [portNumber] [host] -l [user] -You can run ``help`` to list the available commands. +Where: -The shell has a ``man`` command that can be used to get interactive help on many commands. You can also use the -``--help`` or ``-h`` flags to a command to get info about what switches it supports. +* ``[portNumber]`` is the port number specified in the ``node.conf`` file +* ``[host]`` is the node's host (e.g. ``localhost`` if running the node locally) +* ``[user]`` is the RPC username -Commands may have subcommands, in the same style as ``git``. In that case running the command by itself will -list the supported subcommands. +The RPC password will be requested after a connection is established. -Starting flows and performing remote method calls -------------------------------------------------- +:note: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves + trusted hosts and will refuse to connect in case of a change. This check can be disabled using the + ``-o StrictHostKeyChecking=no`` flag. This option should never be used in production environment! -**Flows** are the way the ledger is changed. If you aren't familiar with them, please review ":doc:`flow-state-machines`" -first. The ``flow list`` command can be used to list the flows understood by the node, ``flow watch`` shows all the flows -currently running on the node with the result (or error) information in a user friendly way, ``flow start`` can be -used to start flows. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and -then the data to be passed to the flow constructor. The unambiguous substring feature is helpful for reducing -the needed typing. If the match is ambiguous the possible matches will be printed out. If a flow has multiple -constructors then the names and types of the arguments will be used to try and determine which to use automatically. -If the match against available constructors is unclear, the reasons each available constructor failed to match -will be printed out. In the case of an ambiguous match, the first applicable will be used. +Windows +^^^^^^^ -**RPCs** (remote procedure calls) are commands that can be sent to the node to query it, control it and manage it. -RPCs don't typically do anything that changes the global ledger, but they may change node-specific data in the -database. Each RPC is one method on the ``CordaRPCOps`` interface, and may return a stream of events that will -be shown on screen until you press Ctrl-C. You perform an RPC by using ``run`` followed by the name. +Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used. -.. raw:: html +Permissions +*********** -
Documentation of available RPCs

+When accessing the shell via SSH, some additional RPC permissions are required: -Whichever form of change is used, there is a need to provide *parameters* to either the RPC or the flow -constructor. Because parameters can be any arbitrary Java object graph, we need a convenient syntax to express -this sort of data. The shell uses a syntax called `Yaml`_ to do this. +* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` +* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows``, as well as a + permission for the flow being started -Data syntax ------------ +Interacting with the node via the shell +--------------------------------------- -Yaml (yet another markup language) is a simple JSON-like way to describe object graphs. It has several features -that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can -often skip the quotes around strings. Here is an example of how this syntax is used: +The shell interacts with the node by issuing RPCs (remote procedure calls). You make an RPC from the shell by typing +``run`` followed by the name of the desired RPC method. For example, you'd see a list of the registered flows on your +node by running: + +``run registeredFlows`` + +Some RPCs return a stream of events that will be shown on screen until you press Ctrl-C. + +You can find a list of the available RPC methods +`here `_. + +Flow commands +************* + +The shell also has special commands for working with flows: + +* ``flow list`` lists the flows available on the node +* ``flow watch`` shows all the flows currently running on the node with result (or error) information +* ``flow start`` starts a flow. The ``flow start`` command takes the name of a flow class, or + *any unambiguous substring* thereof, as well as the data to be passed to the flow constructor. If there are several + matches for a given substring, the possible matches will be printed out. If a flow has multiple constructors then the + names and types of the arguments will be used to try and automatically determine which one to use. If the match + against available constructors is unclear, the reasons each available constructor failed to match will be printed + out. In the case of an ambiguous match, the first applicable constructor will be used + +Parameter syntax +**************** + +Parameters are passed to RPC or flow commands using a syntax called `Yaml`_ (yet another markup language), a +simple JSON-like language. The key features of Yaml are: + +* Parameters are separated by commas +* Each parameter is specified as a ``key: value`` pair + + * There **MUST** to be a space after the colon, otherwise you'll get a syntax error + +* Strings do not need to be surrounded by quotes unless they contain commas, colons or embedded quotes +* Class names must be fully-qualified (e.g. ``java.lang.String``) + +.. note:: If your CorDapp is written in Java, named arguments won't work unless you compiled the node using the + ``-parameters`` argument to javac. See :doc:`generating-a-node` for how to specify it via Gradle. + +Creating an instance of a class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Class instances are created using curly-bracket syntax. For example, if we have a ``Campaign`` class with the following +constructor: + +``data class Campaign(val name: String, val target: Int)`` + +Then we could create an instance of this class to pass as a parameter as follows: + +``newCampaign: { name: Roger, target: 1000 }`` + +Where ``newCampaign`` is a parameter of type ``Campaign``. + +Mappings from strings to types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Several parameter types can automatically be mapped from strings. See the `defined parsers`_ for more information. We +cover the most common types here. + +Amount +~~~~~~ +A parameter of type ``Amount`` can be written as either: + +* A dollar ($), pound (£) or euro (€) symbol followed by the amount as a decimal +* The amount as a decimal followed by the ISO currency code (e.g. "100.12 CHF") + +OpaqueBytes +~~~~~~~~~~~ +A parameter of type ``OpaqueBytes`` can be provided as a string, which will be automatically converted to +``OpaqueBytes``. + +Party +~~~~~ +A parameter of type ``Party`` can be written in several ways: + +* By using the node's full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"`` +* By specifying the organisation name only: ``"Monogram Bank"`` +* By specifying any other non-ambiguous part of the name: ``"Sao Paulo"`` (if only one network node is located in Sao + Paulo) + +Instant +~~~~~~~ +A parameter of type ``Instant`` can be written as follows: ``"2017-12-22T00:00:00Z"``. + +Examples +^^^^^^^^ + +Starting a flow +~~~~~~~~~~~~~~~ + +We would start the ``CashIssue`` flow as follows: ``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"`` -This invokes a constructor of a flow with the following prototype in the code: +This breaks down as follows: + +* ``flow start`` is a shell command for starting a flow +* ``CashIssue`` is the flow we want to start +* Each ``name: value`` pair after that is a flow constructor argument + +This command invokes the following ``CashIssue`` constructor: .. container:: codeset @@ -117,50 +203,44 @@ This invokes a constructor of a flow with the following prototype in the code: val recipient: Party, val notary: Party) : AbstractCashFlow(progressTracker) -Here, everything after ``CashIssue`` is specifying the arguments to the constructor of a flow. In Yaml, an object -is specified as a set of ``key: value`` pairs and in our form, we separate them by commas. There are a few things -to note about this syntax: +Querying the vault +~~~~~~~~~~~~~~~~~~ -* When a parameter is of type ``Amount`` you can write it as either one of the dollar symbol ($), - pound (£), euro (€) followed by the amount as a decimal, or as the value followed by the ISO currency code - e.g. "100.12 CHF" -* ``OpaqueBytes`` is filled with the contents of whatever is provided as a string. -* ``Party`` objects are looked up by name. -* Strings do not need to be surrounded by quotes unless they contain a comma or embedded quotes. This makes it - a lot more convenient to type such strings. +We would query the vault for ``IOUState`` states as follows: -Other types also have sensible mappings from strings. See `the defined parsers`_ for more information. +``run vaultQuery contractStateType: com.template.IOUState`` -Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This is helpful when no particular -parser is defined for the type you need, for instance, if an API requires a ``Pair`` -which could be represented as ``{ first: foo, second: 123 }``. +This breaks down as follows: -.. note:: If your CorDapp is written in Java, - named arguments won't work unless you compiled using the ``-parameters`` argument to javac. - See :doc:`generating-a-node` for how to specify it via Gradle. - -The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this: - -``run registeredFlows`` +* ``run`` is a shell command for making an RPC call +* ``vaultQuery`` is the RPC call we want to make +* ``contractStateType: com.template.IOUState`` is the fully-qualified name of the state type we are querying for Attachments ------------ +*********** -The shell can be used to upload and download attachments from the node interactively. To learn more, see -the tutorial ":doc:`tutorial-attachments`". +The shell can be used to upload and download attachments from the node. To learn more, see the tutorial +":doc:`tutorial-attachments`". + +Getting help +************ + +You can type ``help`` in the shell to list the available commands, and ``man`` to get interactive help on many +commands. You can also pass the ``--help`` or ``-h`` flags to a command to get info about what switches it supports. + +Commands may have subcommands, in the same style as ``git``. In that case, running the command by itself will +list the supported subcommands. Extending the shell ------------------- -The shell can be extended using commands written in either Java or `Groovy`_ (Groovy is a scripting language that -is Java compatible). Such commands have full access to the node internal APIs and thus can be used to achieve -almost anything. +The shell can be extended using commands written in either Java or `Groovy`_ (a Java-compatible scripting language). +These commands have full access to the node's internal APIs and thus can be used to achieve almost anything. -A full tutorial on how to write such commands is out of scope for this documentation, to learn more please -refer to the `CRaSH`_ documentation. New commands can be placed in the ``shell-commands`` subdirectory in the -node directory. Edits to existing commands will be used automatically, but at this time commands added after the -node has started won't be automatically detected. Commands should be named in all lower case with either a -``.java`` or ``.groovy`` extension. +A full tutorial on how to write such commands is out of scope for this documentation. To learn more, please refer to +the `CRaSH`_ documentation. New commands are placed in the ``shell-commands`` subdirectory in the node directory. Edits +to existing commands will be used automatically, but currently commands added after the node has started won't be +automatically detected. Commands must have names all in lower-case with either a ``.java`` or ``.groovy`` extension. .. warning:: Commands written in Groovy ignore Java security checks, so have unrestricted access to node and JVM internals regardless of any sandboxing that may be in place. Don't allow untrusted users to edit files in the @@ -171,14 +251,13 @@ Limitations The shell will be enhanced over time. The currently known limitations include: -* SSH access is currently not available. -* There is no command completion for flows or RPCs. -* Command history is not preserved across restarts. -* The ``jdbc`` command requires you to explicitly log into the database first. -* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted. -* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using. +* There is no command completion for flows or RPCs +* Command history is not preserved across restarts +* The ``jdbc`` command requires you to explicitly log into the database first +* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted +* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using .. _Yaml: http://www.yaml.org/spec/1.2/spec.html -.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html +.. _defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html .. _Groovy: http://groovy-lang.org/ .. _CRaSH: http://www.crashub.org/ From ce4a6408352b3a71ad51e02d169bcef15deff613 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 22 Dec 2017 15:15:21 +0000 Subject: [PATCH 07/18] CORDA-886: Removed the 60s timeout when generating node-info files in the network bootstrapper. Instead a warning is given. (#2290) --- .../internal/network/NetworkBootstrapper.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 34ee05851a..a6d6e3df03 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -4,12 +4,15 @@ import com.typesafe.config.ConfigFactory import net.corda.cordform.CordformNode import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.concurrent.fork import net.corda.core.node.NodeInfo import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl @@ -20,7 +23,8 @@ import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.time.Instant -import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.Executors +import java.util.concurrent.TimeoutException import kotlin.streams.toList /** @@ -54,7 +58,7 @@ class NetworkBootstrapper { val processes = startNodeInfoGeneration(nodeDirs) initialiseSerialization() try { - println("Waiting for all nodes to generate their node-info files") + println("Waiting for all nodes to generate their node-info files...") val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs) println("Distributing all node info-files to all nodes") distributeNodeInfos(nodeDirs, nodeInfoFiles) @@ -82,15 +86,22 @@ class NetworkBootstrapper { } private fun gatherNodeInfoFiles(processes: List, nodeDirs: List): List { - val timeOutInSeconds = 60L - return processes.zip(nodeDirs).map { (process, nodeDir) -> - check(process.waitFor(timeOutInSeconds, SECONDS)) { - "Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" + val executor = Executors.newSingleThreadExecutor() + + val future = executor.fork { + processes.zip(nodeDirs).map { (process, nodeDir) -> + check(process.waitFor() == 0) { + "Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" + } + nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() } } - check(process.exitValue() == 0) { - "Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" - } - nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() } + } + + return try { + future.getOrThrow(60.seconds) + } catch (e: TimeoutException) { + println("...still waiting. If this is taking longer than usual, check the node logs.") + future.getOrThrow() } } From 1d66fe9296dd3e4f5976b312202208e85bd0b00c Mon Sep 17 00:00:00 2001 From: Anthony Keenan <34482776+anthonykr3@users.noreply.github.com> Date: Sat, 23 Dec 2017 11:22:31 +0000 Subject: [PATCH 08/18] [CORDA-879] Generate node directories as part of bootstrapping (#2285) * Generate node directories as part of bootstrapping * Include latest corda.jar in bootstrapper package Remove SLF4J warnings on startup * Changes post review * More review changes * Review changes * Making docs clearer --- docs/source/setting-up-a-corda-network.rst | 13 +++++++- .../internal/network/NetworkBootstrapper.kt | 30 +++++++++++++++++-- tools/bootstrapper/build.gradle | 8 ++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 4ae763dce2..1f1e65a966 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -67,10 +67,21 @@ The bootstrapper tool can be built with the command: The resulting jar can be found in ``tools/bootstrapper/build/libs/``. -To use it, run the following command, specifying the root directory which hosts all the node directories as the argument: +To use it, create a directory containing a ``node.conf`` file for each node you want to create. Then run the following command: ``java -jar network-bootstrapper.jar `` +For example running the command on a directory containing these files : + +.. sourcecode:: none + + . + ├── notary.conf // The notary's node.conf file + ├── partya.conf // Party A's node.conf file + └── partyb.conf // Party B's node.conf file + +Would generate directories containing three nodes: notary, partya and partyb. + Starting the nodes ~~~~~~~~~~~~~~~~~~ diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index a6d6e3df03..af625adae5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -19,6 +19,7 @@ import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 +import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption @@ -52,6 +53,7 @@ class NetworkBootstrapper { fun bootstrap(directory: Path) { directory.createDirectories() println("Bootstrapping local network in $directory") + generateDirectoriesIfNeeded(directory) val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() } require(nodeDirs.isNotEmpty()) { "No nodes found" } println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}") @@ -73,6 +75,27 @@ class NetworkBootstrapper { } } + private fun generateDirectoriesIfNeeded(directory: Path) { + val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() } + if (confFiles.isEmpty()) return + println("Node config files found in the root directory - generating node directories") + val cordaJar = extractCordaJarTo(directory) + for (confFile in confFiles) { + val nodeName = confFile.fileName.toString().removeSuffix(".conf") + println("Generating directory for $nodeName") + val nodeDir = (directory / nodeName).createDirectory() + confFile.moveTo(nodeDir / "node.conf") + Files.copy(cordaJar, (nodeDir / "corda.jar")) + } + Files.delete(cordaJar) + } + + private fun extractCordaJarTo(directory: Path): Path { + val cordaJarPath = (directory / "corda.jar") + Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath) + return cordaJarPath + } + private fun startNodeInfoGeneration(nodeDirs: List): List { return nodeDirs.map { nodeDir -> val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories() @@ -147,10 +170,10 @@ class NetworkBootstrapper { private fun NodeInfo.notaryIdentity(): Party { return when (legalIdentities.size) { - // Single node notaries have just one identity like all other nodes. This identity is the notary identity + // Single node notaries have just one identity like all other nodes. This identity is the notary identity 1 -> legalIdentities[0] - // Nodes which are part of a distributed notary have a second identity which is the composite identity of the - // cluster and is shared by all the other members. This is the notary identity. + // Nodes which are part of a distributed notary have a second identity which is the composite identity of the + // cluster and is shared by all the other members. This is the notary identity. 2 -> legalIdentities[1] else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") } @@ -172,6 +195,7 @@ class NetworkBootstrapper { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P } + override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() } diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle index dc578b2c76..74a2c961d8 100644 --- a/tools/bootstrapper/build.gradle +++ b/tools/bootstrapper/build.gradle @@ -6,7 +6,10 @@ configurations { runtimeArtifacts } -// TODO Fix SLF4J warnings that occur when running the bootstrapper +dependencies { + compile "org.slf4j:slf4j-nop:$slf4j_version" +} + task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) { applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper' archiveName "network-bootstrapper.jar" @@ -16,6 +19,9 @@ task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').comp minJavaVersion = '1.8.0' jvmArgs = ['-XX:+UseG1GC'] } + from(project(':node:capsule').tasks['buildCordaJAR']) { + rename 'corda-(.*)', 'corda.jar' + } applicationSource = files( project(':node-api').configurations.runtime, project(':node-api').jar From f8f5639b38f63402cc3b4a205c3d859b86c06fd4 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Thu, 28 Dec 2017 09:12:06 +0000 Subject: [PATCH 09/18] More descriptive error message (#2295) --- .../src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 2a08170192..3e91639000 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -40,7 +40,8 @@ class SellerFlow(private val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) - val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first() + val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java) + .states.firstOrNull() ?: throw IllegalStateException("No commercial paper found. Please check if you issued the papers first, follow the README for instructions.") progressTracker.currentStep = TRADING From 5ce873a6e491e9b08072c6d6e60e59490fc04e54 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Thu, 28 Dec 2017 09:13:57 +0000 Subject: [PATCH 10/18] Removed unparseable % sign (#2294) (cherry picked from commit f05de54) --- .../web/src/main/resources/static/js/controllers/CreateDeal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/irs-demo/web/src/main/resources/static/js/controllers/CreateDeal.js b/samples/irs-demo/web/src/main/resources/static/js/controllers/CreateDeal.js index 41b30a6172..15b7af2381 100644 --- a/samples/irs-demo/web/src/main/resources/static/js/controllers/CreateDeal.js +++ b/samples/irs-demo/web/src/main/resources/static/js/controllers/CreateDeal.js @@ -21,7 +21,7 @@ define([ $scope.formError = resp.data; }, handleHttpFail); }; - $('input.percent').mask("9.999999%", {placeholder: "", autoclear: false}); + $('input.percent').mask("9.999999", {placeholder: "", autoclear: false}); $('#swapirscolumns').click(() => { let first = $('#irscolumns .irscolumn:eq( 0 )'); let last = $('#irscolumns .irscolumn:eq( 1 )'); From cb0b31107700795fbcad870b98a44bc383c660f5 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Thu, 28 Dec 2017 14:56:27 +0100 Subject: [PATCH 11/18] CordaRPCJavaClientTest refactoring to align with enterprise repo. (#2299) * Align CordaRPCJavaClientTest with Kotlin version of the test ((byte)0 instead of "1".getBytes()) * Refactoring to align with enterprise repo (exceptions). --- .../java/net/corda/client/rpc/CordaRPCJavaClientTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index a3796af894..ac6272f579 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -55,7 +55,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { } @Before - public void setUp() throws ExecutionException, InterruptedException { + public void setUp() throws Exception { node = startNode(ALICE_NAME, 1, singletonList(rpcUser)); client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } @@ -71,11 +71,11 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { } @Test - public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { + public void testCashBalances() throws ExecutionException, InterruptedException { login(rpcUser.getUsername(), rpcUser.getPassword()); FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, - DOLLARS(123), OpaqueBytes.of("1".getBytes()), + DOLLARS(123), OpaqueBytes.of((byte)0), CoreTestUtils.chooseIdentity(node.getInfo())); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); From 39d25958e2fe6f22b3c2780275b182e8ecdf5fac Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 28 Dec 2017 15:32:09 +0000 Subject: [PATCH 12/18] Fixed identity generation of single node notaries as used by the driver and MockNetwork. (#2296) The identity cert generated used to be of type SERVICE_IDENTITY when it should have been a LEGAL_IDENTITY. --- .../nodeapi/internal/IdentityGenerator.kt | 83 ++++--- .../nodeapi/internal/KeyStoreConfigHelpers.kt | 47 ++++ .../internal/config/SSLConfiguration.kt | 1 + .../internal/crypto/KeyStoreWrapper.kt | 35 +-- .../internal/crypto/X509UtilitiesTest.kt | 205 ++++++------------ .../net/corda/node/internal/AbstractNode.kt | 6 +- .../node/services/config/ConfigUtilities.kt | 73 +------ .../registration/NetworkRegistrationHelper.kt | 1 + .../corda/notarydemo/RaftNotaryCordform.kt | 1 - .../net/corda/testing/driver/DriverTests.kt | 21 +- .../kotlin/net/corda/testing/node/MockNode.kt | 2 +- .../testing/node/internal/DriverDSLImpl.kt | 8 +- .../corda/demobench/model/NodeController.kt | 2 +- 13 files changed, 209 insertions(+), 276 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt index 97927e54db..1ec0e59c3f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt @@ -1,66 +1,87 @@ package net.corda.nodeapi.internal import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Crypto import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.cert import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.internal.toX509CertHolder import net.corda.core.utilities.trace +import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.crypto.* import org.slf4j.LoggerFactory import java.nio.file.Path import java.security.cert.X509Certificate +/** + * Contains utility methods for generating identities for a node. + * + * WARNING: This is not application for production use and must never called by the node. + */ +// TODO Rename to DevIdentityGenerator object IdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) + // TODO These don't need to be prefixes but can be the full aliases + // TODO Move these constants out of here as the node needs access to them const val NODE_IDENTITY_ALIAS_PREFIX = "identity" const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" - fun generateNodeIdentity(dir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party { - return generateToDisk(listOf(dir), legalName, NODE_IDENTITY_ALIAS_PREFIX, threshold = 1, customRootCert = customRootCert) + /** + * Install a node key store for the given node directory using the given legal name and an optional root cert. If no + * root cert is specified then the default one in certificates/cordadevcakeys.jks is used. + */ + fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party { + val nodeSslConfig = object : NodeSSLConfiguration { + override val baseDirectory = nodeDir + override val keyStorePassword: String = "cordacadevpass" + override val trustStorePassword get() = throw NotImplementedError("Not expected to be called") + } + + // TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location + val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") + val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") + // TODO If using a custom root cert, then the intermidate cert needs to be generated from it as well, and not taken from the default + val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + + nodeSslConfig.certificatesDirectory.createDirectories() + nodeSslConfig.createDevKeyStores(rootCert.toX509CertHolder(), intermediateCa, legalName) + + val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword) + val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair()) + return identity.party } fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party { - return generateToDisk(dirs, notaryName, DISTRIBUTED_NOTARY_ALIAS_PREFIX, threshold, customRootCert) - } + require(dirs.isNotEmpty()) - /** - * Generates signing key pairs and a common distributed service identity for a set of nodes. - * The key pairs and the group identity get serialized to disk in the corresponding node directories. - * This method should be called *before* any of the nodes are started. - * - * @param dirs List of node directories to place the generated identity and key pairs in. - * @param name The name of the identity. - * @param threshold The threshold for the generated group [CompositeKey]. - * @param customRootCert the certificate to use as the Corda root CA. If not specified the one in - * internal/certificates/cordadevcakeys.jks is used. - */ - private fun generateToDisk(dirs: List, - name: CordaX500Name, - aliasPrefix: String, - threshold: Int, - customRootCert: X509Certificate?): Party { - log.trace { "Generating identity \"$name\" for nodes: ${dirs.joinToString()}" } + log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } - val key = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) + val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") + // TODO If using a custom root cert, then the intermidate cert needs to be generated from it as well, and not taken from the default val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) - keyPairs.zip(dirs) { keyPair, dir -> - val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, keyPair.public) - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, key) - val certPath = (dir / "certificates").createDirectories() / "distributedService.jks" - val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass") - keystore.setCertificateEntry("$aliasPrefix-composite-key", compositeKeyCert.cert) - keystore.setKeyEntry("$aliasPrefix-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) - keystore.save(certPath, "cordacadevpass") + keyPairs.zip(dirs) { keyPair, nodeDir -> + val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey -> + X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, notaryName, publicKey) + } + val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks" + val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass") + keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert.cert) + keystore.setKeyEntry( + "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", + keyPair.private, + "cordacadevkeypass".toCharArray(), + arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) + keystore.save(distServKeyStoreFile, "cordacadevpass") } - return Party(name, key) + return Party(notaryName, compositeKey) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt new file mode 100644 index 0000000000..1fa03959a4 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -0,0 +1,47 @@ +package net.corda.nodeapi.internal + +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.x500Name +import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.crypto.* +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.cert.X509CertificateHolder + +/** + * Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using + * the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA. + */ +fun SSLConfiguration.createDevKeyStores(rootCert: X509CertificateHolder, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) { + val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) + val nodeCaCert = X509Utilities.createCertificate(CertificateType.NODE_CA, + intermediateCa.certificate, + intermediateCa.keyPair, + legalName, + nodeCaKeyPair.public, + nameConstraints = nameConstraints) + + loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply { + addOrReplaceKey( + X509Utilities.CORDA_CLIENT_CA, + nodeCaKeyPair.private, + keyStorePassword.toCharArray(), + arrayOf(nodeCaCert, intermediateCa.certificate, rootCert)) + save(nodeKeystore, keyStorePassword) + } + + val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName, tlsKeyPair.public) + + loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply { + addOrReplaceKey( + X509Utilities.CORDA_CLIENT_TLS, + tlsKeyPair.private, + keyStorePassword.toCharArray(), + arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert)) + save(sslKeystore, keyStorePassword) + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt index 90544e92a6..fb5424e19d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt @@ -8,6 +8,7 @@ interface SSLConfiguration { val trustStorePassword: String val certificatesDirectory: Path val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" + // TODO This looks like it should be in NodeSSLConfiguration val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt index 211f156a8d..2504ce221b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt @@ -1,41 +1,26 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.read import java.nio.file.Path import java.security.KeyPair -import java.security.PublicKey -import java.security.cert.CertPath import java.security.cert.Certificate class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) { private val keyStore = storePath.read { loadKeyStore(it, storePassword) } - private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath { - val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + // TODO This method seems misplaced in this class. + fun storeLegalIdentity(legalName: CordaX500Name, alias: String, keyPair: KeyPair): PartyAndCertificate { + val nodeCaCertChain = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val nodeCa = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCa.certificate, nodeCa.keyPair, legalName, keyPair.public) + val identityCertPath = X509CertificateFactory().generateCertPath(identityCert.cert, *nodeCaCertChain) // Assume key password = store password. - val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) - // Create new keys and store in keystore. - val cert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey) - val certPath = X509CertificateFactory().generateCertPath(cert.cert, *clientCertPath) - require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } - // TODO: X509Utilities.validateCertificateChain() - return certPath - } - - fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) { - val certPath = createCertificate(serviceName, keyPair.public) - // Assume key password = store password. - keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray()) - keyStore.save(storePath, storePassword) - } - - fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) { - val certPath = createCertificate(serviceName, pubKey) - // Assume key password = store password. - keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first()) + keyStore.addOrReplaceKey(alias, keyPair.private, storePassword.toCharArray(), identityCertPath.certificates.toTypedArray()) keyStore.save(storePath, storePassword) + return PartyAndCertificate(identityCertPath) } // Delegate methods to keystore. Sadly keystore doesn't have an interface. @@ -47,5 +32,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias) - fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword) + fun getCertificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword) } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 8435ff5791..070fe20659 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -12,12 +12,16 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.serialization.KryoServerSerializationScheme -import net.corda.node.services.config.createKeystoreForCordaNode +import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.SerializationContextImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 -import net.corda.testing.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.TestIdentity +import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension @@ -33,11 +37,8 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.nio.file.Path -import java.security.KeyStore -import java.security.PrivateKey import java.security.SecureRandom import java.security.cert.CertPath -import java.security.cert.Certificate import java.security.cert.X509Certificate import java.util.* import java.util.stream.Stream @@ -52,6 +53,11 @@ class X509UtilitiesTest { val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party val BOB get() = bob.party val BOB_PUBKEY get() = bob.publicKey + val CIPHER_SUITES = arrayOf( + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + ) } @Rule @@ -155,107 +161,62 @@ class X509UtilitiesTest { assertEquals(edDSAKeypair.private, privateKey) } - @Test - fun `create full CA keystore`() { - val tmpKeyStore = tempFile("keystore.jks") - val tmpTrustStore = tempFile("truststore.jks") - - // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store - createCAKeyStoreAndTrustStore(tmpKeyStore, "keystorepass", "keypass", tmpTrustStore, "trustpass") - - // Load back generated root CA Cert and private key from keystore and check against copy in truststore - val keyStore = loadKeyStore(tmpKeyStore, "keystorepass") - val trustStore = loadKeyStore(tmpTrustStore, "trustpass") - val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate - val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey - val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate - assertEquals(rootCaCert, rootCaFromTrustStore) - rootCaCert.checkValidity(Date()) - rootCaCert.verify(rootCaCert.publicKey) - - // Now sign something with private key and verify against certificate public key - val testData = "12345".toByteArray() - val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } - - // Load back generated intermediate CA Cert and private key - val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate - val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey - intermediateCaCert.checkValidity(Date()) - intermediateCaCert.verify(rootCaCert.publicKey) - - // Now sign something with private key and verify against certificate public key - val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } - } - @Test fun `create server certificate in keystore for SSL`() { - val tmpCAKeyStore = tempFile("keystore.jks") - val tmpTrustStore = tempFile("truststore.jks") - val tmpSSLKeyStore = tempFile("sslkeystore.jks") - val tmpServerKeyStore = tempFile("serverkeystore.jks") + val sslConfig = object : SSLConfiguration { + override val certificatesDirectory = tempFolder.root.toPath() + override val keyStorePassword = "serverstorepass" + override val trustStorePassword = "trustpass" + } - // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store - createCAKeyStoreAndTrustStore(tmpCAKeyStore, - "cakeystorepass", - "cakeypass", - tmpTrustStore, - "trustpass") - - // Load signing intermediate CA cert - val caKeyStore = loadKeyStore(tmpCAKeyStore, "cakeystorepass") - val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass") + val (rootCert, intermediateCa) = createRootCertAndIntermediateCa() // Generate server cert and private key and populate another keystore suitable for SSL - createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) + sslConfig.createDevKeyStores(rootCert, intermediateCa, MEGA_CORP.name) // Load back server certificate - val serverKeyStore = loadKeyStore(tmpServerKeyStore, "serverstorepass") - val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass") + val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword) + val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword) - serverCertAndKey.certificate.isValidOn(Date()) - serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo)) + serverCert.cert.checkValidity() + serverCert.cert.verify(intermediateCa.certificate.cert.publicKey) + assertThat(CordaX500Name.parse(serverCert.subject.toString())).isEqualTo(MEGA_CORP.name) - assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) } + // Load back SSL certificate + val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) + val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword) - // Load back server certificate - val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass") - val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass") + sslCert.cert.checkValidity() + sslCert.cert.verify(serverCert.cert.publicKey) + assertThat(CordaX500Name.parse(sslCert.subject.toString())).isEqualTo(MEGA_CORP.name) - sslCertAndKey.certificate.isValidOn(Date()) - sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo)) - - assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() - val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) - val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo) + val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverKeyPair.private, testData) + val publicKey = Crypto.toSupportedPublicKey(serverCert.subjectPublicKeyInfo) assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) } } @Test fun `create server cert and use in SSL socket`() { - val tmpCAKeyStore = tempFile("keystore.jks") - val tmpTrustStore = tempFile("truststore.jks") - val tmpSSLKeyStore = tempFile("sslkeystore.jks") - val tmpServerKeyStore = tempFile("serverkeystore.jks") + val sslConfig = object : SSLConfiguration { + override val certificatesDirectory = tempFolder.root.toPath() + override val keyStorePassword = "serverstorepass" + override val trustStorePassword = "trustpass" + } - // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store - val caKeyStore = createCAKeyStoreAndTrustStore(tmpCAKeyStore, - "cakeystorepass", - "cakeypass", - tmpTrustStore, - "trustpass") + val (rootCert, intermediateCa) = createRootCertAndIntermediateCa() // Generate server cert and private key and populate another keystore suitable for SSL - createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name) - val keyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass") - val trustStore = loadKeyStore(tmpTrustStore, "trustpass") + sslConfig.createDevKeyStores(rootCert, intermediateCa, MEGA_CORP.name) + sslConfig.createTrustStore(rootCert.cert) + + val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) + val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) val context = SSLContext.getInstance("TLS") val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, "serverstorepass".toCharArray()) + keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray()) val keyManagers = keyManagerFactory.keyManagers val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustMgrFactory.init(trustStore) @@ -266,13 +227,7 @@ class X509UtilitiesTest { val clientSocketFactory = context.socketFactory val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket - val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"), - arrayOf("TLSv1.2")) + val serverParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. @@ -280,13 +235,7 @@ class X509UtilitiesTest { serverSocket.useClientMode = false val clientSocket = clientSocketFactory.createSocket() as SSLSocket - val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"), - arrayOf("TLSv1.2")) + val clientParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2")) clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. clientSocket.sslParameters = clientParams clientSocket.useClientMode = true @@ -344,60 +293,30 @@ class X509UtilitiesTest { private fun tempFile(name: String): Path = tempFolder.root.toPath() / name - /** - * All in one wrapper to manufacture a root CA cert and an Intermediate CA cert. - * Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs - * @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into. - * @param storePassword The storage password to protect access to the generated KeyStore and public certificates - * @param keyPassword The password that protects the CA private keys. - * Unlike the SSL libraries that tend to assume the password is the same as the keystore password. - * These CA private keys should be protected more effectively with a distinct password. - * @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore - * @param trustStorePassword The password to protect the truststore - * @return The KeyStore object that was saved to file - */ - private fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path, - storePassword: String, - keyPassword: String, - trustStoreFilePath: Path, - trustStorePassword: String - ): KeyStore { - val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB") - val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey) + private fun createRootCertAndIntermediateCa(): Pair { + val rootKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val baseName = CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB") + val rootCert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Root CA"), rootKeyPair) - val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate( + val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCaCert = X509Utilities.createCertificate( CertificateType.INTERMEDIATE_CA, - rootCACert, - rootCAKey, - baseName.copy(commonName = "Corda Node Intermediate CA"), - intermediateCAKeyPair.public) + rootCert, + rootKeyPair, + baseName.copy(commonName = "Corda Intermediate CA"), + intermediateCaKeyPair.public) - val keyPass = keyPassword.toCharArray() - val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) + return Pair(rootCert, CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair)) + } - keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf(rootCACert.cert)) - - keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, - intermediateCAKeyPair.private, - keyPass, - Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray()) - - keyStore.save(keyStoreFilePath, storePassword) - - val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword) - - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert) - - trustStore.save(trustStoreFilePath, trustStorePassword) - - return keyStore + private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { + val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + trustStore.save(trustStoreFile, trustStorePassword) } @Test - fun `Get correct private key type from Keystore`() { + fun `get correct private key type from Keystore`() { val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB") val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair) 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 0a59cd1804..fff1f7dffb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -696,7 +696,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) - val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val clientCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) val caCertificates = arrayOf(identityCert, clientCa.certificate.cert) return PersistentIdentityService(trustRoot, *caCertificates) } @@ -739,10 +739,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, "Unable to find in the key store the identity of the distributed notary ($id) the node is part of") // TODO: Remove use of [IdentityGenerator.generateToDisk]. log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") - keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair()) + keyStore.storeLegalIdentity(singleName, privateKeyAlias, generateKeyPair()) } - val (x509Cert, keyPair) = keyStore.certificateAndKeyPair(privateKeyAlias) + val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias) // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. val compositeKeyAlias = "$id-composite-key" diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index bface31bcc..bb1e3da993 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -4,18 +4,16 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigRenderOptions -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SignatureScheme import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.* +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.internal.toX509CertHolder import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.crypto.* -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralSubtree -import org.bouncycastle.asn1.x509.NameConstraints import org.slf4j.LoggerFactory import java.nio.file.Path -import java.security.KeyStore fun configOf(vararg pairs: Pair): Config = ConfigFactory.parseMap(mapOf(*pairs)) operator fun Config.plus(overrides: Map): Config = ConfigFactory.parseMap(overrides).withFallback(this) @@ -45,8 +43,10 @@ object ConfigHelper { * Strictly for dev only automatically construct a server certificate/private key signed from * the CA certs in Node resources. Then provision KeyStores into certificates folder under node path. */ +// TODO Move this to KeyStoreConfigHelpers fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName) +// TODO Move this to KeyStoreConfigHelpers fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { @@ -54,7 +54,9 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { } if (!sslKeystore.exists() || !nodeKeystore.exists()) { val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") - createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) + val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder() + val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") + createDevKeyStores(rootCert, intermediateCa, myLegalName) // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" @@ -73,58 +75,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { } } } - -/** - * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine. - * @param sslKeyStorePath KeyStore path to save ssl key and cert to. - * @param clientCAKeystorePath KeyStore path to save client CA key and cert to. - * @param storePassword access password for KeyStore. - * @param keyPassword PrivateKey access password for the generated keys. - * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. - * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore. - * @param caKeyPassword password to unlock private keys in the CA KeyStore. - * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications. - */ -fun createKeystoreForCordaNode(sslKeyStorePath: Path, - clientCAKeystorePath: Path, - storePassword: String, - keyPassword: String, - caKeyStore: KeyStore, - caKeyPassword: String, - legalName: CordaX500Name, - signatureScheme: SignatureScheme = X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) { - - val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder() - val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword) - - val clientKey = Crypto.generateKeyPair(signatureScheme) - - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) - val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, - intermediateCACert, - intermediateCAKeyPair, - legalName, - clientKey.public, - nameConstraints = nameConstraints) - - val tlsKey = Crypto.generateKeyPair(signatureScheme) - val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) - - val keyPass = keyPassword.toCharArray() - - val clientCAKeystore = loadOrCreateKeyStore(clientCAKeystorePath, storePassword) - clientCAKeystore.addOrReplaceKey( - X509Utilities.CORDA_CLIENT_CA, - clientKey.private, - keyPass, - arrayOf(clientCACert, intermediateCACert, rootCACert)) - clientCAKeystore.save(clientCAKeystorePath, storePassword) - - val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword) - tlsKeystore.addOrReplaceKey( - X509Utilities.CORDA_CLIENT_TLS, - tlsKey.private, - keyPass, - arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)) - tlsKeystore.save(sslKeyStorePath, storePassword) -} diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 698420ca0a..f6f3b81177 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -94,6 +94,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v caKeyStore.save(config.nodeKeystore, keystorePassword) println("Node private key and certificate stored in ${config.nodeKeystore}.") + // TODO This should actually be using X509Utilities.validateCertificateChain // Check that the root of the signed certificate matches the expected certificate in the truststore. if (rootCert != certificates.last()) { // Assumes certificate chain always starts with client certificate and end with root certificate. diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 59384a412f..d883f0d75b 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -4,7 +4,6 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name -import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.RaftConfig 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 c618c400cd..475ddd7359 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 @@ -2,6 +2,7 @@ package net.corda.testing.driver import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.CertRole import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines @@ -42,12 +43,28 @@ class DriverTests { @Test fun `simple node startup and shutdown`() { val handle = driver { - val regulator = startNode(providedName = DUMMY_REGULATOR_NAME) - nodeMustBeUp(regulator) + val node = startNode(providedName = DUMMY_REGULATOR_NAME) + nodeMustBeUp(node) } nodeMustBeDown(handle) } + @Test + fun `starting with default notary`() { + driver { + // Make sure the default is a single-node notary + val notary = defaultNotaryNode.getOrThrow() + val notaryIdentities = notary.nodeInfo.legalIdentitiesAndCerts + // Make sure the notary node has only one identity + assertThat(notaryIdentities).hasSize(1) + val identity = notaryIdentities[0] + // Make sure this identity is a legal identity, like it is for normal nodes. + assertThat(CertRole.extract(identity.certificate)).isEqualTo(CertRole.LEGAL_IDENTITY) + // And make sure this identity is published as the notary identity (via the network parameters) + assertThat(notary.rpc.notaryIdentities()).containsOnly(identity.party) + } + } + @Test fun `random free port allocation`() { val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) { 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 65c12cc77b..1b8eae76df 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 @@ -235,7 +235,7 @@ open class MockNetwork(private val cordappPackages: List, private fun generateNotaryIdentities(): List { return notarySpecs.mapIndexed { index, (name, validating) -> - val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name) + val identity = IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name) NotaryInfo(identity, validating) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index c4c36fa8c7..470a87e1d4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -10,10 +10,8 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf -import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.concurrent.* import net.corda.core.messaging.CordaRPCOps @@ -31,7 +29,6 @@ import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.IdentityGenerator -import net.corda.nodeapi.internal.IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -46,7 +43,6 @@ import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME import net.corda.testing.DUMMY_BANK_A_NAME -import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.* @@ -270,7 +266,7 @@ class DriverDSLImpl( clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name) } else { // We have all we need here to generate the identity for single node notaries - val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(name), legalName = name) + val identity = IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name) notaryInfos += NotaryInfo(identity, notaryConfig.validating) } } @@ -364,7 +360,7 @@ class DriverDSLImpl( private fun generateNotaryIdentities(): List { return notarySpecs.map { spec -> val identity = if (spec.cluster == null) { - IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) + IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) } else { IdentityGenerator.generateDistributedNotaryIdentity( dirs = generateNodeNames(spec).map { baseDirectory(it) }, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index ee6b38fdb5..0f98c47f48 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -153,7 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Generate notary identity and save it into node's directory. This identity will be used in network parameters. private fun getNotaryIdentity(config: NodeConfigWrapper): Party { - return IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) + return IdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) } fun reset() { From 4a2f15711847136b6f2ce58dbbc7d607f44ffcdf Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 29 Dec 2017 14:38:30 +0000 Subject: [PATCH 13/18] Validating the entire cert path in node registration, rather just checking the root cert. (#2298) Also reduced duplicate code when creating the node CA cert path for testing, and renamed IdentityGenerator to DevIdentityGenerator. --- .../core/crypto/X509NameConstraintsTest.kt | 37 +++++----- ...tyGenerator.kt => DevIdentityGenerator.kt} | 5 +- .../nodeapi/internal/KeyStoreConfigHelpers.kt | 28 +++++--- .../internal/crypto/X509UtilitiesTest.kt | 27 ++----- .../net/corda/node/NodeKeystoreCheckTest.kt | 35 +++++----- .../node/services/BFTNotaryServiceTests.kt | 4 +- .../registration/NodeRegistrationTest.kt | 70 +++++++++---------- .../net/corda/node/internal/AbstractNode.kt | 6 +- .../registration/NetworkRegistrationHelper.kt | 28 ++------ .../NetworkRegistrationHelperTest.kt | 31 ++++---- .../net/corda/notarydemo/BFTNotaryCordform.kt | 4 +- .../corda/notarydemo/RaftNotaryCordform.kt | 4 +- .../kotlin/net/corda/testing/node/MockNode.kt | 4 +- .../testing/node/internal/DriverDSLImpl.kt | 23 +++--- .../node/internal/network/NetworkMapServer.kt | 17 ++--- .../kotlin/net/corda/testing/CoreTestUtils.kt | 20 ++---- .../testing/internal/InternalTestUtils.kt | 47 +++++++++++++ .../corda/demobench/model/NodeController.kt | 4 +- 18 files changed, 205 insertions(+), 189 deletions(-) rename node-api/src/main/kotlin/net/corda/nodeapi/internal/{IdentityGenerator.kt => DevIdentityGenerator.kt} (96%) diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index e135d1c93c..707a32bba4 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -1,9 +1,9 @@ package net.corda.core.crypto import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.toTypedArray import net.corda.core.internal.cert import net.corda.nodeapi.internal.crypto.* +import net.corda.testing.internal.createDevIntermediateCaCertPath import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -11,35 +11,40 @@ import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test import java.security.KeyStore -import java.security.cert.* -import java.util.stream.Stream +import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.PKIXParameters import kotlin.test.assertFailsWith import kotlin.test.assertTrue class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { - val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys) - - val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public) - - val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, CordaX500Name(commonName = "Corda Client CA", organisation = "R3 Ltd", locality = "London", country = "GB"), clientCAKeyPair.public, nameConstraints = nameConstraints) + val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() + val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + intermediateCa.certificate, + intermediateCa.keyPair, + CordaX500Name("Corda Client CA", "R3 Ltd", "London", "GB"), + nodeCaKeyPair.public, + nameConstraints = nameConstraints) val keyPass = "password" val trustStore = KeyStore.getInstance(KEYSTORE_TYPE) trustStore.load(null, keyPass.toCharArray()) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate.cert) - val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public) + val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, subjectName, tlsKeyPair.public) val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) keyStore.load(null, keyPass.toCharArray()) - keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(), - Stream.of(tlsCert, clientCACert, intermediateCACert, rootCACert).map { it.cert }.toTypedArray()) + keyStore.addOrReplaceKey( + X509Utilities.CORDA_CLIENT_TLS, + tlsKeyPair.private, + keyPass.toCharArray(), + arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate)) return Pair(keyStore, trustStore) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt similarity index 96% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index 1ec0e59c3f..484ab18116 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -19,10 +19,9 @@ import java.security.cert.X509Certificate /** * Contains utility methods for generating identities for a node. * - * WARNING: This is not application for production use and must never called by the node. + * WARNING: This is not application for production use. */ -// TODO Rename to DevIdentityGenerator -object IdentityGenerator { +object DevIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) // TODO These don't need to be prefixes but can be the full aliases diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 1fa03959a4..03bd39e635 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -15,14 +15,7 @@ import org.bouncycastle.cert.X509CertificateHolder * the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA. */ fun SSLConfiguration.createDevKeyStores(rootCert: X509CertificateHolder, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) { - val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) - val nodeCaCert = X509Utilities.createCertificate(CertificateType.NODE_CA, - intermediateCa.certificate, - intermediateCa.keyPair, - legalName, - nodeCaKeyPair.public, - nameConstraints = nameConstraints) + val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName) loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply { addOrReplaceKey( @@ -44,4 +37,21 @@ fun SSLConfiguration.createDevKeyStores(rootCert: X509CertificateHolder, interme arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert)) save(sslKeystore, keyStorePassword) } -} \ No newline at end of file +} + +/** + * Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given + * [CordaX500Name] as the cert subject. + */ +fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) + val cert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + intermediateCa.certificate, + intermediateCa.keyPair, + legalName, + keyPair.public, + nameConstraints = nameConstraints) + return CertificateAndKeyPair(cert, keyPair) +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 070fe20659..f078035241 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -21,6 +21,7 @@ import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1 import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME import net.corda.testing.TestIdentity +import net.corda.testing.internal.createDevIntermediateCaCertPath import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints @@ -169,10 +170,10 @@ class X509UtilitiesTest { override val trustStorePassword = "trustpass" } - val (rootCert, intermediateCa) = createRootCertAndIntermediateCa() + val (rootCert, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(rootCert, intermediateCa, MEGA_CORP.name) + sslConfig.createDevKeyStores(rootCert.certificate, intermediateCa, MEGA_CORP.name) // Load back server certificate val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword) @@ -205,11 +206,11 @@ class X509UtilitiesTest { override val trustStorePassword = "trustpass" } - val (rootCert, intermediateCa) = createRootCertAndIntermediateCa() + val (rootCert, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(rootCert, intermediateCa, MEGA_CORP.name) - sslConfig.createTrustStore(rootCert.cert) + sslConfig.createDevKeyStores(rootCert.certificate, intermediateCa, MEGA_CORP.name) + sslConfig.createTrustStore(rootCert.certificate.cert) val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) @@ -293,22 +294,6 @@ class X509UtilitiesTest { private fun tempFile(name: String): Path = tempFolder.root.toPath() / name - private fun createRootCertAndIntermediateCa(): Pair { - val rootKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val baseName = CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB") - val rootCert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Root CA"), rootKeyPair) - - val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCaCert = X509Utilities.createCertificate( - CertificateType.INTERMEDIATE_CA, - rootCert, - rootKeyPair, - baseName.copy(commonName = "Corda Intermediate CA"), - intermediateCaKeyPair.public) - - return Pair(rootCert, CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair)) - } - private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index ca1a3e9792..17cad81d22 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -10,23 +10,23 @@ import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.* import net.corda.testing.ALICE_NAME import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import java.nio.file.Path -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue class NodeKeystoreCheckTest { + @Test + fun `starting node in non-dev mode with no key store`() { + driver(startNodesInProcess = true) { + assertThatThrownBy { + startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() + }.hasMessageContaining("Identity certificate not found") + } + } + @Test fun `node should throw exception if cert path doesn't chain to the trust root`() { driver(startNodesInProcess = true) { - // This will fail because there are no keystore configured. - assertFailsWith(IllegalArgumentException::class) { - startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() - }.apply { - assertTrue(message?.startsWith("Identity certificate not found. ") ?: false) - } - // Create keystores val keystorePassword = "password" val config = object : SSLConfiguration { @@ -37,9 +37,12 @@ class NodeKeystoreCheckTest { config.configureDevKeyAndTrustStores(ALICE_NAME) // This should pass with correct keystore. - val node = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false, - "keyStorePassword" to keystorePassword, - "trustStorePassword" to keystorePassword)).get() + val node = startNode( + providedName = ALICE_NAME, + customOverrides = mapOf("devMode" to false, + "keyStorePassword" to keystorePassword, + "trustStorePassword" to keystorePassword) + ).getOrThrow() node.stop() // Fiddle with node keystore. @@ -53,11 +56,9 @@ class NodeKeystoreCheckTest { keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert)) keystore.save(config.nodeKeystore, config.keyStorePassword) - assertFailsWith(IllegalArgumentException::class) { + assertThatThrownBy { startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow() - }.apply { - assertEquals("Client CA certificate must chain to the trusted root.", message) - } + }.hasMessage("Client CA certificate must chain to the trusted root.") } } } 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 869957c010..0fa5ed8d36 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 @@ -23,7 +23,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.chooseIdentity @@ -60,7 +60,7 @@ class BFTNotaryServiceTests { (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - notary = IdentityGenerator.generateDistributedNotaryIdentity( + notary = DevIdentityGenerator.generateDistributedNotaryIdentity( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, CordaX500Name("BFT", "Zurich", "CH")) diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index af1a44426c..c7f0e2c1e1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -6,7 +6,10 @@ import net.corda.core.internal.cert import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.toX509CertHolder import net.corda.core.messaging.startFlow -import net.corda.core.utilities.* +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.minutes import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -16,7 +19,6 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.ROOT_CA import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation @@ -25,7 +27,6 @@ import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.network.NetworkMapServer import net.corda.testing.singleIdentity -import net.corda.testing.singleIdentityAndCert import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -39,6 +40,7 @@ import java.io.InputStream import java.net.URL import java.security.KeyPair import java.security.cert.CertPath +import java.security.cert.CertPathValidatorException import java.security.cert.Certificate import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -50,8 +52,10 @@ class NodeRegistrationTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + private val portAllocation = PortAllocation.Incremental(13000) private val registrationHandler = RegistrationHandler(ROOT_CA) + private lateinit var server: NetworkMapServer private lateinit var serverHostAndPort: NetworkHostAndPort @@ -73,72 +77,63 @@ class NodeRegistrationTest { portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false, - notarySpecs = listOf(NotarySpec(CordaX500Name(organisation = "NotaryService", locality = "Zurich", country = "CH"), validating = false)), + notarySpecs = listOf(NotarySpec(CordaX500Name("NotaryService", "Zurich", "CH"), validating = false)), extraCordappPackagesToScan = listOf("net.corda.finance"), onNetworkParametersGeneration = { server.networkParameters = it } ) { - val notary = defaultNotaryNode.get() + val aliceName = "Alice" + val genevieveName = "Genevieve" - val ALICE_NAME = "Alice" - val GENEVIEVE_NAME = "Genevieve" - val nodesFutures = listOf(startNode(providedName = CordaX500Name(ALICE_NAME, "London", "GB")), - startNode(providedName = CordaX500Name(GENEVIEVE_NAME, "London", "GB"))) + val nodes = listOf( + startNode(providedName = CordaX500Name(aliceName, "London", "GB")), + startNode(providedName = CordaX500Name(genevieveName, "London", "GB")), + defaultNotaryNode + ).transpose().getOrThrow() + val (alice, genevieve) = nodes - val (alice, genevieve) = nodesFutures.transpose().get() - val nodes = listOf(alice, genevieve, notary) - - assertThat(registrationHandler.idsPolled).contains(ALICE_NAME, GENEVIEVE_NAME) + assertThat(registrationHandler.idsPolled).contains(aliceName, genevieveName) // Notary identities are generated beforehand hence notary nodes don't go through registration. // This test isn't specifically testing this, or relying on this behavior, though if this check fail, // this will probably lead to the rest of the test to fail. assertThat(registrationHandler.idsPolled).doesNotContain("NotaryService") // Check each node has each other identity in their network map cache. - val nodeIdentities = nodes.map { it.nodeInfo.singleIdentity() } for (node in nodes) { - assertThat(node.rpc.networkMapSnapshot().map { it.singleIdentity() }).containsAll(nodeIdentities) + assertThat(node.rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo }) } // Check we nodes communicate among themselves (and the notary). val anonymous = false - genevieve.rpc.startFlow(::CashIssueAndPaymentFlow, 1000.DOLLARS, OpaqueBytes.of(12), + genevieve.rpc.startFlow( + ::CashIssueAndPaymentFlow, + 1000.DOLLARS, + OpaqueBytes.of(12), alice.nodeInfo.singleIdentity(), anonymous, - notary.nodeInfo.singleIdentity()) - .returnValue - .getOrThrow() + defaultNotaryIdentity + ).returnValue.getOrThrow() } } @Test fun `node registration wrong root cert`() { - val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert - val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert) + val someRootCert = X509Utilities.createSelfSignedCACertificate( + CordaX500Name("Integration Test Corda Node Root CA", "R3 Ltd", "London", "GB"), + Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someRootCert.cert) internalDriver( portAllocation = portAllocation, notarySpecs = emptyList(), compatibilityZone = compatibilityZone, initialiseSerialization = false, - // Changing the content of the truststore makes the node fail in a number of ways if started out process. - startNodesInProcess = true + startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception ) { assertThatThrownBy { startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() - }.isInstanceOf(WrongRootCertException::class.java) + }.isInstanceOf(CertPathValidatorException::class.java) } } - private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair { - val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate( - CordaX500Name( - commonName = "Integration Test Corda Node Root CA", - organisation = "R3 Ltd", - locality = "London", - country = "GB"), - rootCAKey) - return CertificateAndKeyPair(rootCACert, rootCAKey) - } } @Path("certificate") @@ -185,13 +180,14 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair) caCertPath: Array): Pair { val request = JcaPKCS10CertificationRequest(certificationRequest) val name = CordaX500Name.parse(request.subject.toString()) - val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.NODE_CA, + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, caCertPath.first().toX509CertHolder(), caKeyPair, name, request.publicKey, nameConstraints = null) - val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath) + val certPath = X509CertificateFactory().generateCertPath(nodeCaCert.cert, *caCertPath) return Pair(certPath, 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 fff1f7dffb..c2ef31ae33 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -59,7 +59,7 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.KeyStoreWrapper import net.corda.nodeapi.internal.crypto.X509CertificateFactory @@ -726,10 +726,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { // Node's main identity or if it's a single node notary - Pair(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) + Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) } else { // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) + Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index f6f3b81177..af9735b89a 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -12,10 +12,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter -import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.cert.Certificate +import java.security.cert.X509Certificate /** * Helper for managing the node registration process, which checks for any existing certificates and requests them if @@ -32,7 +32,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v // TODO: Use different password for private key. private val privateKeyPassword = config.keyStorePassword private val trustStore: KeyStore - private val rootCert: Certificate + private val rootCert: X509Certificate init { require(config.trustStoreFile.exists()) { @@ -46,7 +46,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v "This file must contain the root CA cert of your compatibility zone. " + "Please contact your CZ operator." } - this.rootCert = rootCert + this.rootCert = rootCert as X509Certificate } /** @@ -94,12 +94,8 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v caKeyStore.save(config.nodeKeystore, keystorePassword) println("Node private key and certificate stored in ${config.nodeKeystore}.") - // TODO This should actually be using X509Utilities.validateCertificateChain - // Check that the root of the signed certificate matches the expected certificate in the truststore. - if (rootCert != certificates.last()) { - // Assumes certificate chain always starts with client certificate and end with root certificate. - throw WrongRootCertException(rootCert, certificates.last(), config.trustStoreFile) - } + println("Checking root of the certificate path is what we expect.") + X509Utilities.validateCertificateChain(rootCert, *certificates) println("Generating SSL certificate for node messaging service.") val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -169,17 +165,3 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v } } } - -/** - * Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate. - * This usually means that there has been a Man-in-the-middle attack when contacting the doorman. - */ -class WrongRootCertException(expected: Certificate, - actual: Certificate, - expectedFilePath: Path): - Exception(""" - The Root CA returned back from the registration process does not match the expected Root CA - expected: $expected - actual: $actual - the expected certificate is stored in: $expectedFilePath with alias $CORDA_ROOT_CA - """.trimMargin()) diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 209153fdb8..13113fccf9 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -14,12 +14,14 @@ import net.corda.core.internal.createDirectories import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.* import net.corda.testing.ALICE_NAME +import net.corda.testing.internal.createDevNodeCaCertPath import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After import org.junit.Before import org.junit.Test +import java.security.cert.CertPathValidatorException import java.security.cert.Certificate import java.security.cert.X509Certificate import kotlin.test.assertFalse @@ -29,16 +31,19 @@ class NetworkRegistrationHelperTest { private val fs = Jimfs.newFileSystem(unix()) private val requestId = SecureHash.randomSHA256().toString() private val nodeLegalName = ALICE_NAME - private val intermediateCaName = CordaX500Name("CORDA_INTERMEDIATE_CA", "R3 Ltd", "London", "GB") - private val rootCaName = CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB") - private val nodeCaCert = createCaCert(nodeLegalName) - private val intermediateCaCert = createCaCert(intermediateCaName) - private val rootCaCert = createCaCert(rootCaName) + private lateinit var rootCaCert: X509Certificate + private lateinit var intermediateCaCert: X509Certificate + private lateinit var nodeCaCert: X509Certificate private lateinit var config: NodeConfiguration @Before fun init() { + val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(nodeLegalName) + this.rootCaCert = rootCa.certificate.cert + this.intermediateCaCert = intermediateCa.certificate.cert + this.nodeCaCert = nodeCa.certificate.cert + val baseDirectory = fs.getPath("/baseDir").createDirectories() abstract class AbstractNodeConfiguration : NodeConfiguration config = rigorousMock().also { @@ -108,11 +113,13 @@ class NetworkRegistrationHelperTest { @Test fun `wrong root cert in truststore`() { - saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB"))) + val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), rootKeyPair) + saveTrustStoreWithRootCa(rootCert.cert) val registrationHelper = createRegistrationHelper() assertThatThrownBy { registrationHelper.buildKeystore() - }.isInstanceOf(WrongRootCertException::class.java) + }.isInstanceOf(CertPathValidatorException::class.java) } private fun createRegistrationHelper(): NetworkRegistrationHelper { @@ -123,15 +130,11 @@ class NetworkRegistrationHelperTest { return NetworkRegistrationHelper(config, certService) } - private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) { - config.trustStoreFile.parent.createDirectories() + private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) { + config.certificatesDirectory.createDirectories() loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { - it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa) + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) it.save(config.trustStoreFile, config.trustStorePassword) } } - - private fun createCaCert(name: CordaX500Name): X509Certificate { - return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert - } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 311a5c51b2..1b2967c953 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -8,7 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - IdentityGenerator.generateDistributedNotaryIdentity( + DevIdentityGenerator.generateDistributedNotaryIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName, minCorrectReplicas(clusterSize) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index d883f0d75b..abceabbe77 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -7,7 +7,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.RaftConfig -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - IdentityGenerator.generateDistributedNotaryIdentity( + DevIdentityGenerator.generateDistributedNotaryIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName ) 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 1b8eae76df..fb4a7ec2e3 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 @@ -38,7 +38,7 @@ import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo @@ -235,7 +235,7 @@ open class MockNetwork(private val cordappPackages: List, private fun generateNotaryIdentities(): List { return notarySpecs.mapIndexed { index, (name, validating) -> - val identity = IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name) + val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name) NotaryInfo(identity, validating) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 470a87e1d4..bdd14a5278 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -10,10 +10,12 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf -import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.* +import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.* +import net.corda.core.internal.copyTo +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.NetworkMapCache import net.corda.core.toFuture @@ -28,7 +30,7 @@ import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -37,13 +39,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.nodeapi.internal.crypto.save +import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME import net.corda.testing.DUMMY_BANK_A_NAME -import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.* import net.corda.testing.node.ClusterSpec @@ -66,7 +68,7 @@ import java.nio.file.StandardCopyOption import java.security.cert.X509Certificate import java.time.Duration import java.time.Instant -import java.time.ZoneOffset +import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter import java.util.* import java.util.concurrent.Executors @@ -266,13 +268,13 @@ class DriverDSLImpl( clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name) } else { // We have all we need here to generate the identity for single node notaries - val identity = IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name) + val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name) notaryInfos += NotaryInfo(identity, notaryConfig.validating) } } clusterNodes.asMap().forEach { type, nodeNames -> - val identity = IdentityGenerator.generateDistributedNotaryIdentity( + val identity = DevIdentityGenerator.generateDistributedNotaryIdentity( dirs = nodeNames.map { baseDirectory(it) }, notaryName = type.clusterName ) @@ -360,9 +362,9 @@ class DriverDSLImpl( private fun generateNotaryIdentities(): List { return notarySpecs.map { spec -> val identity = if (spec.cluster == null) { - IdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) + DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) } else { - IdentityGenerator.generateDistributedNotaryIdentity( + DevIdentityGenerator.generateDistributedNotaryIdentity( dirs = generateNodeNames(spec).map { baseDirectory(it) }, notaryName = spec.name, customRootCert = compatibilityZone?.rootCert @@ -890,8 +892,7 @@ fun internalDriver( } fun getTimestampAsDirectoryName(): String { - // Add a random number in case 2 tests are started in the same instant. - return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + random63BitValue() + return DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC).format(Instant.now()) } fun writeConfig(path: Path, filename: String, config: Config) { 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 995817c74e..4b16c09a47 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 @@ -1,26 +1,21 @@ package net.corda.testing.node.internal.network -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.identity.CordaX500Name import net.corda.core.crypto.* +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.DigitalSignatureWithCert import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.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 import net.corda.testing.ROOT_CA import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector @@ -50,7 +45,7 @@ class NetworkMapServer(cacheTimeout: Duration, private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair { val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val networkMapCert = X509Utilities.createCertificate( - CertificateType.INTERMEDIATE_CA, + CertificateType.NETWORK_MAP, rootCAKeyAndCert.certificate, rootCAKeyAndCert.keyPair, CordaX500Name("Corda Network Map", "R3 Ltd", "London","GB"), 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 860af415ac..a61744984b 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 @@ -5,24 +5,24 @@ package net.corda.testing import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.unspecifiedCountry -import net.corda.core.internal.x500Name import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair 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.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralSubtree -import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.cert.X509CertificateHolder import java.math.BigInteger import java.nio.file.Files @@ -92,15 +92,7 @@ fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT val intermediate: CertificateAndKeyPair = DEV_CA - - val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val nodeCaCert = X509Utilities.createCertificate( - CertificateType.NODE_CA, - intermediate.certificate, - intermediate.keyPair, - party.name, - nodeCaKeyPair.public, - nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf())) + val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediate, party.name) val identityCert = X509Utilities.createCertificate( CertificateType.LEGAL_IDENTITY, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index fc65d3603c..2624109897 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -1,6 +1,12 @@ package net.corda.testing.internal +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.createDevNodeCa +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED import org.mockito.Mockito import org.mockito.internal.stubbing.answers.ThrowsException @@ -42,3 +48,44 @@ fun rigorousMock(clazz: Class): T = Mockito.mock(clazz) { it.callRealMethod() } } + +private val defaultRootCaName = CordaX500Name("Corda Root CA", "R3 Ltd", "London", "GB") +private val defaultIntermediateCaName = CordaX500Name("Corda Intermediate CA", "R3 Ltd", "London", "GB") + +/** + * Returns a pair of [CertificateAndKeyPair]s, the first being the root CA and the second the intermediate CA. + * @param rootCaName The subject name for the root CA cert. + * @param intermediateCaName The subject name for the intermediate CA cert. + */ +fun createDevIntermediateCaCertPath( + rootCaName: CordaX500Name = defaultRootCaName, + intermediateCaName: CordaX500Name = defaultIntermediateCaName +): Pair { + val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(rootCaName, rootKeyPair) + + val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCaCert = X509Utilities.createCertificate( + CertificateType.INTERMEDIATE_CA, + rootCert, + rootKeyPair, + intermediateCaName, + intermediateCaKeyPair.public) + + return Pair(CertificateAndKeyPair(rootCert, rootKeyPair), CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair)) +} + +/** + * Returns a triple of [CertificateAndKeyPair]s, the first being the root CA, the second the intermediate CA and the third + * the node CA. + * @param legalName The subject name for the node CA cert. + */ +fun createDevNodeCaCertPath( + legalName: CordaX500Name, + rootCaName: CordaX500Name = defaultRootCaName, + intermediateCaName: CordaX500Name = defaultIntermediateCaName +): Triple { + val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName) + val nodeCa = createDevNodeCa(intermediateCa, legalName) + return Triple(rootCa, intermediateCa, nodeCa) +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 0f98c47f48..fddcfdf19a 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -13,7 +13,7 @@ import net.corda.demobench.pty.R3Pty import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.DevIdentityGenerator import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory @@ -153,7 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Generate notary identity and save it into node's directory. This identity will be used in network parameters. private fun getNotaryIdentity(config: NodeConfigWrapper): Party { - return IdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) + return DevIdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) } fun reset() { From 07258c0a166ff41b9adf5c482316b2a3d89150ce Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 2 Jan 2018 09:15:26 +0000 Subject: [PATCH 14/18] Moved configureTestSSL out of testing API and made it internal. (#2297) --- .../corda/services/messaging/MQSecurityTest.kt | 9 ++++++--- .../corda/services/messaging/SimpleMQClient.kt | 2 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 13 ------------- .../corda/testing/internal/InternalTestUtils.kt | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 5ec0dca2c4..3381d1bcc9 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -18,14 +18,17 @@ import net.corda.core.utilities.toBase58String import net.corda.core.utilities.unwrap import net.corda.node.internal.Node import net.corda.node.internal.StartedNode +import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX -import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.testing.* +import net.corda.nodeapi.internal.config.User +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.chooseIdentity +import net.corda.testing.internal.configureTestSSL import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt index 4257e7e3c0..70d6c90ecb 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt @@ -6,7 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.testing.configureTestSSL +import net.corda.testing.internal.configureTestSSL import org.apache.activemq.artemis.api.core.client.* /** 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 a61744984b..362672ce89 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 @@ -16,8 +16,6 @@ import net.corda.core.internal.cert import net.corda.core.internal.unspecifiedCountry import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType @@ -25,7 +23,6 @@ import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import org.bouncycastle.cert.X509CertificateHolder import java.math.BigInteger -import java.nio.file.Files import java.security.KeyPair import java.security.PublicKey import java.util.concurrent.atomic.AtomicInteger @@ -78,16 +75,6 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List rigorousMock(clazz: Class): T = Mockito.mock(clazz) { } } +fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration { + return object : SSLConfiguration { + override val certificatesDirectory = Files.createTempDirectory("certs") + override val keyStorePassword: String get() = "cordacadevpass" + override val trustStorePassword: String get() = "trustpass" + + init { + configureDevKeyAndTrustStores(legalName) + } + } +} + private val defaultRootCaName = CordaX500Name("Corda Root CA", "R3 Ltd", "London", "GB") private val defaultIntermediateCaName = CordaX500Name("Corda Intermediate CA", "R3 Ltd", "London", "GB") From b8421e502827a754b912197eee8e413b4ec53a89 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Tue, 2 Jan 2018 09:56:26 +0000 Subject: [PATCH 15/18] Increase memory allowance for SIMM demo to pass with external DB on MacOS (#2303) --- samples/simm-valuation-demo/build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 832e2eb283..36b214c4cb 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -69,6 +69,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { notary = [validating : true] p2pPort 10002 cordapps = ["$project.group:finance:$corda_release_version"] + extraConfig = [ + jvmArgs : [ "-Xmx1g"] + ] } node { name "O=Bank A,L=London,C=GB" @@ -77,6 +80,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcPort 10006 cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers + extraConfig = [ + jvmArgs : [ "-Xmx1g"] + ] } node { name "O=Bank B,L=New York,C=US" @@ -85,6 +91,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcPort 10009 cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers + extraConfig = [ + jvmArgs : [ "-Xmx1g"] + ] } node { name "O=Bank C,L=Tokyo,C=JP" @@ -93,6 +102,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcPort 10012 cordapps = ["$project.group:finance:$corda_release_version"] rpcUsers = ext.rpcUsers + extraConfig = [ + jvmArgs : [ "-Xmx1g"] + ] } } From a9f9bf2c0b31dbc33667f0fc8f9a0bc37cecfd2f Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 2 Jan 2018 10:47:57 +0000 Subject: [PATCH 16/18] Adding tests to check EdDSA compatibility with RFC8032 (#2302) --- .../kotlin/net/corda/core/crypto/Crypto.kt | 6 +- .../net/corda/core/crypto/CryptoUtilsTest.kt | 4 +- .../net/corda/core/crypto/EdDSATests.kt | 190 ++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index cd153a18c0..e736628215 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -110,7 +110,11 @@ object Crypto { "ECDSA signature scheme using the secp256r1 (NIST P-256) curve." ) - /** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */ + /** + * EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. + * The actual algorithm is PureEdDSA Ed25519 as defined in https://tools.ietf.org/html/rfc8032 + * Not to be confused with the EdDSA variants, Ed25519ctx and Ed25519ph. + */ @JvmField val EDDSA_ED25519_SHA512 = SignatureScheme( 4, diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index f45fc377fb..d10944bdb8 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -27,8 +27,8 @@ import kotlin.test.* */ class CryptoUtilsTest { - val testString = "Hello World" - val testBytes = testString.toByteArray() + private val testString = "Hello World" + private val testBytes = testString.toByteArray() // key generation test @Test diff --git a/core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt b/core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt new file mode 100644 index 0000000000..2023cc76e5 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt @@ -0,0 +1,190 @@ +package net.corda.core.crypto + +import net.corda.core.utilities.hexToByteArray +import net.corda.core.utilities.toHex +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSASecurityProvider +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec +import org.junit.Test +import java.security.PrivateKey +import java.security.Signature +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +/** + * Testing PureEdDSA Ed25519 using test vectors from https://tools.ietf.org/html/rfc8032#section-7.1 + */ +class EdDSATests { + @Test + fun `PureEdDSA Ed25519 test vectors`() { + val edParams = Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec + + // MESSAGE (length 0 bytes). + val testVector1 = SignatureTestVector( + "9d61b19deffd5a60ba844af492ec2cc4" + + "4449c5697b326919703bac031cae7f60", + "d75a980182b10ab7d54bfed3c964073a" + + "0ee172f3daa62325af021a68f707511a", + "", + "e5564300c360ac729086e2cc806e828a" + + "84877f1eb8e5d974d873e06522490155" + + "5fb8821590a33bacc61e39701cf9b46b" + + "d25bf5f0595bbe24655141438e7a100b" + ) + + // MESSAGE (length 1 byte). + val testVector2 = SignatureTestVector( + "4ccd089b28ff96da9db6c346ec114e0f" + + "5b8a319f35aba624da8cf6ed4fb8a6fb", + "3d4017c3e843895a92b70aa74d1b7ebc" + + "9c982ccf2ec4968cc0cd55f12af4660c", + "72", + "92a009a9f0d4cab8720e820b5f642540" + + "a2b27b5416503f8fb3762223ebdb69da" + + "085ac1e43e15996e458f3613d0f11d8c" + + "387b2eaeb4302aeeb00d291612bb0c00" + ) + + // MESSAGE (length 2 bytes). + val testVector3 = SignatureTestVector( + "c5aa8df43f9f837bedb7442f31dcb7b1" + + "66d38535076f094b85ce3a2e0b4458f7", + "fc51cd8e6218a1a38da47ed00230f058" + + "0816ed13ba3303ac5deb911548908025", + "af82", + "6291d657deec24024827e69c3abe01a3" + + "0ce548a284743a445e3680d7db5ac3ac" + + "18ff9b538d16f290ae67f760984dc659" + + "4a7c15e9716ed28dc027beceea1ec40a" + ) + + // MESSAGE (length 1023 bytes). + val testVector1024 = SignatureTestVector( + "f5e5767cf153319517630f226876b86c" + + "8160cc583bc013744c6bf255f5cc0ee5", + "278117fc144c72340f67d0f2316e8386" + + "ceffbf2b2428c9c51fef7c597f1d426e", + "08b8b2b733424243760fe426a4b54908" + + "632110a66c2f6591eabd3345e3e4eb98" + + "fa6e264bf09efe12ee50f8f54e9f77b1" + + "e355f6c50544e23fb1433ddf73be84d8" + + "79de7c0046dc4996d9e773f4bc9efe57" + + "38829adb26c81b37c93a1b270b20329d" + + "658675fc6ea534e0810a4432826bf58c" + + "941efb65d57a338bbd2e26640f89ffbc" + + "1a858efcb8550ee3a5e1998bd177e93a" + + "7363c344fe6b199ee5d02e82d522c4fe" + + "ba15452f80288a821a579116ec6dad2b" + + "3b310da903401aa62100ab5d1a36553e" + + "06203b33890cc9b832f79ef80560ccb9" + + "a39ce767967ed628c6ad573cb116dbef" + + "efd75499da96bd68a8a97b928a8bbc10" + + "3b6621fcde2beca1231d206be6cd9ec7" + + "aff6f6c94fcd7204ed3455c68c83f4a4" + + "1da4af2b74ef5c53f1d8ac70bdcb7ed1" + + "85ce81bd84359d44254d95629e9855a9" + + "4a7c1958d1f8ada5d0532ed8a5aa3fb2" + + "d17ba70eb6248e594e1a2297acbbb39d" + + "502f1a8c6eb6f1ce22b3de1a1f40cc24" + + "554119a831a9aad6079cad88425de6bd" + + "e1a9187ebb6092cf67bf2b13fd65f270" + + "88d78b7e883c8759d2c4f5c65adb7553" + + "878ad575f9fad878e80a0c9ba63bcbcc" + + "2732e69485bbc9c90bfbd62481d9089b" + + "eccf80cfe2df16a2cf65bd92dd597b07" + + "07e0917af48bbb75fed413d238f5555a" + + "7a569d80c3414a8d0859dc65a46128ba" + + "b27af87a71314f318c782b23ebfe808b" + + "82b0ce26401d2e22f04d83d1255dc51a" + + "ddd3b75a2b1ae0784504df543af8969b" + + "e3ea7082ff7fc9888c144da2af58429e" + + "c96031dbcad3dad9af0dcbaaaf268cb8" + + "fcffead94f3c7ca495e056a9b47acdb7" + + "51fb73e666c6c655ade8297297d07ad1" + + "ba5e43f1bca32301651339e22904cc8c" + + "42f58c30c04aafdb038dda0847dd988d" + + "cda6f3bfd15c4b4c4525004aa06eeff8" + + "ca61783aacec57fb3d1f92b0fe2fd1a8" + + "5f6724517b65e614ad6808d6f6ee34df" + + "f7310fdc82aebfd904b01e1dc54b2927" + + "094b2db68d6f903b68401adebf5a7e08" + + "d78ff4ef5d63653a65040cf9bfd4aca7" + + "984a74d37145986780fc0b16ac451649" + + "de6188a7dbdf191f64b5fc5e2ab47b57" + + "f7f7276cd419c17a3ca8e1b939ae49e4" + + "88acba6b965610b5480109c8b17b80e1" + + "b7b750dfc7598d5d5011fd2dcc5600a3" + + "2ef5b52a1ecc820e308aa342721aac09" + + "43bf6686b64b2579376504ccc493d97e" + + "6aed3fb0f9cd71a43dd497f01f17c0e2" + + "cb3797aa2a2f256656168e6c496afc5f" + + "b93246f6b1116398a346f1a641f3b041" + + "e989f7914f90cc2c7fff357876e506b5" + + "0d334ba77c225bc307ba537152f3f161" + + "0e4eafe595f6d9d90d11faa933a15ef1" + + "369546868a7f3a45a96768d40fd9d034" + + "12c091c6315cf4fde7cb68606937380d" + + "b2eaaa707b4c4185c32eddcdd306705e" + + "4dc1ffc872eeee475a64dfac86aba41c" + + "0618983f8741c5ef68d3a101e8a3b8ca" + + "c60c905c15fc910840b94c00a0b9d0", + "0aab4c900501b3e24d7cdf4663326a3a" + + "87df5e4843b2cbdb67cbf6e460fec350" + + "aa5371b1508f9f4528ecea23c436d94b" + + "5e8fcd4f681e30a6ac00a9704a188a03" + ) + + // MESSAGE (length 64 bytes). TEST SHA(abc). + val testVectorSHAabc = SignatureTestVector( + "833fe62409237b9d62ec77587520911e" + + "9a759cec1d19755b7da901b96dca3d42", + "ec172b93ad5e563bf4932c70e1245034" + + "c35467ef2efd4d64ebf819683467e2bf", + "ddaf35a193617abacc417349ae204131" + + "12e6fa4e89a97ea20a9eeee64b55d39a" + + "2192992a274fc1a836ba3c23a3feebbd" + + "454d4423643ce80e2a9ac94fa54ca49f", + "dc2a4459e7369633a52b1bf277839a00" + + "201009a3efbf3ecb69bea2186c26b589" + + "09351fc9ac90b3ecfdfbc7c66431e030" + + "3dca179c138ac17ad9bef1177331a704" + ) + + val testVectors = listOf(testVector1, testVector2, testVector3, testVector1024, testVectorSHAabc) + testVectors.forEach { + val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(it.privateKeyHex.hexToByteArray(), edParams)) + assertEquals(it.signatureOutputHex, doSign(privateKey, it.messageToSignHex.hexToByteArray()).toHex().toLowerCase()) + } + + // Test vector for the variant Ed25519ctx, expected to fail. + val testVectorEd25519ctx = SignatureTestVector( + "0305334e381af78f141cb666f6199f57" + + "bc3495335a256a95bd2a55bf546663f6", + "dfc9425e4f968f7f0c29f0259cf5f9ae" + + "d6851c2bb4ad8bfb860cfee0ab248292", + "f726936d19c800494e3fdaff20b276a8", + "fc60d5872fc46b3aa69f8b5b4351d580" + + "8f92bcc044606db097abab6dbcb1aee3" + + "216c48e8b3b66431b5b186d1d28f8ee1" + + "5a5ca2df6668346291c2043d4eb3e90d" + ) + + val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(testVectorEd25519ctx.privateKeyHex.hexToByteArray(), edParams)) + assertNotEquals(testVectorEd25519ctx.signatureOutputHex, doSign(privateKey, testVectorEd25519ctx.messageToSignHex.hexToByteArray()).toHex().toLowerCase()) + } + + /** A test vector object for digital signature schemes. */ + private data class SignatureTestVector(val privateKeyHex: String, + val publicKeyHex: String, + val messageToSignHex: String, + val signatureOutputHex: String) + + // Required to implement a custom doSign function, because Corda's Crypto.doSign does not allow empty messages (testVector1). + private fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray { + val signature = Signature.getInstance(Crypto.EDDSA_ED25519_SHA512.signatureName, EdDSASecurityProvider()) + signature.initSign(privateKey) + signature.update(clearData) + return signature.sign() + } +} From fe3c2b39839387f3a655024f26d96bc600b74d50 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 2 Jan 2018 13:11:43 +0000 Subject: [PATCH 17/18] CORDA-891 Refactoring for #2273 (#2306) * Make FlowLogicRefFactoryImpl a class. * Replace instanceof with polymorphism. * Fix out-of-scope spelling error. --- .../net/corda/core/flows/FlowLogicRef.kt | 2 ++ .../net/corda/node/internal/AbstractNode.kt | 27 ++++++++++++------- .../net/corda/node/internal/CordaClock.kt | 10 ++++--- .../kotlin/net/corda/node/internal/Node.kt | 3 +-- .../node/services/api/ServiceHubInternal.kt | 8 +----- .../services/events/NodeSchedulerService.kt | 24 ++++++++--------- .../events/ScheduledActivityObserver.kt | 10 +++---- .../statemachine/FlowLogicRefFactoryImpl.kt | 10 +++---- .../events/FlowLogicRefFromJavaTest.java | 6 +++-- .../node/services/events/FlowLogicRefTest.kt | 17 ++++++------ .../events/NodeSchedulerServiceTest.kt | 5 ++-- .../corda/node/utilities/ClockUtilsTest.kt | 6 +++-- 12 files changed, 69 insertions(+), 59 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index a3043709d0..4768ff9259 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -11,6 +11,8 @@ import net.corda.core.serialization.CordaSerializable @DoNotImplement interface FlowLogicRefFactory { fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef + fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef + fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> } @CordaSerializable 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 c2ef31ae33..064b450dea 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -104,7 +104,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. abstract class AbstractNode(val configuration: NodeConfiguration, - val platformClock: Clock, + val platformClock: CordaClock, protected val versionInfo: VersionInfo, protected val cordappLoader: CordappLoader, private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { @@ -217,14 +217,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) - val flowStarter = FlowStarterImpl(serverThread, smm) + val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) + val flowStarter = FlowStarterImpl(serverThread, smm, flowLogicRefFactory) val schedulerService = NodeSchedulerService( platformClock, database, flowStarter, transactionStorage, unfinishedSchedules = busyNodeLatch, - serverThread = serverThread) + serverThread = serverThread, + flowLogicRefFactory = flowLogicRefFactory) if (serverThread is ExecutorService) { runOnStop += { // We wait here, even though any in-flight messages should have been drained away because the @@ -233,7 +235,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS) } } - makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService) + makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory) val rpcOps = makeRPCOps(flowStarter, database, smm) startMessagingService(rpcOps) installCoreFlows() @@ -241,7 +243,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, tokenizableServices = nodeServices + cordaServices + schedulerService registerCordappFlows(smm) _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } - FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader startShell(rpcOps) Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } @@ -558,10 +559,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage() - - private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService) { + private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService, flowLogicRefFactory: FlowLogicRefFactory) { VaultSoftLockManager.install(services.vaultService, smm) - ScheduledActivityObserver.install(services.vaultService, schedulerService) + ScheduledActivityObserver.install(services.vaultService, schedulerService, flowLogicRefFactory) HibernateObserver.install(services.vaultService.rawUpdates, hibernateConfig, schemaService) } @@ -820,10 +820,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } -internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager) : FlowStarter { +internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager, private val flowLogicRefFactory: FlowLogicRefFactory) : FlowStarter { override fun startFlow(logic: FlowLogic, context: InvocationContext): CordaFuture> { return serverThread.fetchFrom { smm.startFlow(logic, context) } } + + override fun invokeFlowAsync( + logicType: Class>, + context: InvocationContext, + vararg args: Any?): CordaFuture> { + val logicRef = flowLogicRefFactory.createForRPC(logicType, *args) + val logic: FlowLogic = uncheckedCast(flowLogicRefFactory.toFlowLogic(logicRef)) + return startFlow(logic, context) + } } class ConfigurationException(message: String) : CordaException(message) diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt b/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt index cd86529f67..c08961ee77 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt @@ -22,10 +22,15 @@ abstract class CordaClock : Clock(), SerializeAsToken { override fun getZone(): ZoneId = delegateClock.zone @Deprecated("Do not use this. Instead seek to use ZonedDateTime methods.", level = DeprecationLevel.ERROR) override fun withZone(zone: ZoneId) = throw UnsupportedOperationException("Tokenized clock does not support withZone()") + + /** This is an observer on the mutation count of this [Clock], which reflects the occurrence of mutations. */ + abstract val mutations: Observable } @ThreadSafe -class SimpleClock(override val delegateClock: Clock) : CordaClock() +class SimpleClock(override val delegateClock: Clock) : CordaClock() { + override val mutations: Observable = Observable.never() +} /** * An abstract class with helper methods for a type of Clock that might have it's concept of "now" adjusted externally. @@ -38,8 +43,7 @@ abstract class MutableClock(private var _delegateClock: Clock) : CordaClock() { _delegateClock = clock } private val _version = AtomicLong(0L) - /** This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. */ - val mutations: Observable by lazy { + override val mutations: Observable by lazy { Observable.create { subscriber: Subscriber -> if (!subscriber.isUnsubscribed) { mutationObservers.add(subscriber) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 201f31a20b..69faba369f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,7 +2,6 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture -import net.corda.core.context.AuthServiceId import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.uncheckedCast @@ -67,7 +66,7 @@ open class Node(configuration: NodeConfiguration, exitProcess(1) } - private fun createClock(configuration: NodeConfiguration): Clock { + private fun createClock(configuration: NodeConfiguration): CordaClock { return (if (configuration.useTestClock) ::DemoClock else ::SimpleClock)(Clock.systemUTC()) } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index e9794842d6..7a4f2f669d 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo @@ -21,7 +20,6 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -137,11 +135,7 @@ interface FlowStarter { fun invokeFlowAsync( logicType: Class>, context: InvocationContext, - vararg args: Any?): CordaFuture> { - val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args) - val logic: FlowLogic = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef)) - return startFlow(logic, context) - } + vararg args: Any?): CordaFuture> } interface StartedNodeServices : ServiceHubInternal, FlowStarter diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 31badf6ec0..8c18043db7 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -10,6 +10,7 @@ import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap @@ -19,10 +20,10 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace +import net.corda.node.internal.CordaClock import net.corda.node.internal.MutableClock import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.SchedulerService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.PersistentMap import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -55,13 +56,14 @@ import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture * activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on. */ @ThreadSafe -class NodeSchedulerService(private val clock: Clock, +class NodeSchedulerService(private val clock: CordaClock, private val database: CordaPersistence, private val flowStarter: FlowStarter, private val stateLoader: StateLoader, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch(), - private val serverThread: AffinityExecutor) + private val serverThread: AffinityExecutor, + private val flowLogicRefFactory: FlowLogicRefFactory) : SchedulerService, SingletonSerializeAsToken() { companion object { @@ -78,16 +80,12 @@ class NodeSchedulerService(private val clock: Clock, @Suspendable @VisibleForTesting // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name - fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { + fun awaitWithDeadline(clock: CordaClock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { var nanos: Long do { val originalFutureCompleted = makeStrandFriendlySettableFuture(future) - val subscription = if (clock is MutableClock) { - clock.mutations.first().subscribe { - originalFutureCompleted.set(false) - } - } else { - null + val subscription = clock.mutations.first().subscribe { + originalFutureCompleted.set(false) } nanos = (clock.instant() until deadline).toNanos() if (nanos > 0) { @@ -102,7 +100,7 @@ class NodeSchedulerService(private val clock: Clock, // No need to take action as will fall out of the loop due to future.isDone } } - subscription?.unsubscribe() + subscription.unsubscribe() originalFutureCompleted.cancel(false) } while (nanos > 0 && !future.isDone) return future.isDone @@ -279,7 +277,7 @@ class NodeSchedulerService(private val clock: Clock, scheduledStatesQueue.remove(scheduledState) scheduledStatesQueue.add(newState) } else { - val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef) + val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef) log.trace { "Scheduler starting FlowLogic $flowLogic" } scheduledFlow = flowLogic scheduledStates.remove(scheduledState.ref) @@ -297,7 +295,7 @@ class NodeSchedulerService(private val clock: Clock, val state = txState.data as SchedulableState return try { // This can throw as running contract code. - state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl) + state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory) } catch (e: Exception) { log.error("Attempt to run scheduled state $scheduledState resulted in error.", e) null diff --git a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt index 3020a9e528..2c3e939c98 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt @@ -4,19 +4,19 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.SchedulableState import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateAndRef +import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.node.services.VaultService import net.corda.node.services.api.SchedulerService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl /** * This observes the vault and schedules and unschedules activities appropriately based on state production and * consumption. */ -class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService) { +class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService, private val FlowLogicRefFactory: FlowLogicRefFactory) { companion object { @JvmStatic - fun install(vaultService: VaultService, schedulerService: SchedulerService) { - val observer = ScheduledActivityObserver(schedulerService) + fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) { + val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory) vaultService.rawUpdates.subscribe { (consumed, produced) -> consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) } produced.forEach { observer.scheduleStateActivity(it) } @@ -32,7 +32,7 @@ class ScheduledActivityObserver private constructor(private val schedulerService private fun scheduleStateActivity(produced: StateAndRef) { val producedState = produced.state.data if (producedState is SchedulableState) { - val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return + val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactory)?.scheduledAt } ?: return schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index f0f10ebed4..1b20e75c5b 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -31,10 +31,8 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, * measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital" * in response to a potential malicious use or buggy update to an app etc. */ -object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory { - // TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now - var classloader: ClassLoader = javaClass.classLoader - +// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now +class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : SingletonSerializeAsToken(), FlowLogicRefFactory { override fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef { if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) { throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow") @@ -42,7 +40,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor return createForRPC(flowClass, *args) } - fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { + override fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { // TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly // to avoid requiring only a single constructor. val argTypes = args.map { it?.javaClass } @@ -81,7 +79,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor return FlowLogicRefImpl(type.name, args) } - fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { + override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java) return createConstructor(klass, ref.args)() diff --git a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java index 5e0a69cbad..0f83de90cf 100644 --- a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java +++ b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java @@ -48,13 +48,15 @@ public class FlowLogicRefFromJavaTest { } } + private final FlowLogicRefFactoryImpl flowLogicRefFactory = new FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl.class.getClassLoader()); + @Test public void test() { - FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); + flowLogicRefFactory.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); } @Test public void testNoArg() { - FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class); + flowLogicRefFactory.createForRPC(JavaNoArgFlowLogic.class); } } diff --git a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt index ae6a5b7680..3794a39ebf 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt @@ -34,47 +34,48 @@ class FlowLogicRefTest { override fun call() = Unit } + private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader) @Test fun `create kotlin no arg`() { - FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java) + flowLogicRefFactory.createForRPC(KotlinNoArgFlowLogic::class.java) } @Test fun `create kotlin`() { val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack"))) - FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args) } @Test fun `create primary`() { - FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) + flowLogicRefFactory.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) } @Test fun `create kotlin void`() { - FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap()) + flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, emptyMap()) } @Test fun `create kotlin non primary`() { val args = mapOf(Pair("C", ParamType2("Hello Jack"))) - FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args) } @Test fun `create java primitive no registration required`() { val args = mapOf(Pair("primitive", "A string")) - FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args) } @Test fun `create kotlin primitive no registration required`() { val args = mapOf(Pair("kotlinType", 3)) - FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args) } @Test(expected = IllegalFlowLogicException::class) fun `create for non-schedulable flow logic`() { - FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java) + flowLogicRefFactory.create(NonSchedulableFlow::class.java) } } diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index d2998cf641..f771b5a197 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -63,6 +63,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader) private val realClock: Clock = Clock.systemUTC() private val stoppedClock: Clock = Clock.fixed(realClock.instant(), realClock.zone) private val testClock = TestClock(stoppedClock) @@ -121,7 +122,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) mockSMM = StateMachineManagerImpl(services, DBCheckpointStorage(), smmExecutor, database) - scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor) + scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM, flowLogicRefFactory), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor, flowLogicRefFactory = flowLogicRefFactory) mockSMM.changes.subscribe { change -> if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) { smmHasRemovedAllFlows.countDown() @@ -304,7 +305,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { apply { val freshKey = kms.freshKey() - val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party) + val state = TestState(flowLogicRefFactory.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party) val builder = TransactionBuilder(null).apply { addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY) addCommand(Command(), freshKey) diff --git a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt index 783564a664..d8dbfac65f 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt @@ -8,6 +8,8 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.hours import net.corda.core.utilities.minutes +import net.corda.node.internal.CordaClock +import net.corda.node.internal.SimpleClock import net.corda.node.services.events.NodeSchedulerService import net.corda.testing.node.TestClock import org.junit.After @@ -25,13 +27,13 @@ import kotlin.test.fail class ClockUtilsTest { lateinit var realClock: Clock - lateinit var stoppedClock: Clock + lateinit var stoppedClock: CordaClock lateinit var executor: ExecutorService @Before fun setup() { realClock = Clock.systemUTC() - stoppedClock = Clock.fixed(realClock.instant(), realClock.zone) + stoppedClock = SimpleClock(Clock.fixed(realClock.instant(), realClock.zone)) executor = Executors.newSingleThreadExecutor() } From 730fec2eb4e32bb473f3ede6ed543336a10d3312 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 2 Jan 2018 15:12:30 +0000 Subject: [PATCH 18/18] Internal driver now also does the registration for the notaries. (#2304) Using the --just-generate-node-info flag for the notary nodes so that their identities can be submitted to the network map server, which does the network parameters generation. --- .../nodeapi/internal/DevIdentityGenerator.kt | 16 +- .../nodeapi/internal/crypto/X509Utilities.kt | 7 +- .../internal/crypto/X509UtilitiesTest.kt | 10 +- .../node/services/network/NetworkMapTest.kt | 88 ++-- .../registration/NodeRegistrationTest.kt | 56 +-- .../net/corda/node/internal/AbstractNode.kt | 5 +- .../kotlin/net/corda/node/internal/Node.kt | 17 +- .../net/corda/node/internal/NodeStartup.kt | 3 +- .../services/network/NetworkMapClientTest.kt | 7 +- .../kotlin/net/corda/testing/driver/Driver.kt | 7 +- .../testing/node/internal/DriverDSLImpl.kt | 405 +++++++++++------- .../corda/testing/node/internal/RPCDriver.kt | 3 +- .../node/internal/network/NetworkMapServer.kt | 4 +- .../net/corda/verifier/VerifierDriver.kt | 3 +- 14 files changed, 370 insertions(+), 261 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index 484ab18116..ef58a930b2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -14,7 +14,6 @@ import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.crypto.* import org.slf4j.LoggerFactory import java.nio.file.Path -import java.security.cert.X509Certificate /** * Contains utility methods for generating identities for a node. @@ -29,11 +28,8 @@ object DevIdentityGenerator { const val NODE_IDENTITY_ALIAS_PREFIX = "identity" const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" - /** - * Install a node key store for the given node directory using the given legal name and an optional root cert. If no - * root cert is specified then the default one in certificates/cordadevcakeys.jks is used. - */ - fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party { + /** Install a node key store for the given node directory using the given legal name. */ + fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party { val nodeSslConfig = object : NodeSSLConfiguration { override val baseDirectory = nodeDir override val keyStorePassword: String = "cordacadevpass" @@ -43,8 +39,7 @@ object DevIdentityGenerator { // TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") - // TODO If using a custom root cert, then the intermidate cert needs to be generated from it as well, and not taken from the default - val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) nodeSslConfig.certificatesDirectory.createDirectories() nodeSslConfig.createDevKeyStores(rootCert.toX509CertHolder(), intermediateCa, legalName) @@ -54,7 +49,7 @@ object DevIdentityGenerator { return identity.party } - fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party { + fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1): Party { require(dirs.isNotEmpty()) log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" } @@ -63,8 +58,7 @@ object DevIdentityGenerator { val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") - // TODO If using a custom root cert, then the intermidate cert needs to be generated from it as well, and not taken from the default - val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) keyPairs.zip(dirs) { keyPair, nodeDir -> val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey -> diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 563ddaa9f0..78b44a8786 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -4,12 +4,8 @@ import net.corda.core.CordaOID import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureScheme import net.corda.core.crypto.random63BitValue -import net.corda.core.internal.CertRole import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.cert -import net.corda.core.internal.reader -import net.corda.core.internal.writer -import net.corda.core.internal.x500Name +import net.corda.core.internal.* import net.corda.core.utilities.days import net.corda.core.utilities.millis import org.bouncycastle.asn1.* @@ -43,6 +39,7 @@ object X509Utilities { val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512 val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 + // TODO This class is more of a general purpose utility class and as such these constants belong elsewhere // Aliases for private keys and certificates. const val CORDA_ROOT_CA = "cordarootca" const val CORDA_INTERMEDIATE_CA = "cordaintermediateca" diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index f078035241..47c2c8b9ef 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -170,10 +170,10 @@ class X509UtilitiesTest { override val trustStorePassword = "trustpass" } - val (rootCert, intermediateCa) = createDevIntermediateCaCertPath() + val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(rootCert.certificate, intermediateCa, MEGA_CORP.name) + sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name) // Load back server certificate val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword) @@ -206,11 +206,11 @@ class X509UtilitiesTest { override val trustStorePassword = "trustpass" } - val (rootCert, intermediateCa) = createDevIntermediateCaCertPath() + val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(rootCert.certificate, intermediateCa, MEGA_CORP.name) - sslConfig.createTrustStore(rootCert.certificate.cert) + sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name) + sslConfig.createTrustStore(rootCa.certificate.cert) val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 3f9aade18c..5530ba7850 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -1,6 +1,11 @@ package net.corda.node.services.network +import net.corda.cordform.CordformNode import net.corda.core.crypto.SignedData +import net.corda.core.crypto.random63BitValue +import net.corda.core.internal.concurrent.transpose +import net.corda.core.internal.div +import net.corda.core.internal.exists import net.corda.core.internal.list import net.corda.core.internal.readAll import net.corda.core.node.NodeInfo @@ -10,9 +15,9 @@ import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.testing.ALICE_NAME -import net.corda.testing.ROOT_CA import net.corda.testing.BOB_NAME import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.node.internal.CompatibilityZoneParams @@ -24,12 +29,15 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import java.net.URL +import java.time.Instant +import kotlin.streams.toList import kotlin.test.assertEquals class NetworkMapTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) + private val cacheTimeout = 1.seconds private val portAllocation = PortAllocation.Incremental(10000) @@ -40,7 +48,9 @@ class NetworkMapTest { fun start() { networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort()) val address = networkMapServer.start() - compatibilityZone = CompatibilityZoneParams(URL("http://$address")) + compatibilityZone = CompatibilityZoneParams(URL("http://$address"), publishNotaries = { + networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue())) + }) } @After @@ -50,27 +60,35 @@ class NetworkMapTest { @Test fun `node correctly downloads and saves network parameters file on startup`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, - initialiseSerialization = false, notarySpecs = emptyList()) { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + initialiseSerialization = false, + notarySpecs = emptyList() + ) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val networkParameters = alice.configuration.baseDirectory - .list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() } + val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME) .readAll() .deserialize>() .verified() + // We use a random modified time above to make the network parameters unqiue so that we're sure they came + // from the server assertEquals(networkMapServer.networkParameters, networkParameters) } } @Test fun `nodes can see each other using the http network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, - initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { - val alice = startNode(providedName = ALICE_NAME) - val bob = startNode(providedName = BOB_NAME) - val notaryNode = defaultNotaryNode.get() - val aliceNode = alice.get() - val bobNode = bob.get() + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + initialiseSerialization = false + ) { + val (aliceNode, bobNode, notaryNode) = listOf( + startNode(providedName = ALICE_NAME), + startNode(providedName = BOB_NAME), + defaultNotaryNode + ).transpose().getOrThrow() notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) @@ -80,16 +98,20 @@ class NetworkMapTest { @Test fun `nodes process network map add updates correctly when adding new node to network map`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, - initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { - val alice = startNode(providedName = ALICE_NAME) - val notaryNode = defaultNotaryNode.get() - val aliceNode = alice.get() + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + initialiseSerialization = false + ) { + val (aliceNode, notaryNode) = listOf( + startNode(providedName = ALICE_NAME), + defaultNotaryNode + ).transpose().getOrThrow() notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo) aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo) - val bob = startNode(providedName = BOB_NAME) - val bobNode = bob.get() + + val bobNode = startNode(providedName = BOB_NAME).getOrThrow() // Wait for network map client to poll for the next update. Thread.sleep(cacheTimeout.toMillis() * 2) @@ -102,13 +124,16 @@ class NetworkMapTest { @Test fun `nodes process network map remove updates correctly`() { - internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, - initialiseSerialization = false, onNetworkParametersGeneration = { networkMapServer.networkParameters = it }) { - val alice = startNode(providedName = ALICE_NAME) - val bob = startNode(providedName = BOB_NAME) - val notaryNode = defaultNotaryNode.get() - val aliceNode = alice.get() - val bobNode = bob.get() + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + initialiseSerialization = false + ) { + val (aliceNode, bobNode, notaryNode) = listOf( + startNode(providedName = ALICE_NAME), + startNode(providedName = BOB_NAME), + defaultNotaryNode + ).transpose().getOrThrow() notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo) @@ -124,5 +149,12 @@ class NetworkMapTest { } } - private fun NodeHandle.onlySees(vararg nodes: NodeInfo) = assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) + private fun NodeHandle.onlySees(vararg nodes: NodeInfo) { + // Make sure the nodes aren't getting the node infos from their additional directories + val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY + if (nodeInfosDir.exists()) { + assertThat(nodeInfosDir.list { it.toList() }).isEmpty() + } + assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) + } } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index c7f0e2c1e1..2fe371202a 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -21,6 +21,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.testing.ROOT_CA import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.CompatibilityZoneParams @@ -31,10 +32,7 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test +import org.junit.* import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.URL @@ -49,6 +47,12 @@ import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response class NodeRegistrationTest { + companion object { + private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH") + private val aliceName = CordaX500Name("Alice", "London", "GB") + private val genevieveName = CordaX500Name("Genevieve", "London", "GB") + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) @@ -72,37 +76,30 @@ class NodeRegistrationTest { @Test fun `node registration correct root cert`() { - val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = ROOT_CA.certificate.cert) + val compatibilityZone = CompatibilityZoneParams( + URL("http://$serverHostAndPort"), + publishNotaries = { server.networkParameters = testNetworkParameters(it) }, + rootCert = ROOT_CA.certificate.cert) internalDriver( portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false, - notarySpecs = listOf(NotarySpec(CordaX500Name("NotaryService", "Zurich", "CH"), validating = false)), - extraCordappPackagesToScan = listOf("net.corda.finance"), - onNetworkParametersGeneration = { server.networkParameters = it } + notarySpecs = listOf(NotarySpec(notaryName)), + extraCordappPackagesToScan = listOf("net.corda.finance") ) { - val aliceName = "Alice" - val genevieveName = "Genevieve" - val nodes = listOf( - startNode(providedName = CordaX500Name(aliceName, "London", "GB")), - startNode(providedName = CordaX500Name(genevieveName, "London", "GB")), + startNode(providedName = aliceName), + startNode(providedName = genevieveName), defaultNotaryNode ).transpose().getOrThrow() val (alice, genevieve) = nodes - assertThat(registrationHandler.idsPolled).contains(aliceName, genevieveName) - // Notary identities are generated beforehand hence notary nodes don't go through registration. - // This test isn't specifically testing this, or relying on this behavior, though if this check fail, - // this will probably lead to the rest of the test to fail. - assertThat(registrationHandler.idsPolled).doesNotContain("NotaryService") + assertThat(registrationHandler.idsPolled).containsOnly( + aliceName.organisation, + genevieveName.organisation, + notaryName.organisation) - // Check each node has each other identity in their network map cache. - for (node in nodes) { - assertThat(node.rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo }) - } - - // Check we nodes communicate among themselves (and the notary). + // Check the nodes can communicate among themselves (and the notary). val anonymous = false genevieve.rpc.startFlow( ::CashIssueAndPaymentFlow, @@ -120,20 +117,22 @@ class NodeRegistrationTest { val someRootCert = X509Utilities.createSelfSignedCACertificate( CordaX500Name("Integration Test Corda Node Root CA", "R3 Ltd", "London", "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someRootCert.cert) + val compatibilityZone = CompatibilityZoneParams( + URL("http://$serverHostAndPort"), + publishNotaries = { server.networkParameters = testNetworkParameters(it) }, + rootCert = someRootCert.cert) internalDriver( portAllocation = portAllocation, - notarySpecs = emptyList(), compatibilityZone = compatibilityZone, initialiseSerialization = false, + notarySpecs = listOf(NotarySpec(notaryName)), startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception ) { assertThatThrownBy { - startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() + defaultNotaryNode.getOrThrow() }.isInstanceOf(CertPathValidatorException::class.java) } } - } @Path("certificate") @@ -150,6 +149,7 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair) certificationRequest, rootCertAndKeyPair.keyPair, arrayOf(rootCertAndKeyPair.certificate.cert)) + require(!name.organisation.contains("\\s".toRegex())) { "Whitespace in the organisation name not supported" } certPaths[name.organisation] = certPath return Response.ok(name.organisation).build() } 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 064b450dea..c924c42a91 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -180,13 +180,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return SignedNodeInfo(serialised, listOf(signature)) } - open fun generateNodeInfo() { + open fun generateAndSaveNodeInfo(): NodeInfo { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") initCertificate() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) - initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database -> + return initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database -> // TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like // a code smell. val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) @@ -196,6 +196,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, privateKey.sign(serialised.bytes) } NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) + info } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 69faba369f..16dae0ee2e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -19,7 +19,9 @@ import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.api.SchemaService -import net.corda.node.services.config.* +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.SecurityConfiguration +import net.corda.node.services.config.VerifierType import net.corda.node.services.messaging.* import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AddressUtils @@ -32,6 +34,7 @@ import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import org.slf4j.Logger import org.slf4j.LoggerFactory +import rx.Scheduler import rx.schedulers.Schedulers import java.time.Clock import java.util.concurrent.atomic.AtomicInteger @@ -264,15 +267,13 @@ open class Node(configuration: NodeConfiguration, private val _startupComplete = openFuture() val startupComplete: CordaFuture get() = _startupComplete - override fun generateNodeInfo() { + override fun generateAndSaveNodeInfo(): NodeInfo { initialiseSerialization() - super.generateNodeInfo() + return super.generateAndSaveNodeInfo() } override fun start(): StartedNode { - if (initialiseSerialization) { - initialiseSerialization() - } + initialiseSerialization() val started: StartedNode = uncheckedCast(super.start()) nodeReadyFuture.thenMatch({ serverThread.execute { @@ -305,8 +306,10 @@ open class Node(configuration: NodeConfiguration, return started } - override fun getRxIoScheduler() = Schedulers.io()!! + override fun getRxIoScheduler(): Scheduler = Schedulers.io() + private fun initialiseSerialization() { + if (!initialiseSerialization) return val classloader = cordappLoader.appClassLoader nodeSerializationEnv = SerializationEnvironmentImpl( SerializationFactoryImpl().apply { diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 6eabdb1d82..7f3d45ffea 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -1,7 +1,6 @@ package net.corda.node.internal import com.jcabi.manifests.Manifests -import com.typesafe.config.ConfigException import joptsimple.OptionException import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch @@ -126,7 +125,7 @@ open class NodeStartup(val args: Array) { val node = createNode(conf, versionInfo) if (cmdlineOptions.justGenerateNodeInfo) { // Perform the minimum required start-up logic to be able to write a nodeInfo to disk - node.generateNodeInfo() + node.generateAndSaveNodeInfo() return } val startedNode = node.start() 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 30819a16ef..c609d59f2c 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,7 +5,10 @@ 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.testing.* +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 @@ -30,7 +33,7 @@ class NetworkMapClientTest { @Before fun setUp() { - server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort(), root_ca = ROOT_CA) + server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort()) val hostAndPort = server.start() networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_TRUST_ROOT.cert) } 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 cdcf9d966d..577a00b479 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 @@ -14,11 +14,11 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.nodeapi.internal.config.User +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.genericDriver import net.corda.testing.node.internal.getTimestampAsDirectoryName -import net.corda.testing.DUMMY_NOTARY_NAME -import net.corda.testing.node.NotarySpec import java.net.InetSocketAddress import java.net.ServerSocket import java.nio.file.Path @@ -193,8 +193,7 @@ fun driver( notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, - compatibilityZone = null, - onNetworkParametersGeneration = { } + compatibilityZone = null ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index bdd14a5278..e7b65d2572 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -11,13 +11,11 @@ import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.ThreadBox +import net.corda.core.internal.* import net.corda.core.internal.concurrent.* -import net.corda.core.internal.copyTo -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.NetworkMapCache +import net.corda.core.serialization.deserialize import net.corda.core.toFuture import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger @@ -31,6 +29,7 @@ import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -39,7 +38,6 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.nodeapi.internal.crypto.save -import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NotaryInfo @@ -89,24 +87,23 @@ class DriverDSLImpl( extraCordappPackagesToScan: List, val jmxPolicy: JmxPolicy, val notarySpecs: List, - val compatibilityZone: CompatibilityZoneParams?, - val onNetworkParametersGeneration: (NetworkParameters) -> Unit + val compatibilityZone: CompatibilityZoneParams? ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! private var _shutdownManager: ShutdownManager? = null override val shutdownManager get() = _shutdownManager!! private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() - // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ - // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. - // Investigate whether we can avoid that. - private var nodeInfoFilesCopier: NodeInfoFilesCopier? = null // Map from a nodes legal name to an observable emitting the number of nodes in its network map. private val countObservables = mutableMapOf>() - private lateinit var _notaries: List - override val notaryHandles: List get() = _notaries - private var networkParametersCopier: NetworkParametersCopier? = null - lateinit var networkParameters: NetworkParameters + /** + * Future which completes when the network map is available, whether a local one or one from the CZ. This future acts + * as a gate to prevent nodes from starting too early. The value of the future is a [LocalNetworkMap] object, which + * is null if the network map is being provided by the CZ. + */ + private lateinit var networkMapAvailability: CordaFuture + private lateinit var _notaries: CordaFuture> + override val notaryHandles: List get() = _notaries.getOrThrow() class State { val processes = ArrayList() @@ -147,12 +144,12 @@ class DriverDSLImpl( _executorService?.shutdownNow() } - private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture): CordaFuture { - val rpcAddress = config.rpcAddress!! + private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { + val rpcAddress = config.corda.rpcAddress!! val client = CordaRPCClient(rpcAddress) val connectionFuture = poll(executorService, "RPC connection") { try { - client.start(config.rpcUsers[0].username, config.rpcUsers[0].password) + config.corda.rpcUsers[0].run { client.start(username, password) } } catch (e: Exception) { if (processDeathFuture.isDone) throw e log.error("Exception $e, Retrying RPC connection at $rpcAddress") @@ -180,65 +177,74 @@ class DriverDSLImpl( ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") - val isNotary = name in notarySpecs.map { it.name } - val registrationFuture = if (compatibilityZone?.rootCert != null && !isNotary) { - nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url) + val name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") + val registrationFuture = if (compatibilityZone?.rootCert != null) { + // We don't need the network map to be available to be able to register the node + startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url) } else { doneFuture(Unit) } return registrationFuture.flatMap { - val rpcAddress = portAllocation.nextHostAndPort() - val webAddress = portAllocation.nextHostAndPort() - val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } - val configMap = configOf( - "myLegalName" to name.toString(), - "p2pAddress" to p2pAddress.toString(), - "rpcAddress" to rpcAddress.toString(), - "webAddress" to webAddress.toString(), - "useTestClock" to useTestClock, - "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, - "verifierType" to verifierType.name - ) + customOverrides - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), - allowMissingConfig = true, - configOverrides = if (compatibilityZone != null) { - configMap + mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) - } else { - configMap - } - ) - startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) + networkMapAvailability.flatMap { + // But starting the node proper does require the network map + startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress) + } } } - private fun nodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { + private fun startRegisteredNode(name: CordaX500Name, + localNetworkMap: LocalNetworkMap?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean? = null, + maximumHeapSize: String = "200m", + p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort()): CordaFuture { + val rpcAddress = portAllocation.nextHostAndPort() + val webAddress = portAllocation.nextHostAndPort() + val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } + val czUrlConfig = if (compatibilityZone != null) mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) else emptyMap() + val config = NodeConfig(ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = configOf( + "myLegalName" to name.toString(), + "p2pAddress" to p2pAddress.toString(), + "rpcAddress" to rpcAddress.toString(), + "webAddress" to webAddress.toString(), + "useTestClock" to useTestClock, + "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + "verifierType" to verifierType.name + ) + czUrlConfig + customOverrides + )) + return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap) + } + + private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { val baseDirectory = baseDirectory(providedName).createDirectories() - val config = ConfigHelper.loadConfig( + val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = configOf( "p2pAddress" to "localhost:1222", // required argument, not really used "compatibilityZoneURL" to compatibilityZoneURL.toString(), "myLegalName" to providedName.toString()) - ) - val configuration = config.parseAsNodeConfiguration() + )) - configuration.trustStoreFile.parent.createDirectories() - loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also { - it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) - it.save(configuration.trustStoreFile, configuration.trustStorePassword) + config.corda.certificatesDirectory.createDirectories() + loadOrCreateKeyStore(config.corda.trustStoreFile, config.corda.trustStorePassword).apply { + addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + save(config.corda.trustStoreFile, config.corda.trustStorePassword) } return if (startNodesInProcess) { - // This is a bit cheating, we're not starting a full node, we're just calling the code nodes call - // when registering. - NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() - doneFuture(Unit) + executorService.fork { + NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() + config + } } else { - startOutOfProcessNodeRegistration(config, configuration) + startOutOfProcessMiniNode(config, "--initial-registration").map { config } } } @@ -249,7 +255,9 @@ class DriverDSLImpl( } internal fun startCordformNodes(cordforms: List): CordaFuture<*> { - check(compatibilityZone == null) { "Cordform nodes should be run without compatibilityZone configuration" } + check(notarySpecs.isEmpty()) { "Specify notaries in the CordformDefinition" } + check(compatibilityZone == null) { "Cordform nodes cannot be run with compatibilityZoneURL" } + val clusterNodes = HashMultimap.create() val notaryInfos = ArrayList() @@ -281,12 +289,10 @@ class DriverDSLImpl( notaryInfos += NotaryInfo(identity, type.validating) } - networkParameters = testNetworkParameters(notaryInfos) - onNetworkParametersGeneration(networkParameters) - networkParametersCopier = NetworkParametersCopier(networkParameters) + val localNetworkMap = LocalNetworkMap(notaryInfos) return cordforms.map { - val startedNode = startCordformNode(it) + val startedNode = startCordformNode(it, localNetworkMap) if (it.webAddress != null) { // Start a webserver if an address for it was specified startedNode.flatMap { startWebserver(it) } @@ -296,21 +302,21 @@ class DriverDSLImpl( }.transpose() } - private fun startCordformNode(cordform: CordformNode): CordaFuture { + private fun startCordformNode(cordform: CordformNode, localNetworkMap: LocalNetworkMap): CordaFuture { val name = CordaX500Name.parse(cordform.name) // TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap() val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() val rpcUsers = cordform.rpcUsers - val config = ConfigHelper.loadConfig( + val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, configOverrides = cordform.config + rpcAddress + notary + mapOf( "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers ) - ) - return startNodeInternal(config, webAddress, null, "200m") + )) + return startNodeInternal(config, webAddress, null, "200m", localNetworkMap) } private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle { @@ -344,33 +350,80 @@ class DriverDSLImpl( } _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) - if (compatibilityZone == null) { - // Without a compatibility zone URL we have to copy the node info files ourselves to make sure the nodes see each other - nodeInfoFilesCopier = NodeInfoFilesCopier().also { - shutdownManager.registerShutdown(it::close) + val notaryInfosFuture = if (compatibilityZone == null) { + // If no CZ is specified then the driver does the generation of the network parameters and the copying of the + // node info files. + startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos)) } + } else { + // Otherwise it's the CZ's job to distribute thse via the HTTP network map, as that is what the nodes will be expecting. + val notaryInfosFuture = if (compatibilityZone.rootCert == null) { + // No root cert specified so we use the dev root cert to generate the notary identities. + startNotaryIdentityGeneration() + } else { + // With a root cert specified we delegate generation of the notary identities to the CZ. + startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.url) + } + notaryInfosFuture.map { notaryInfos -> + compatibilityZone.publishNotaries(notaryInfos) + Pair(notaryInfos, null) + } + } + + networkMapAvailability = notaryInfosFuture.map { it.second } + + _notaries = notaryInfosFuture.map { (notaryInfos, localNetworkMap) -> + val listOfFutureNodeHandles = startNotaries(localNetworkMap) + notaryInfos.zip(listOfFutureNodeHandles) { (identity, validating), nodeHandlesFuture -> + NotaryHandle(identity, validating, nodeHandlesFuture) } } - val notaryInfos = generateNotaryIdentities() - networkParameters = testNetworkParameters(notaryInfos) - onNetworkParametersGeneration(networkParameters) - // The network parameters must be serialised before starting any of the nodes - networkParametersCopier = NetworkParametersCopier(networkParameters) - val nodeHandles = startNotaries() - _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } } - private fun generateNotaryIdentities(): List { - return notarySpecs.map { spec -> - val identity = if (spec.cluster == null) { - DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) - } else { - DevIdentityGenerator.generateDistributedNotaryIdentity( - dirs = generateNodeNames(spec).map { baseDirectory(it) }, - notaryName = spec.name, - customRootCert = compatibilityZone?.rootCert - ) + private fun startNotaryIdentityGeneration(): CordaFuture> { + return executorService.fork { + notarySpecs.map { spec -> + val identity = if (spec.cluster == null) { + DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name) + } else { + DevIdentityGenerator.generateDistributedNotaryIdentity( + dirs = generateNodeNames(spec).map { baseDirectory(it) }, + notaryName = spec.name + ) + } + NotaryInfo(identity, spec.validating) + } + } + } + + private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture> { + // Start the registration process for all the notaries together then wait for their responses. + return notarySpecs.map { spec -> + require(spec.cluster == null) { "Registering distributed notaries not supported" } + startNotaryRegistration(spec, rootCert, compatibilityZoneURL) + }.transpose() + } + + private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { + return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config -> + // Node registration only gives us the node CA cert, not the identity cert. That is only created on first + // startup or when the node is told to just generate its node info file. We do that here. + if (startNodesInProcess) { + executorService.fork { + val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo() + NotaryInfo(nodeInfo.legalIdentities[0], spec.validating) + } + } else { + // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper + // This causes two node info files to be generated. + startOutOfProcessMiniNode(config, "--just-generate-node-info").map { + // Once done we have to read the signed node info file that's been generated + val nodeInfoFile = config.corda.baseDirectory.list { paths -> + paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() + } + val nodeInfo = nodeInfoFile.readAll().deserialize().verified() + NotaryInfo(nodeInfo.legalIdentities[0], spec.validating) + } } - NotaryInfo(identity, spec.validating) } } @@ -378,11 +431,11 @@ class DriverDSLImpl( return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") } } - private fun startNotaries(): List>> { + private fun startNotaries(localNetworkMap: LocalNetworkMap?): List>> { return notarySpecs.map { when { - it.cluster == null -> startSingleNotary(it) - it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it) + it.cluster == null -> startSingleNotary(it, localNetworkMap) + it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap) else -> throw IllegalArgumentException("BFT-SMaRt not supported") } } @@ -392,16 +445,17 @@ class DriverDSLImpl( // generating the configs for the nodes, probably making use of Any.toConfig() private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) - private fun startSingleNotary(spec: NotarySpec): CordaFuture> { - return startNode( - providedName = spec.name, - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, + private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture> { + return startRegisteredNode( + spec.name, + localNetworkMap, + spec.rpcUsers, + spec.verifierType, customOverrides = NotaryConfig(spec.validating).toConfigMap() ).map { listOf(it) } } - private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture> { + private fun startRaftNotaryCluster(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture> { fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() val config = NotaryConfig( @@ -414,10 +468,11 @@ class DriverDSLImpl( val clusterAddress = portAllocation.nextHostAndPort() // Start the first node that will bootstrap the cluster - val firstNodeFuture = startNode( - providedName = nodeNames[0], - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, + val firstNodeFuture = startRegisteredNode( + nodeNames[0], + localNetworkMap, + spec.rpcUsers, + spec.verifierType, customOverrides = notaryConfig(clusterAddress) + mapOf( "database.serverNameTablePrefix" to nodeNames[0].toString().replace(Regex("[^0-9A-Za-z]+"), "") ) @@ -426,10 +481,11 @@ class DriverDSLImpl( // All other nodes will join the cluster val restNodeFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() - startNode( - providedName = it, - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, + startRegisteredNode( + it, + localNetworkMap, + spec.rpcUsers, + spec.verifierType, customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf( "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "") ) @@ -446,8 +502,6 @@ class DriverDSLImpl( return driverDirectory / nodeDirectoryName } - override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) - /** * @param initial number of nodes currently in the network map of a running node. * @param networkMapCacheChangeObservable an observable returning the updates to the node network map. @@ -457,7 +511,7 @@ class DriverDSLImpl( private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable): ConnectableObservable { val count = AtomicInteger(initial) - return networkMapCacheChangeObservable.map { it -> + return networkMapCacheChangeObservable.map { when (it) { is NetworkMapCache.MapChange.Added -> count.incrementAndGet() is NetworkMapCache.MapChange.Removed -> count.decrementAndGet() @@ -492,35 +546,44 @@ class DriverDSLImpl( return future } - private fun startOutOfProcessNodeRegistration(config: Config, configuration: NodeConfiguration): CordaFuture { + /** + * Start the node with the given flag which is expected to start the node for some function, which once complete will + * terminate the node. + */ + private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null - val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, - systemProperties, cordappPackages, "200m", initialRegistration = true) + val process = startOutOfProcessNode( + config, + quasarJarPath, + debugPort, + jolokiaJarPath, + monitorPort, + systemProperties, + cordappPackages, + "200m", + extraCmdLineFlag + ) - return poll(executorService, "node registration (${configuration.myLegalName})") { + return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") { if (process.isAlive) null else Unit } } - private fun startNodeInternal(config: Config, + private fun startNodeInternal(config: NodeConfig, webAddress: NetworkHostAndPort, startInProcess: Boolean?, - maximumHeapSize: String): CordaFuture { - val configuration = config.parseAsNodeConfiguration() - val baseDirectory = configuration.baseDirectory.createDirectories() - nodeInfoFilesCopier?.addConfig(baseDirectory) - if (configuration.myLegalName in networkParameters.notaries.map { it.identity.name } || compatibilityZone == null) { - // If a notary is being started, then its identity appears in networkParameters (generated by Driver), - // and Driver itself must pass the network parameters to the notary. - networkParametersCopier?.install(baseDirectory) - } + maximumHeapSize: String, + localNetworkMap: LocalNetworkMap?): CordaFuture { + val baseDirectory = config.corda.baseDirectory.createDirectories() + localNetworkMap?.networkParametersCopier?.install(baseDirectory) + localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory) val onNodeExit: () -> Unit = { - nodeInfoFilesCopier?.removeConfig(baseDirectory) - countObservables.remove(configuration.myLegalName) + localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory) + countObservables.remove(config.corda.myLegalName) } if (startInProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages) + val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages) shutdownManager.registerShutdown( nodeAndThreadFuture.map { (node, thread) -> { @@ -530,16 +593,16 @@ class DriverDSLImpl( } ) return nodeAndThreadFuture.flatMap { (node, thread) -> - establishRpc(configuration, openFuture()).flatMap { rpc -> + establishRpc(config, openFuture()).flatMap { rpc -> allNodesConnected(rpc).map { - NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit) + NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit) } } } } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null - val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, initialRegistration = false) + val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null) if (waitForNodesToFinish) { state.locked { processes += process @@ -547,22 +610,21 @@ class DriverDSLImpl( } else { shutdownManager.registerProcessShutdown(process) } - val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process) + val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process) return p2pReadyFuture.flatMap { - val processDeathFuture = poll(executorService, "process death while waiting for RPC (${configuration.myLegalName})") { + val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") { if (process.isAlive) null else process } - establishRpc(configuration, processDeathFuture).flatMap { rpc -> + establishRpc(config, processDeathFuture).flatMap { rpc -> // Check for all nodes to have all other nodes in background in case RPC is failing over: val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { - throw ListenProcessDeathException(configuration.p2pAddress, process) + throw ListenProcessDeathException(config.corda.p2pAddress, process) } processDeathFuture.cancel(false) log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress") - NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process, - onNodeExit) + NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit) } } } @@ -575,6 +637,25 @@ class DriverDSLImpl( return pollFuture } + /** + * The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories. + */ + private inner class LocalNetworkMap(notaryInfos: List) { + val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ + // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. + // Investigate whether we can avoid that. + val nodeInfosCopier = NodeInfoFilesCopier().also { shutdownManager.registerShutdown(it::close) } + } + + /** + * Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration]. + * Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration]. + */ + private class NodeConfig(val typesafe: Config) { + val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration() + } + companion object { internal val log = contextLogger() @@ -602,28 +683,26 @@ class DriverDSLImpl( private fun startInProcessNode( executorService: ScheduledExecutorService, - nodeConf: NodeConfiguration, - config: Config, + config: NodeConfig, cordappPackages: List ): CordaFuture, Thread>> { return executorService.fork { - log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") + log.info("Starting in-process Node ${config.corda.myLegalName.organisation}") // Write node.conf - writeConfig(nodeConf.baseDirectory, "node.conf", config) + writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe) // TODO pass the version in? - val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start() - val nodeThread = thread(name = nodeConf.myLegalName.organisation) { + val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start() + val nodeThread = thread(name = config.corda.myLegalName.organisation) { node.internals.run() } node to nodeThread }.flatMap { nodeAndThread -> - addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } + addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread } } } private fun startOutOfProcessNode( - nodeConf: NodeConfiguration, - config: Config, + config: NodeConfig, quasarJarPath: String, debugPort: Int?, jolokiaJarPath: String, @@ -631,15 +710,17 @@ class DriverDSLImpl( overriddenSystemProperties: Map, cordappPackages: List, maximumHeapSize: String, - initialRegistration: Boolean + extraCmdLineFlag: String? ): Process { - log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled")) + log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " + + "debug port is " + (debugPort ?: "not enabled") + ", " + + "jolokia monitoring port is " + (monitorPort ?: "not enabled")) // Write node.conf - writeConfig(nodeConf.baseDirectory, "node.conf", config) + writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe) val systemProperties = mutableMapOf( - "name" to nodeConf.myLegalName, - "visualvm.display.name" to "corda-${nodeConf.myLegalName}", + "name" to config.corda.myLegalName, + "visualvm.display.name" to "corda-${config.corda.myLegalName}", "java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process "log4j2.debug" to if(debugPort != null) "true" else "false" ) @@ -664,11 +745,11 @@ class DriverDSLImpl( val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" val arguments = mutableListOf( - "--base-directory=${nodeConf.baseDirectory}", + "--base-directory=${config.corda.baseDirectory}", "--logging-level=$loggingLevel", "--no-local-shell").also { - if (initialRegistration) { - it += "--initial-registration" + if (extraCmdLineFlag != null) { + it += extraCmdLineFlag } }.toList() @@ -677,8 +758,8 @@ class DriverDSLImpl( arguments = arguments, jdwpPort = debugPort, extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent), - errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", - workingDirectory = nodeConf.baseDirectory, + errorLogPath = config.corda.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", + workingDirectory = config.corda.baseDirectory, maximumHeapSize = maximumHeapSize ) } @@ -827,8 +908,7 @@ fun genericDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, notarySpecs = notarySpecs, - compatibilityZone = null, - onNetworkParametersGeneration = {} + compatibilityZone = null ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -846,11 +926,16 @@ fun genericDriver( } /** + * Internal API to enable testing of the network map service and node registration process using the internal driver. * @property url The base CZ URL for registration and network map updates - * @property rootCert If specified then the node will register itself using [url] and expect the registration response - * to be rooted at this cert. + * @property publishNotaries Hook for a network map server to capture the generated [NotaryInfo] objects needed for + * creating the network parameters. This is needed as the network map server is expected to distribute it. The callback + * will occur on a different thread to the driver-calling thread. + * @property rootCert If specified then the nodes will register themselves with the doorman service using [url] and expect + * the registration response to be rooted at this cert. If not specified then no registration is performed and the dev + * root cert is used as normal. */ -data class CompatibilityZoneParams(val url: URL, val rootCert: X509Certificate? = null) +data class CompatibilityZoneParams(val url: URL, val publishNotaries: (List) -> Unit, val rootCert: X509Certificate? = null) fun internalDriver( isDebug: Boolean = DriverParameters().isDebug, @@ -866,7 +951,6 @@ fun internalDriver( extraCordappPackagesToScan: List = DriverParameters().extraCordappPackagesToScan, jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, compatibilityZone: CompatibilityZoneParams? = null, - onNetworkParametersGeneration: (NetworkParameters) -> Unit = {}, dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -882,8 +966,7 @@ fun internalDriver( notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, - compatibilityZone = compatibilityZone, - onNetworkParametersGeneration = onNetworkParametersGeneration + compatibilityZone = compatibilityZone ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 4b761f0b05..a5dff915ca 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -121,8 +121,7 @@ fun rpcDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, - compatibilityZone = null, - onNetworkParametersGeneration = {} + compatibilityZone = null ), externalTrace ), coerce = { it }, 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 4b16c09a47..345eaf84b8 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 @@ -36,7 +36,7 @@ import javax.ws.rs.core.Response.ok class NetworkMapServer(cacheTimeout: Duration, hostAndPort: NetworkHostAndPort, - root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing. + rootCa: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing. private val myHostNameValue: String = "test.host.name", vararg additionalServices: Any) : Closeable { companion object { @@ -64,7 +64,7 @@ class NetworkMapServer(cacheTimeout: Duration, field = networkParameters } private val serializedParameters get() = networkParameters.serialize() - private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca)) + private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa)) init { diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 32013bfc32..791df21211 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -76,8 +76,7 @@ fun verifierDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, - compatibilityZone = null, - onNetworkParametersGeneration = { } + compatibilityZone = null ) ), coerce = { it },