From 040c4a0fe3467985f541b4457940e9e40613be3a Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Tue, 28 Aug 2018 18:40:10 +0100 Subject: [PATCH 001/119] CORDA-1940 remove any transitive dependency on Logabck (brougth by Liquibase 3.X) Change to older Liquibase 3.5.5 version to align with Enterprise repo - Liquibase 3.6.X changed schema case sensitivity behaviour and it's brakes in Corda for one database vendor. --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 12d70e0312..d49c97ed04 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ buildscript { ext.shiro_version = '1.4.0' ext.shadow_version = '2.0.4' ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - ext.liquibase_version = '3.6.2' + ext.liquibase_version = '3.5.5' ext.artifactory_contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' ext.snake_yaml_version = constants.getProperty('snakeYamlVersion') ext.docker_compose_rule_version = '0.33.0' @@ -235,6 +235,8 @@ allprojects { // We want to use SLF4J's version of these bindings: jcl-over-slf4j // Remove any transitive dependency on Apache's version. exclude group: 'commons-logging', module: 'commons-logging' + // Remove any transitive dependency on Logback (e.g. Liquibase 3.6 introduces this dependency) + exclude group: 'ch.qos.logback' // Netty-All is an uber-jar which contains every Netty module. // Exclude it to force us to use the individual Netty modules instead. From 2d39b39e31c29d4f2e97dc07124563a95402f79d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 21 Aug 2018 23:27:24 +0200 Subject: [PATCH 002/119] Improve the error messages printed by the shell when a flow c'tor doesn't match. --- .../net/corda/tools/shell/InteractiveShell.kt | 44 +++++++++++++++++-- .../tools/shell/InteractiveShellJavaTest.java | 22 ++++++++-- .../corda/tools/shell/InteractiveShellTest.kt | 27 ++++++++++-- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index aa8b9776ac..ed1b5e94cd 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -47,8 +47,7 @@ import java.io.FileDescriptor import java.io.FileInputStream import java.io.InputStream import java.io.PrintWriter -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.UndeclaredThrowableException +import java.lang.reflect.* import java.nio.file.Path import java.util.* import java.util.concurrent.CountDownLatch @@ -300,6 +299,38 @@ object InteractiveShell { override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) } + /** + * Tidies up a possibly generic type name by chopping off the package names of classes in a hard-coded set of + * hierarchies that are known to be widely used and recognised, and also not have (m)any ambiguous names in them. + * + * This is used for printing error messages when something doesn't match. + */ + private fun maybeAbbreviateGenericType(type: Type, extraRecognisedPackage: String): String { + val packagesToAbbreviate = listOf("java.", "net.corda.core.", "kotlin.", extraRecognisedPackage) + + fun shouldAbbreviate(typeName: String) = packagesToAbbreviate.any { typeName.startsWith(it) } + fun abbreviated(typeName: String) = if (shouldAbbreviate(typeName)) typeName.split('.').last() else typeName + + fun innerLoop(type: Type): String = when (type) { + is ParameterizedType -> { + val args: List = type.actualTypeArguments.map(::innerLoop) + abbreviated(type.rawType.typeName) + '<' + args.joinToString(", ") + '>' + } + is GenericArrayType -> { + innerLoop(type.genericComponentType) + "[]" + } + is Class<*> -> { + if (type.isArray) + abbreviated(type.simpleName) + else + abbreviated(type.name).replace('$', '.') + } + else -> type.toString() + } + + return innerLoop(type) + } + // TODO: This utility is generally useful and might be better moved to the node class, or an RPC, if we can commit to making it stable API. /** * Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts @@ -319,10 +350,17 @@ object InteractiveShell { // and keep track of the reasons we failed so we can print them out if no constructors are usable. val parser = StringToMethodCallParser(clazz, om) val errors = ArrayList() + + val classPackage = clazz.packageName for (ctor in clazz.constructors) { var paramNamesFromConstructor: List? = null + fun getPrototype(): List { - val argTypes = ctor.genericParameterTypes.map { it.typeName } + val argTypes = ctor.genericParameterTypes.map { it: Type -> + // If the type name is in the net.corda.core or java namespaces, chop off the package name + // because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise. + maybeAbbreviateGenericType(it, classPackage) + } return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } } diff --git a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java index b3581caf11..e3dde61bb6 100644 --- a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java +++ b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java @@ -52,8 +52,8 @@ public class InteractiveShellJavaTest { } } - public FlowA(Integer b) { - this(b.toString()); + public FlowA(int b) { + this(Integer.valueOf(b).toString()); } public FlowA(Integer b, String c) { @@ -111,6 +111,9 @@ public class InteractiveShellJavaTest { this.a = a; } + public FlowB(Amount amount, int abc) { + } + @Nullable @Override public ProgressTracker getProgressTracker() { @@ -142,6 +145,7 @@ public class InteractiveShellJavaTest { this.label = label; } + @SuppressWarnings("unused") // Used via reflection. public String getLabel() { return label; } @@ -160,17 +164,17 @@ public class InteractiveShellJavaTest { private void check(String input, String expected, Class flowClass) throws InteractiveShell.NoApplicableConstructor { InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> { - StringFlow instance = null; try { instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args); } catch (Exception e) { System.out.println(e); + throw new RuntimeException(e); } output = instance.getA(); OpenFuture future = CordaFutureImplKt.openFuture(); future.set("ABC"); - return new FlowProgressHandleImpl(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); + return new FlowProgressHandleImpl(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); }, input, flowClass, om); assertEquals(input, expected, output); } @@ -245,4 +249,14 @@ public class InteractiveShellJavaTest { public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor { check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class); } + + @Test + public void niceErrors() { + // Most cases are checked in the Kotlin test, so we only check raw types here. + try { + check("amount: $100", "", FlowB.class); + } catch (InteractiveShell.NoApplicableConstructor e) { + assertEquals("[amount: Amount, abc: int]: missing parameter abc", e.getErrors().get(1)); + } + } } diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index 6d7f614a0f..e184c6ae86 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -14,12 +14,13 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.utilities.ProgressTracker import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.DEV_ROOT_CA import org.junit.Test import rx.Observable import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class InteractiveShellTest { companion object { @@ -28,7 +29,7 @@ class InteractiveShellTest { @Suppress("UNUSED") class FlowA(val a: String) : FlowLogic() { - constructor(b: Int?) : this(b.toString()) + constructor(b: Int) : this(b.toString()) constructor(b: Int?, c: String) : this(b.toString() + c) constructor(amount: Amount) : this(amount.toString()) constructor(pair: Pair, SecureHash.SHA256>) : this(pair.toString()) @@ -48,7 +49,6 @@ class InteractiveShellTest { private fun check(input: String, expected: String) { var output: String? = null InteractiveShell.runFlowFromString({ clazz, args -> - val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA output = instance.a val future = openFuture() @@ -101,6 +101,27 @@ class InteractiveShellTest { @Test(expected = InteractiveShell.NoApplicableConstructor::class) fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") + @Test + fun niceTypeNamesInErrors() { + val e = assertFailsWith { + check("", expected = "") + } + val correct = setOf( + "[amounts: Amount[]]: missing parameter amounts", + "[amount: Amount]: missing parameter amount", + "[pair: Pair, SecureHash.SHA256>]: missing parameter pair", + "[party: Party]: missing parameter party", + "[b: Integer, amount: Amount]: missing parameter b", + "[b: String[]]: missing parameter b", + "[b: Integer, c: String]: missing parameter b", + "[a: String]: missing parameter a", + "[b: int]: missing parameter b" + ) + val errors = e.errors.toHashSet() + errors.removeAll(correct) + assert(errors.isEmpty()) { errors.joinToString(", ") } + } + @Test fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString()) From e87b33d1e8dc581bfebc7c14a28e9a718df9a957 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 21 Aug 2018 15:04:43 +0100 Subject: [PATCH 003/119] Added extra logging to build.gradle when maxParallelForks system property is set --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 12d70e0312..188a995d6e 100644 --- a/build.gradle +++ b/build.gradle @@ -195,6 +195,7 @@ allprojects { if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) + logger.debug("System property test.maxParallelForks found - setting max parallel forks to $maxParallelForks for $project") } if (project.path.startsWith(':experimental') && System.getProperty("experimental.test.enable") == null) { From 2e943d089b6af1e0c76d1b9587da8abc7715d5f6 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 27 Aug 2018 21:06:01 +0200 Subject: [PATCH 004/119] Update RxJava to the last 1.x release so we get JavaDocs back. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 83e9f1bdd5..669311b082 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ buildscript { ext.hibernate_version = '5.2.6.Final' ext.h2_version = '1.4.197' // Update docs if renamed or removed. ext.postgresql_version = '42.1.4' - ext.rxjava_version = '1.2.4' + ext.rxjava_version = '1.3.8' ext.dokka_version = '0.9.17' ext.eddsa_version = '0.2.0' ext.dependency_checker_version = '3.1.0' From c68134ad6034d031b94f9d4b7cc267717204df2f Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 27 Aug 2018 22:31:20 +0200 Subject: [PATCH 005/119] Minor: fix changelog markup errors. --- docs/source/changelog.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 403b1155ba..51c95b4d1a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -17,7 +17,7 @@ Unreleased * Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. -* Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database. +* Change type of the ``checkpoint_value`` column. Please check the upgrade-notes on how to update your database. * Removed buggy :serverNameTablePrefix: configuration. @@ -144,7 +144,7 @@ Unreleased Values are: [FAIL, WARN, IGNORE], default to FAIL if unspecified. * Introduced a placeholder for custom properties within ``node.conf``; the property key is "custom". * The deprecated web server now has its own ``web-server.conf`` file, separate from ``node.conf``. - * Property keys with double quotes (e.g. `"key"`) in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. + * Property keys with double quotes (e.g. "key") in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api` @@ -161,7 +161,7 @@ Unreleased * The whitelist.txt file is no longer needed. The existing network parameters file is used to update the current contracts whitelist. - * The CorDapp jars are also copied to each nodes' `cordapps` directory. + * The CorDapp jars are also copied to each nodes' ``cordapps`` directory. * Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. @@ -171,7 +171,7 @@ Unreleased reference to the outer class) as per the Java documentation `here `_ we are disallowing this as the paradigm in general makes little sense for contract states. -* Node can be shut down abruptly by ``shutdown`` function in `CordaRPCOps` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. +* Node can be shut down abruptly by ``shutdown`` function in ``CordaRPCOps`` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. * API change: ``net.corda.core.schemas.PersistentStateRef`` fields (index and txId) are now non-nullable. The fields were always effectively non-nullable - values were set from non-nullable fields of other objects. @@ -185,8 +185,8 @@ Unreleased * Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``. -* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema`. - The log entry starts with `Cross-reference between MappedSchemas.`. +* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema``. + The log entry starts with "Cross-reference between MappedSchemas". API: Persistence documentation no longer suggests mapping between different schemas. * Upgraded Artemis to v2.6.2. From 12707c8df0fc4392348806d4fc22f3b4f6a6f029 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 21 Aug 2018 19:10:20 +0200 Subject: [PATCH 006/119] Signature constraints design doc: address review comments from Jose. --- .../data-model-upgrades/signature-constraints.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/design/data-model-upgrades/signature-constraints.md b/docs/source/design/data-model-upgrades/signature-constraints.md index 67f5de1b95..09bbeef5f1 100644 --- a/docs/source/design/data-model-upgrades/signature-constraints.md +++ b/docs/source/design/data-model-upgrades/signature-constraints.md @@ -4,7 +4,7 @@ This design document outlines an additional kind of *contract constraint*, used ## Background -Contract constraints are a part of how Corda manages application upgrades. There are two kinds of upgrade that can be applied to the ledger: +Contract constraints are a part of how Corda ensures the correct code is executed to verify transactions, and also how it manages application upgrades. There are two kinds of upgrade that can be applied to the ledger: * Explicit * Implicit @@ -31,21 +31,20 @@ We would like a new kind of constraint that is more convenient and decentralised ## Goals * Improve usability by eliminating the need to change the network parameters. - * Improve decentralisation by allowing apps to be developed and upgraded without the zone operator knowing or being able to influence it. - * Eventually, phase out zone whitelisting constraints. - ## Non-goals * Preventing downgrade attacks. Downgrade attack prevention will be tackled in a different design effort. * Phase out of hash constraints. If malicious app creators are in the users threat model then hash constraints are the way to go. * Handling the case where third parties re-sign app jars. +* Package namespace ownership (a separate effort). +* Allowing the zone operator to override older constraints, to provide a non-explicit upgrade path. ## Design details -We propose being able to constrain to any attachments signed by a specified set of keys. +We propose being able to constrain to any attachments whose files are signed by a specified set of keys. This satisfies the usability requirement because the creation of a new application is as simple as invoking the `jarsigner` tool that comes with the JDK. This can be integrated with the build system via a Gradle or Maven task. For example, Gradle can use jarsigner via [the signjar task](https://ant.apache.org/manual/Tasks/signjar.html) ([example](https://gist.github.com/Lien/7150434)). @@ -87,7 +86,7 @@ The `TransactionBuilder` class can select the right constraint given what it alr ### Tooling and workflow -The primary tool required is of course `jarsigner`. In dev and integration test modes, the node will ignore missing signatures in attachment JARs and will simply log a warning if no signature is present. +The primary tool required is of course `jarsigner`. In dev mode, the node will ignore missing signatures in attachment JARs and will simply log an error if no signature is present when a constraint requires one. To verify and print information about the signatures on a JAR, the `jarsigner` tool can be used again. In addition, we should add some new shell commands that do the same thing, but for a given attachment hash or transaction hash - these may be useful for debugging and analysis. Actually a new shell command should cover all aspects of inspecting attachments - not just signatures but what's inside them, simple way to save them to local disk etc. From 4337537791a02b1a2471936d83d443300322647d Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Wed, 29 Aug 2018 17:43:17 +0100 Subject: [PATCH 007/119] CORDA-1945: properly support double-width interp stack slots in superclasses when synthesising (#3859) * Fix for CORDA-1945 * Revert irrelevant style change --- .../internal/carpenter/ClassCarpenter.kt | 16 +++++++------- .../internal/carpenter/ClassCarpenterTest.kt | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt index 315ad35a50..f1a422f22e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt @@ -130,8 +130,8 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas */ override fun build(schema: Schema): Class<*> { validateSchema(schema) - // Walk up the inheritance hierarchy and then start walking back down once we either hit the top, or - // find a class we haven't generated yet. + // Walk up the inheritance hierarchy until we hit either the top or a class we've already generated, + // then walk back down it generating classes. val hierarchy = ArrayList() hierarchy += schema var cursor = schema.superclass @@ -306,16 +306,16 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas visitInsn(DUP) var idx = 0 - schema.fields.forEach { + schema.fields.keys.forEach { key -> visitInsn(DUP) visitIntInsn(BIPUSH, idx) visitTypeInsn(NEW, schema.jvmName) visitInsn(DUP) - visitLdcInsn(it.key) + visitLdcInsn(key) visitIntInsn(BIPUSH, idx++) visitMethodInsn(INVOKESPECIAL, schema.jvmName, "", "(L$jlString;I)V", false) visitInsn(DUP) - visitFieldInsn(PUTSTATIC, schema.jvmName, it.key, "L${schema.jvmName};") + visitFieldInsn(PUTSTATIC, schema.jvmName, key, "L${schema.jvmName};") visitInsn(AASTORE) } @@ -381,20 +381,18 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas visitCode() // Calculate the super call. - val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap() visitVarInsn(ALOAD, 0) val sc = schema.superclass + var slot = 1 if (sc == null) { visitMethodInsn(INVOKESPECIAL, jlObject, "", "()V", false) } else { - var slot = 1 - superclassFields.values.forEach { slot += load(slot, it) } + slot = sc.fieldsIncludingSuperclasses().values.fold(slot) { acc, field -> acc + load(acc, field) } val superDesc = sc.descriptorsIncludingSuperclasses().values.joinToString("") visitMethodInsn(INVOKESPECIAL, sc.jvmName, "", "($superDesc)V", false) } // Assign the fields from parameters. - var slot = 1 + superclassFields.size for ((name, field) in schema.fields) { (field as ClassField).nullTest(this, slot) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt index e74206e5b0..ac510642d7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt @@ -129,6 +129,27 @@ class ClassCarpenterTest { assertEquals("B{a=xa, b=xb}", i.toString()) } + /** + * Tests the fix for [Corda-1945](https://r3-cev.atlassian.net/secure/RapidBoard.jspa?rapidView=83&modal=detail&selectedIssue=CORDA-1945) + */ + @Test + fun `superclasses with double-size primitive constructor parameters`() { + val schema1 = ClassSchema( + "gen.A", + mapOf("a" to NonNullableField(Long::class.javaPrimitiveType!!))) + + val schema2 = ClassSchema( + "gen.B", + mapOf("b" to NonNullableField(String::class.java)), + schema1) + + val clazz = cc.build(schema2) + val i = clazz.constructors[0].newInstance(1L, "xb") as SimpleFieldAccess + assertEquals(1L, i["a"]) + assertEquals("xb", i["b"]) + assertEquals("B{a=1, b=xb}", i.toString()) + } + @Test fun interfaces() { val schema1 = ClassSchema( From 0f36e2231462c2e3697f33b79c411f925dc7c948 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Thu, 30 Aug 2018 10:18:02 +0100 Subject: [PATCH 008/119] Corda-1869 serialisation refactor (#3780) * Pull out and tidy type parameter inference * Contain null proliferation * Extract fingerprinter state * SerializerFingerPrinter is always initialised with a SerializerFactory * Move non-recursive state transition functions into state * Move all state transition functions into state * Simplify and optimise with mutable state * Move TypeParameterUtils back into internal.amqp * Clarify behaviour of constructorForDeserialisation * constructorForDeserialization no longer returns null * Capture field properties * Narrow PropertyDescriptor * Use map rather than apply on a mutable list * Remove printStackTrace added for debugging * CORDA-1869 minor tweaks * Use groupingBy to avoid creating an intermediate map * Convert some functional origami to plain old for-loops * Eliminate nested lambda to unbreak pre-serialisation * Use EnumMap for map of Enums --- .../internal/amqp/ArraySerializer.kt | 12 +- .../internal/amqp/CorDappCustomSerializer.kt | 6 +- .../internal/amqp/CustomSerializer.kt | 2 +- .../internal/amqp/DeserializationInput.kt | 2 +- .../internal/amqp/EnumEvolutionSerializer.kt | 4 +- .../internal/amqp/EnumSerializer.kt | 2 +- .../internal/amqp/EvolutionSerializer.kt | 10 +- .../internal/amqp/FingerPrinter.kt | 311 ++++++------ .../internal/amqp/ObjectSerializer.kt | 3 +- .../internal/amqp/PropertyDescriptor.kt | 205 ++++++++ .../internal/amqp/PropertySerializer.kt | 4 +- .../internal/amqp/SerializationHelper.kt | 456 ++++++------------ .../internal/amqp/SerializerFactory.kt | 99 +--- .../internal/amqp/TypeParameterUtils.kt | 94 ++++ .../amqp/custom/ThrowableSerializer.kt | 4 +- .../internal/amqp/FingerPrinterTesting.kt | 6 +- 16 files changed, 627 insertions(+), 593 deletions(-) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 913accf079..6869e71d45 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -49,7 +49,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) "$typeName[]" } else { - val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" + val arrayType = if (type.asClass().componentType.isPrimitive) "[p]" else "[]" "${type.componentType().typeName}$arrayType" } } @@ -93,7 +93,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) } open fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, list[it]) } @@ -105,7 +105,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array::class.java, factory) { override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, (list[it] as Int).toChar()) } @@ -159,11 +159,7 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer( } override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException( - type, - "Unexpected array element type $type", - "blob is corrupt") - + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { val array = this diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt index 9506fbd510..f1b509c940 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt @@ -93,8 +93,8 @@ class CorDappCustomSerializer( * For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will * not support base class serializers for derivedtypes */ - override fun isSerializerFor(clazz: Class<*>) : Boolean { - return type.asClass()?.let { TypeToken.of(it) == TypeToken.of(clazz) } ?: false - } + override fun isSerializerFor(clazz: Class<*>) = + TypeToken.of(type.asClass()) == TypeToken.of(clazz) + } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index f99a842758..cbd54f08c2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -67,7 +67,7 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override val type: Type get() = clazz override val typeDescriptor: Symbol by lazy { - Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") } private val typeNotation: TypeNotation = RestrictedType( SerializerFactory.nameForType(clazz), diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 6d75e3a553..b9c50f7250 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -156,7 +156,7 @@ class DeserializationInput constructor( "is outside of the bounds for the list of size: ${objectHistory.size}") val objectRetrieved = objectHistory[objectIndex] - if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { + if (!objectRetrieved::class.java.isSubClassOf(type.asClass())) { throw AMQPNotSerializableException( type, "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index 2dfb36b503..5e7010c71c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -80,7 +80,7 @@ class EnumEvolutionSerializer( val renameRules: List? = uncheckedCast(transforms[TransformTypes.Rename]) // What values exist on the enum as it exists on the class path - val localValues = new.type.asClass()!!.enumConstants.map { it.toString() } + val localValues = new.type.asClass().enumConstants.map { it.toString() } val conversions: MutableMap = localValues .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) @@ -130,7 +130,7 @@ class EnumEvolutionSerializer( throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName") } - return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] + return type.asClass().enumConstants[ordinals[conversions[enumName]]!!] } override fun writeClassInfo(output: SerializationOutput) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt index 34b6697901..1bb12190f2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt @@ -34,7 +34,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria ): Any { val enumName = (obj as List<*>)[0] as String val enumOrd = obj[1] as Int - val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? + val fromOrd = type.asClass().enumConstants[enumOrd] as Enum<*>? if (enumName != fromOrd?.name) { throw AMQPNotSerializableException( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 0aec3e6832..79a3f85c00 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -32,7 +32,7 @@ abstract class EvolutionSerializer( clazz: Type, factory: SerializerFactory, protected val oldReaders: Map, - override val kotlinConstructor: KFunction? + override val kotlinConstructor: KFunction ) : ObjectSerializer(clazz, factory) { // explicitly set as empty to indicate it's unused by this type of serializer override val propertySerializers = PropertySerializersEvolution() @@ -74,7 +74,7 @@ abstract class EvolutionSerializer( * TODO: rename annotation */ private fun getEvolverConstructor(type: Type, oldArgs: Map): KFunction? { - val clazz: Class<*> = type.asClass()!! + val clazz: Class<*> = type.asClass() if (!clazz.isConcreteClass) return null @@ -189,7 +189,7 @@ abstract class EvolutionSerializer( // return the synthesised object which is, given the absence of a constructor, a no op val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new - val classProperties = new.type.asClass()?.propertyDescriptors() ?: emptyMap() + val classProperties = new.type.asClass().propertyDescriptors() return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) { makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties) @@ -210,7 +210,7 @@ class EvolutionSerializerViaConstructor( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction?, + kotlinConstructor: KFunction, private val constructorArgs: Array) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { /** * Unlike a normal [readObject] call where we simply apply the parameter deserialisers @@ -242,7 +242,7 @@ class EvolutionSerializerViaSetters( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction?, + kotlinConstructor: KFunction, private val setters: Map) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt index 6b698dd057..d03e3e6da1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt @@ -3,15 +3,15 @@ package net.corda.serialization.internal.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing import net.corda.core.KeepForDJVM +import net.corda.core.internal.isConcreteClass import net.corda.core.internal.kotlinObjectInstance -import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 -import java.io.NotSerializableException +import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive import java.lang.reflect.* import java.util.* /** - * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory] + * Should be implemented by classes which wish to provide pluggable fingerprinting on types for a [SerializerFactory] */ @KeepForDJVM interface FingerPrinter { @@ -20,34 +20,13 @@ interface FingerPrinter { * of said type such that any modification to any sub element wll generate a different fingerprint */ fun fingerprint(type: Type): String - - /** - * If required, associate an instance of the fingerprinter with a specific serializer factory - */ - fun setOwner(factory: SerializerFactory) } /** * Implementation of the finger printing mechanism used by default */ @KeepForDJVM -class SerializerFingerPrinter : FingerPrinter { - private var factory: SerializerFactory? = null - - private val ARRAY_HASH: String = "Array = true" - private val ENUM_HASH: String = "Enum = true" - private val ALREADY_SEEN_HASH: String = "Already seen = true" - private val NULLABLE_HASH: String = "Nullable = true" - private val NOT_NULLABLE_HASH: String = "Nullable = false" - private val ANY_TYPE_HASH: String = "Any type = true" - private val TYPE_VARIABLE_HASH: String = "Type variable = true" - private val WILDCARD_TYPE_HASH: String = "Wild card = true" - - private val logger by lazy { loggerFor() } - - override fun setOwner(factory: SerializerFactory) { - this.factory = factory - } +class SerializerFingerPrinter(val factory: SerializerFactory) : FingerPrinter { /** * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. @@ -57,147 +36,167 @@ class SerializerFingerPrinter : FingerPrinter { * The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be * different. */ - override fun fingerprint(type: Type): String { - return fingerprintForType( - type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64() + override fun fingerprint(type: Type): String = FingerPrintingState(factory).fingerprint(type) +} + +// Representation of the current state of fingerprinting +internal class FingerPrintingState(private val factory: SerializerFactory) { + + companion object { + private const val ARRAY_HASH: String = "Array = true" + private const val ENUM_HASH: String = "Enum = true" + private const val ALREADY_SEEN_HASH: String = "Already seen = true" + private const val NULLABLE_HASH: String = "Nullable = true" + private const val NOT_NULLABLE_HASH: String = "Nullable = false" + private const val ANY_TYPE_HASH: String = "Any type = true" } - private fun isCollectionOrMap(type: Class<*>) = - (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) - && !EnumSet::class.java.isAssignableFrom(type) + private val typesSeen: MutableSet = mutableSetOf() + private var currentContext: Type? = null + private var hasher: Hasher = newDefaultHasher() - internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { - val hasher = Hashing.murmur3_128().newHasher() - for (typeDescriptor in typeDescriptors) { - hasher.putUnencodedChars(typeDescriptor) - } - return hasher.hash().asBytes().toBase64() - } - - private fun Hasher.fingerprintWithCustomSerializerOrElse( - factory: SerializerFactory, - clazz: Class<*>, - declaredType: Type, - block: () -> Hasher): Hasher { - // Need to check if a custom serializer is applicable - val customSerializer = factory.findCustomSerializer(clazz, declaredType) - return if (customSerializer != null) { - putUnencodedChars(customSerializer.typeDescriptor) - } else { - block() - } - } + // Fingerprint the type recursively, and return the encoded fingerprint written into the hasher. + fun fingerprint(type: Type) = fingerprintType(type).hasher.fingerprint // This method concatenates various elements of the types recursively as unencoded strings into the hasher, // effectively creating a unique string for a type which we then hash in the calling function above. - private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, - hasher: Hasher, debugIndent: Int = 1): Hasher { - // We don't include Example and Example where type is ? or T in this otherwise we - // generate different fingerprints for class Outer(val a: Inner) when serialising - // and deserializing (assuming deserialization is occurring in a factory that didn't - // serialise the object in the first place (and thus the cache lookup fails). This is also - // true of Any, where we need Example and Example to have the same fingerprint - return if ((type in alreadySeen) - && (type !== SerializerFactory.AnyType) - && (type !is TypeVariable<*>) - && (type !is WildcardType) - ) { - hasher.putUnencodedChars(ALREADY_SEEN_HASH) - } else { - alreadySeen += type - ifThrowsAppend({ type.typeName }) { - when (type) { - is ParameterizedType -> { - // Hash the rawType + params - val clazz = type.rawType as Class<*> + private fun fingerprintType(type: Type): FingerPrintingState = apply { + // Don't go round in circles. + if (hasSeen(type)) append(ALREADY_SEEN_HASH) + else ifThrowsAppend( + { type.typeName }, + { + typesSeen.add(type) + currentContext = type + fingerprintNewType(type) + }) + } - val startingHash = if (isCollectionOrMap(clazz)) { - hasher.putUnencodedChars(clazz.name) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } + // For a type we haven't seen before, determine the correct path depending on the type of type it is. + private fun fingerprintNewType(type: Type) = when (type) { + is ParameterizedType -> fingerprintParameterizedType(type) + // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing + // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- + // directional fingerprints. That is, fingerprinting a concrete instance of a generic + // type (Example), creates a different fingerprint from the generic type itself (Example) + // + // On serialization Example is treated as Example, a TypeVariable + // On deserialisation it is seen as Example, A WildcardType *and* a TypeVariable + // Note: AnyType is a special case of WildcardType used in other parts of the + // serializer so both cases need to be dealt with here + // + // If we treat these types as fundamentally different and alter the fingerprint we will + // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. + is SerializerFactory.AnyType, + is WildcardType, + is TypeVariable<*> -> append("?$ANY_TYPE_HASH") + is Class<*> -> fingerprintClass(type) + is GenericArrayType -> fingerprintType(type.genericComponentType).append(ARRAY_HASH) + else -> throw AMQPNotSerializableException(type, "Don't know how to hash") + } - // ... and concatenate the type data for each parameter type. - type.actualTypeArguments.fold(startingHash) { orig, paramType -> - fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1) - } - } - // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing - // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- - // directional fingerprints. That is, fingerprinting a concrete instance of a generic - // type (Example), creates a different fingerprint from the generic type itself (Example) - // - // On serialization Example is treated as Example, a TypeVariable - // On deserialisation it is seen as Example, A WildcardType *and* a TypeVariable - // Note: AnyType is a special case of WildcardType used in other parts of the - // serializer so both cases need to be dealt with here - // - // If we treat these types as fundamentally different and alter the fingerprint we will - // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. - is SerializerFactory.AnyType, - is WildcardType, - is TypeVariable<*> -> { - hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH) - } - is Class<*> -> { - if (type.isArray) { - fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1) - .putUnencodedChars(ARRAY_HASH) - } else if (SerializerFactory.isPrimitive(type)) { - hasher.putUnencodedChars(type.name) - } else if (isCollectionOrMap(type)) { - hasher.putUnencodedChars(type.name) - } else if (type.isEnum) { - // ensures any change to the enum (adding constants) will trigger the need for evolution - hasher.apply { - type.enumConstants.forEach { - putUnencodedChars(it.toString()) - } - }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) { - if (type.kotlinObjectInstance != null) { - // TODO: name collision is too likely for kotlin objects, we need to introduce some - // reference to the CorDapp but maybe reference to the JAR in the short term. - hasher.putUnencodedChars(type.name) - } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } - } - } - // Hash the element type + some array hash - is GenericArrayType -> { - fingerprintForType(type.genericComponentType, contextType, alreadySeen, - hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH) - } - else -> throw AMQPNotSerializableException(type, "Don't know how to hash") - } - } + private fun fingerprintClass(type: Class<*>) = when { + type.isArray -> fingerprintType(type.componentType).append(ARRAY_HASH) + type.isPrimitiveOrCollection -> append(type.name) + type.isEnum -> fingerprintEnum(type) + else -> fingerprintWithCustomSerializerOrElse(type, type) { + if (type.kotlinObjectInstance != null) append(type.name) + else fingerprintObject(type) } } - private fun fingerprintForObject( - type: Type, - contextType: Type?, - alreadySeen: MutableSet, - hasher: Hasher, - factory: SerializerFactory, - debugIndent: Int = 0): Hasher { - // Hash the class + properties + interfaces - val name = type.asClass()?.name - ?: throw AMQPNotSerializableException(type, "Expected only Class or ParameterizedType but found $type") + private fun fingerprintParameterizedType(type: ParameterizedType) { + // Hash the rawType + params + type.asClass().let { clazz -> + if (clazz.isCollectionOrMap) append(clazz.name) + else fingerprintWithCustomSerializerOrElse(clazz, type) { + fingerprintObject(type) + } + } - propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) - .serializationOrder - .fold(hasher.putUnencodedChars(name)) { orig, prop -> - fingerprintForType(prop.serializer.resolvedType, type, alreadySeen, orig, debugIndent + 1) - .putUnencodedChars(prop.serializer.name) - .putUnencodedChars(if (prop.serializer.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) - } - interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) } - return hasher + // ...and concatenate the type data for each parameter type. + type.actualTypeArguments.forEach { paramType -> + fingerprintType(paramType) + } } -} \ No newline at end of file + + private fun fingerprintObject(type: Type) { + // Hash the class + properties + interfaces + append(type.asClass().name) + + orderedPropertiesForSerialization(type).forEach { prop -> + fingerprintType(prop.serializer.resolvedType) + fingerprintPropSerialiser(prop) + } + + interfacesForSerialization(type, factory).forEach { iface -> + fingerprintType(iface) + } + } + + // ensures any change to the enum (adding constants) will trigger the need for evolution + private fun fingerprintEnum(type: Class<*>) { + append(type.enumConstants.joinToString()) + append(type.name) + append(ENUM_HASH) + } + + private fun fingerprintPropSerialiser(prop: PropertyAccessor) { + append(prop.serializer.name) + append(if (prop.serializer.mandatory) NOT_NULLABLE_HASH + else NULLABLE_HASH) + } + + // Write the given character sequence into the hasher. + private fun append(chars: CharSequence) { + hasher = hasher.putUnencodedChars(chars) + } + + // Give any custom serializers loaded into the factory the chance to supply their own type-descriptors + private fun fingerprintWithCustomSerializerOrElse( + clazz: Class<*>, + declaredType: Type, + defaultAction: () -> Unit) + : Unit = factory.findCustomSerializer(clazz, declaredType)?.let { + append(it.typeDescriptor) + } ?: defaultAction() + + // Test whether we are in a state in which we have already seen the given type. + // + // We don't include Example and Example where type is ? or T in this otherwise we + // generate different fingerprints for class Outer(val a: Inner) when serialising + // and deserializing (assuming deserialization is occurring in a factory that didn't + // serialise the object in the first place (and thus the cache lookup fails). This is also + // true of Any, where we need Example and Example to have the same fingerprint + private fun hasSeen(type: Type) = (type in typesSeen) + && (type !== SerializerFactory.AnyType) + && (type !is TypeVariable<*>) + && (type !is WildcardType) + + private fun orderedPropertiesForSerialization(type: Type): List { + return propertiesForSerialization( + if (type.asClass().isConcreteClass) constructorForDeserialization(type) else null, + currentContext ?: type, + factory).serializationOrder + } + +} + +// region Utility functions + +// Create a new instance of the [Hasher] used for fingerprinting by the default [SerializerFingerPrinter] +private fun newDefaultHasher() = Hashing.murmur3_128().newHasher() + +// We obtain a fingerprint from a [Hasher] by taking the Base 64 encoding of its hash bytes +private val Hasher.fingerprint get() = hash().asBytes().toBase64() + +internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String = + newDefaultHasher().putUnencodedChars(typeDescriptors.joinToString()).fingerprint + +private val Class<*>.isCollectionOrMap get() = + (Collection::class.java.isAssignableFrom(this) || Map::class.java.isAssignableFrom(this)) + && !EnumSet::class.java.isAssignableFrom(this) + +private val Class<*>.isPrimitiveOrCollection get() = + isPrimitive(this) || isCollectionOrMap +// endregion diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index 7a8f0d2334..dc142fce16 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -1,5 +1,6 @@ package net.corda.serialization.internal.amqp +import net.corda.core.internal.isConcreteClass import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace @@ -18,7 +19,7 @@ import kotlin.reflect.jvm.javaConstructor */ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz - open val kotlinConstructor = constructorForDeserialization(clazz) + open val kotlinConstructor = if (clazz.asClass().isConcreteClass) constructorForDeserialization(clazz) else null val javaConstructor by lazy { kotlinConstructor?.javaConstructor } companion object { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt new file mode 100644 index 0000000000..882f067eba --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt @@ -0,0 +1,205 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.reflect.TypeToken +import net.corda.core.KeepForDJVM +import net.corda.core.internal.isPublic +import net.corda.serialization.internal.amqp.MethodClassifier.* +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Type +import java.util.* + +/** + * Encapsulates the property of a class and its potential getter and setter methods. + * + * @property field a property of a class. + * @property setter the method of a class that sets the field. Determined by locating + * a function called setXyz on the class for the property named in field as xyz. + * @property getter the method of a class that returns a fields value. Determined by + * locating a function named getXyz for the property named in field as xyz. + */ +@KeepForDJVM +data class PropertyDescriptor(val field: Field?, val setter: Method?, val getter: Method?) { + override fun toString() = StringBuilder("").apply { + appendln("Property - ${field?.name ?: "null field"}\n") + appendln(" getter - ${getter?.name ?: "no getter"}") + appendln(" setter - ${setter?.name ?: "no setter"}") + }.toString() + + /** + * Check the types of the field, getter and setter methods against each other. + */ + fun validate() { + getter?.apply { + val getterType = genericReturnType + field?.apply { + if (!getterType.isSupertypeOf(genericReturnType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined getter for parameter $name returns type $getterType " + + "yet underlying type is $genericType") + } + } + + setter?.apply { + val setterType = genericParameterTypes[0]!! + + field?.apply { + if (!genericType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType " + + "yet underlying type is $genericType") + } + + getter?.apply { + if (!genericReturnType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType, " + + "but getter returns $genericReturnType") + } + } + } +} + +private fun Type.isSupertypeOf(that: Type) = TypeToken.of(this).isSupertypeOf(that) + +// match an uppercase letter that also has a corresponding lower case equivalent +private val propertyMethodRegex = Regex("(?get|set|is)(?\\p{Lu}.*)") + +/** + * Collate the properties of a class and match them with their getter and setter + * methods as per a JavaBean. + * + * for a property + * exampleProperty + * + * We look for methods + * setExampleProperty + * getExampleProperty + * isExampleProperty + * + * Where getExampleProperty must return a type compatible with exampleProperty, setExampleProperty must + * take a single parameter of a type compatible with exampleProperty and isExampleProperty must + * return a boolean + */ +fun Class.propertyDescriptors(): Map { + val fieldProperties = superclassChain().declaredFields().byFieldName() + + return superclassChain().declaredMethods() + .thatArePublic() + .thatArePropertyMethods() + .withValidSignature() + .byNameAndClassifier(fieldProperties.keys) + .toClassProperties(fieldProperties) + .validated() +} + +// Generate the sequence of classes starting with this class and ascending through it superclasses. +private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass) + +// Obtain the fields declared by all classes in this sequence of classes. +private fun Sequence>.declaredFields() = flatMap { it.declaredFields.asSequence() } + +// Obtain the methods declared by all classes in this sequence of classes. +private fun Sequence>.declaredMethods() = flatMap { it.declaredMethods.asSequence() } + +// Map a sequence of fields by field name. +private fun Sequence.byFieldName() = map { it.name to it }.toMap() + +// Select only those methods that are public (and are not the "getClass" method) +private fun Sequence.thatArePublic() = filter { it.isPublic && it.name != "getClass" } + +// Select only those methods that are isX/getX/setX methods +private fun Sequence.thatArePropertyMethods() = map { method -> + propertyMethodRegex.find(method.name)?.let { result -> + PropertyNamedMethod( + result.groups[2]!!.value, + MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()), + method) + } +}.filterNotNull() + +// Pick only those methods whose signatures are valid, discarding the remainder without warning. +private fun Sequence.withValidSignature() = filter { it.hasValidSignature() } + +// Group methods by name and classifier, picking the method with the least generic signature if there is more than one +// of a given name and type. +private fun Sequence.byNameAndClassifier(fieldNames: Set): Map> { + val result = mutableMapOf>() + + forEach { (fieldName, classifier, method) -> + result.compute(getPropertyName(fieldName, fieldNames)) { _, byClassifier -> + (byClassifier ?: EnumMap(MethodClassifier::class.java)).merge(classifier, method) + } + } + + return result +} + +// Merge the given method into a map of methods by method classifier, picking the least generic method for each classifier. +private fun EnumMap.merge(classifier: MethodClassifier, method: Method): EnumMap { + compute(classifier) { _, existingMethod -> + if (existingMethod == null) method + else when (classifier) { + IS -> existingMethod + GET -> leastGenericBy({ genericReturnType }, existingMethod, method) + SET -> leastGenericBy({ genericParameterTypes[0] }, existingMethod, method) + } + } + return this +} + +// Make the property name conform to the underlying field name, if there is one. +private fun getPropertyName(propertyName: String, fieldNames: Set) = + if (propertyName.decapitalize() in fieldNames) propertyName.decapitalize() + else propertyName + + +// Which of the three types of property method the method is. +private enum class MethodClassifier { GET, SET, IS } + +private data class PropertyNamedMethod(val fieldName: String, val classifier: MethodClassifier, val method: Method) { + // Validate the method's signature against its classifier + fun hasValidSignature(): Boolean = method.run { + when (classifier) { + GET -> parameterCount == 0 && returnType != Void.TYPE + SET -> parameterCount == 1 && returnType == Void.TYPE + IS -> parameterCount == 0 && + (returnType == Boolean::class.java || + returnType == Boolean::class.javaObjectType) + } + } +} + +// Construct a map of PropertyDescriptors by name, by merging the raw field map with the map of classified property methods +private fun Map>.toClassProperties(fieldMap: Map): Map { + val result = mutableMapOf() + + // Fields for which we have no property methods + for ((name, field) in fieldMap) { + if (name !in keys) { + result[name] = PropertyDescriptor(field, null, null) + } + } + + for ((name, methodMap) in this) { + result[name] = PropertyDescriptor( + fieldMap[name], + methodMap[SET], + methodMap[GET] ?: methodMap[IS] + ) + } + + return result +} + +// Select the least generic of two methods by a type associated with each. +private fun leastGenericBy(feature: Method.() -> Type, first: Method, second: Method) = + if (first.feature().isSupertypeOf(second.feature())) second else first + +// Throw an exception if any property descriptor is inconsistent, e.g. the types don't match +private fun Map.validated() = apply { + forEach { _, value -> value.validate() } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt index e83c5c4119..3b3ee33478 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt @@ -19,8 +19,8 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true - private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true + private val isInterface: Boolean get() = resolvedType.asClass().isInterface + private val isJVMPrimitive: Boolean get() = resolvedType.asClass().isPrimitive private fun generateType(): String { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index f393fb7aad..f2820cc505 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -2,15 +2,12 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken -import net.corda.core.KeepForDJVM import net.corda.core.internal.isConcreteClass -import net.corda.core.internal.isPublic import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.* import java.lang.reflect.Field import java.util.* @@ -26,42 +23,37 @@ import kotlin.reflect.jvm.javaType /** * Code for finding the constructor we will use for deserialization. * - * If there's only one constructor, it selects that. If there are two and one is the default, it selects the other. - * Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is - * annotated with [@ConstructorForDeserialization]. It will report an error if more than one constructor is annotated. + * If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen. + * An error is reported if more than one constructor is annotated. + * + * Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique + * constructor or, if there are two and one is the default no-argument constructor, the non-default constructor. */ -fun constructorForDeserialization(type: Type): KFunction? { - val clazz: Class<*> = type.asClass()!! - if (clazz.isConcreteClass) { - var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor - var annotatedCount = 0 - val kotlinConstructors = clazz.kotlin.constructors - val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } - - for (kotlinConstructor in kotlinConstructors) { - if (preferredCandidate == null && kotlinConstructors.size == 1) { - preferredCandidate = kotlinConstructor - } else if (preferredCandidate == null && - kotlinConstructors.size == 2 && - hasDefault && - kotlinConstructor.parameters.isNotEmpty() - ) { - preferredCandidate = kotlinConstructor - } else if (kotlinConstructor.findAnnotation() != null) { - if (annotatedCount++ > 0) { - throw AMQPNotSerializableException( - type, - "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") - } - preferredCandidate = kotlinConstructor - } - } - - return preferredCandidate?.apply { isAccessible = true } - ?: throw AMQPNotSerializableException(type, "No constructor for deserialization found for $clazz.") - } else { - return null +fun constructorForDeserialization(type: Type): KFunction { + val clazz = type.asClass().apply { + if (!isConcreteClass) throw AMQPNotSerializableException(type, + "Cannot find deserialisation constructor for non-concrete class $this") } + + val kotlinCtors = clazz.kotlin.constructors + + val annotatedCtors = kotlinCtors.filter { it.findAnnotation() != null } + if (annotatedCtors.size > 1) throw AMQPNotSerializableException( + type, + "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") + + val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() } + val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor } + + val preferredCandidate = annotatedCtors.firstOrNull() ?: + clazz.kotlin.primaryConstructor ?: + when(nonDefaultCtors.size) { + 1 -> nonDefaultCtors.first() + 0 -> defaultCtor ?: throw AMQPNotSerializableException(type, "No constructor found for $clazz.") + else -> throw AMQPNotSerializableException(type, "No unique non-default constructor found for $clazz.") + } + + return preferredCandidate.apply { isAccessible = true } } /** @@ -75,145 +67,13 @@ fun constructorForDeserialization(type: Type): KFunction? { fun propertiesForSerialization( kotlinConstructor: KFunction?, type: Type, - factory: SerializerFactory): PropertySerializers { - return PropertySerializers.make( + factory: SerializerFactory): PropertySerializers = PropertySerializers.make( if (kotlinConstructor != null) { propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) } else { - propertiesForSerializationFromAbstract(type.asClass()!!, type, factory) + propertiesForSerializationFromAbstract(type.asClass(), type, factory) }.sortedWith(PropertyAccessor) ) -} - -/** - * Encapsulates the property of a class and its potential getter and setter methods. - * - * @property field a property of a class. - * @property setter the method of a class that sets the field. Determined by locating - * a function called setXyz on the class for the property named in field as xyz. - * @property getter the method of a class that returns a fields value. Determined by - * locating a function named getXyz for the property named in field as xyz. - */ -@KeepForDJVM -data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) { - override fun toString() = StringBuilder("").apply { - appendln("Property - ${field?.name ?: "null field"}\n") - appendln(" getter - ${getter?.name ?: "no getter"}") - appendln(" setter - ${setter?.name ?: "no setter"}") - appendln(" iser - ${iser?.name ?: "no isXYZ defined"}") - }.toString() - - constructor() : this(null, null, null, null) - - fun preferredGetter(): Method? = getter ?: iser -} - -object PropertyDescriptorsRegex { - // match an uppercase letter that also has a corresponding lower case equivalent - val re = Regex("(?get|set|is)(?\\p{Lu}.*)") -} - -/** - * Collate the properties of a class and match them with their getter and setter - * methods as per a JavaBean. - * - * for a property - * exampleProperty - * - * We look for methods - * setExampleProperty - * getExampleProperty - * isExampleProperty - * - * Where setExampleProperty must return a type compatible with exampleProperty, getExampleProperty must - * take a single parameter of a type compatible with exampleProperty and isExampleProperty must - * return a boolean - */ -fun Class.propertyDescriptors(): Map { - val classProperties = mutableMapOf() - - var clazz: Class? = this - - do { - clazz!!.declaredFields.forEach { property -> - classProperties.computeIfAbsent(property.name) { - PropertyDescriptor() - }.apply { - this.field = property - } - } - clazz = clazz.superclass - } while (clazz != null) - - // - // Running as two loops rather than one as we need to ensure we have captured all of the properties - // before looking for interacting methods and need to cope with the class hierarchy introducing - // new properties / methods - // - clazz = this - do { - // Note: It is possible for a class to have multiple instances of a function where the types - // differ. For example: - // interface I { val a: T } - // class D(override val a: String) : I - // instances of D will have both - // getA - returning a String (java.lang.String) and - // getA - returning an Object (java.lang.Object) - // In this instance we take the most derived object - // - // In addition, only getters that take zero parameters and setters that take a single - // parameter will be considered - clazz!!.declaredMethods?.map { func -> - if (!func.isPublic) return@map - if (func.name == "getClass") return@map - - PropertyDescriptorsRegex.re.find(func.name)?.apply { - // matching means we have an func getX where the property could be x or X - // so having pre-loaded all of the properties we try to match to either case. If that - // fails the getter doesn't refer to a property directly, but may refer to a constructor - // parameter that shadows a property - val properties = - classProperties[groups[2]!!.value] ?: classProperties[groups[2]!!.value.decapitalize()] ?: - // take into account those constructor properties that don't directly map to a named - // property which are, by default, already added to the map - classProperties.computeIfAbsent(groups[2]!!.value) { PropertyDescriptor() } - - properties.apply { - when (groups[1]!!.value) { - "set" -> { - if (func.parameterCount == 1) { - if (setter == null) setter = func - else if (TypeToken.of(setter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - setter = func - } - } - } - "get" -> { - if (func.parameterCount == 0) { - if (getter == null) getter = func - else if (TypeToken.of(getter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - getter = func - } - } - } - "is" -> { - if (func.parameterCount == 0) { - val rtnType = TypeToken.of(func.genericReturnType) - if ((rtnType == TypeToken.of(Boolean::class.java)) - || (rtnType == TypeToken.of(Boolean::class.javaObjectType))) { - if (iser == null) iser = func - } - } - } - } - } - } - } - clazz = clazz.superclass - } while (clazz != null) - - return classProperties -} /** * From a constructor, determine which properties of a class are to be serialized. @@ -235,66 +95,48 @@ internal fun propertiesForSerializationFromConstructor( // think you could inspect the parameter and check the isSynthetic flag but that is always // false so given the naming convention is specified by the standard we can just check for // this - if (kotlinConstructor.javaConstructor?.parameterCount ?: 0 > 0 && - kotlinConstructor.javaConstructor?.parameters?.get(0)?.name == "this$0" - ) { - throw SyntheticParameterException(type) + kotlinConstructor.javaConstructor?.apply { + if (parameterCount > 0 && parameters[0].name == "this$0") throw SyntheticParameterException(type) } if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) { return propertiesForSerializationFromSetters(classProperties, type, factory) } - return mutableListOf().apply { - kotlinConstructor.parameters.withIndex().forEach { param -> - // name cannot be null, if it is then this is a synthetic field and we will have bailed - // out prior to this - val name = param.value.name!! - - // We will already have disambiguated getA for property A or a but we still need to cope - // with the case we don't know the case of A when the parameter doesn't match a property - // but has a getter - val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] - ?: throw AMQPNotSerializableException(type, - "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") - - // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't - // *for *know* we switch to a reflection based method - val propertyReader = if (matchingProperty.getter != null) { - val getter = matchingProperty.getter ?: throw AMQPNotSerializableException( - type, - "Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name" - + "looks anonymous, check that you have the -parameters option specified in the " - + "Java compiler. Alternately, provide a proxy serializer " - + "(SerializationCustomSerializer) if recompiling isn't an option.") - - val returnType = resolveTypeVariables(getter.genericReturnType, type) - if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { - throw AMQPNotSerializableException( - type, - "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " + - "parameter type \"${param.value.type.javaType}\"") - } - - Pair(PublicPropertyReader(getter), returnType) - } else { - val field = classProperties[name]!!.field - ?: throw AMQPNotSerializableException(type, - "No property matching constructor parameter named - \"$name\" - " + - "of \"$clazz\". If using Java, check that you have the -parameters option specified " + - "in the Java compiler. Alternately, provide a proxy serializer " + - "(SerializationCustomSerializer) if recompiling isn't an option") - - Pair(PrivatePropertyReader(field, type), resolveTypeVariables(field.genericType, type)) - } - - this += PropertyAccessorConstructor( - param.index, - PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) - } + return kotlinConstructor.parameters.withIndex().map { param -> + toPropertyAccessorConstructor(param.index, param.value, classProperties, type, clazz, factory) } } +private fun toPropertyAccessorConstructor(index: Int, param: KParameter, classProperties: Map, type: Type, clazz: Class, factory: SerializerFactory): PropertyAccessorConstructor { + // name cannot be null, if it is then this is a synthetic field and we will have bailed + // out prior to this + val name = param.name!! + + // We will already have disambiguated getA for property A or a but we still need to cope + // with the case we don't know the case of A when the parameter doesn't match a property + // but has a getter + val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] + ?: throw AMQPNotSerializableException(type, + "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") + + // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't + // *for *now* we switch to a reflection based method + val propertyReader = matchingProperty.getter?.let { getter -> + getPublicPropertyReader(getter, type, param, name, clazz) + } ?: matchingProperty.field?.let { field -> + getPrivatePropertyReader(field, type) + } ?: throw AMQPNotSerializableException(type, + "No property matching constructor parameter named - \"$name\" - " + + "of \"${param}\". If using Java, check that you have the -parameters option specified " + + "in the Java compiler. Alternately, provide a proxy serializer " + + "(SerializationCustomSerializer) if recompiling isn't an option") + + return PropertyAccessorConstructor( + index, + PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) +} + /** * If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters * and use those @@ -302,107 +144,83 @@ internal fun propertiesForSerializationFromConstructor( fun propertiesForSerializationFromSetters( properties: Map, type: Type, - factory: SerializerFactory): List { - return mutableListOf().apply { - var idx = 0 + factory: SerializerFactory): List = + properties.asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry - properties.forEach { property -> - val getter: Method? = property.value.preferredGetter() - val setter: Method? = property.value.setter + val getter = property.getter + val setter = property.setter - if (getter == null || setter == null) return@forEach + if (getter == null || setter == null) return@map null - if (setter.parameterCount != 1) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} takes too many arguments") - } - - val setterType = setter.genericParameterTypes[0]!! - - if ((property.value.field != null) && - (!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType))) - ) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet underlying type is " + - "${property.value.field?.genericType!!}") - } - - // Make sure the getter returns the same type (within inheritance bounds) the setter accepts. - if (!(TypeToken.of(getter.genericReturnType).isSupertypeOf(setterType))) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet the defined getter returns a value of type " + - "${getter.returnType} [${getter.genericReturnType}]") - } - this += PropertyAccessorGetterSetter( - idx++, - PropertySerializer.make(property.key, PublicPropertyReader(getter), - resolveTypeVariables(getter.genericReturnType, type), factory), + PropertyAccessorGetterSetter( + index, + PropertySerializer.make( + name, + PublicPropertyReader(getter), + resolveTypeVariables(getter.genericReturnType, type), + factory), setter) - } - } -} + }.filterNotNull().toList() -private fun constructorParamTakesReturnTypeOfGetter( - getterReturnType: Type, - rawGetterReturnType: Type, - param: KParameter): Boolean { +private fun getPrivatePropertyReader(field: Field, type: Type) = + PrivatePropertyReader(field, type) to resolveTypeVariables(field.genericType, type) + +private fun getPublicPropertyReader(getter: Method, type: Type, param: KParameter, name: String, clazz: Class): Pair { + val returnType = resolveTypeVariables(getter.genericReturnType, type) val paramToken = TypeToken.of(param.type.javaType) val rawParamType = TypeToken.of(paramToken.rawType) - return paramToken.isSupertypeOf(getterReturnType) - || paramToken.isSupertypeOf(rawGetterReturnType) - // cope with the case where the constructor parameter is a generic type (T etc) but we - // can discover it's raw type. When bounded this wil be the bounding type, unbounded - // generics this will be object - || rawParamType.isSupertypeOf(getterReturnType) - || rawParamType.isSupertypeOf(rawGetterReturnType) + if (!(paramToken.isSupertypeOf(returnType) + || paramToken.isSupertypeOf(getter.genericReturnType) + // cope with the case where the constructor parameter is a generic type (T etc) but we + // can discover it's raw type. When bounded this wil be the bounding type, unbounded + // generics this will be object + || rawParamType.isSupertypeOf(returnType) + || rawParamType.isSupertypeOf(getter.genericReturnType))) { + throw AMQPNotSerializableException( + type, + "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" " + + "but differs from constructor parameter type \"${param.type.javaType}\"") + } + + return PublicPropertyReader(getter) to returnType } private fun propertiesForSerializationFromAbstract( clazz: Class<*>, type: Type, - factory: SerializerFactory): List { - val properties = clazz.propertyDescriptors() - - return mutableListOf().apply { - properties.toList().withIndex().forEach { - val getter = it.value.second.getter ?: return@forEach - if (it.value.second.field == null) return@forEach + factory: SerializerFactory): List = + clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry + if (property.getter == null || property.field == null) return@map null + val getter = property.getter val returnType = resolveTypeVariables(getter.genericReturnType, type) - this += PropertyAccessorConstructor( - it.index, - PropertySerializer.make(it.value.first, PublicPropertyReader(getter), returnType, factory)) - } - } -} -internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List { - val interfaces = LinkedHashSet() - exploreType(type, interfaces, serializerFactory) - return interfaces.toList() -} + PropertyAccessorConstructor( + index, + PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)) + }.filterNotNull().toList() -private fun exploreType(type: Type?, interfaces: MutableSet, serializerFactory: SerializerFactory) { - val clazz = type?.asClass() - if (clazz != null) { - if (clazz.isInterface) { - if (serializerFactory.whitelist.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. - else interfaces += type - } - for (newInterface in clazz.genericInterfaces) { - if (newInterface !in interfaces) { - exploreType(resolveTypeVariables(newInterface, type), interfaces, serializerFactory) - } - } - val superClass = clazz.genericSuperclass ?: return - exploreType(resolveTypeVariables(superClass, type), interfaces, serializerFactory) +internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List = + exploreType(type, serializerFactory).toList() + +private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet = LinkedHashSet()): MutableSet { + val clazz = type.asClass() + + if (clazz.isInterface) { + // Ignore classes we've already seen, and stop exploring once we reach a branch that has no `CordaSerializable` + // annotation or whitelisting. + if (clazz in interfaces || serializerFactory.whitelist.isNotWhitelisted(clazz)) return interfaces + else interfaces += type } + + (clazz.genericInterfaces.asSequence() + clazz.genericSuperclass) + .filterNotNull() + .forEach { exploreType(resolveTypeVariables(it, type), serializerFactory, interfaces) } + + return interfaces } /** @@ -459,21 +277,23 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { } } -internal fun Type.asClass(): Class<*>? { - return when { - this is Class<*> -> this - this is ParameterizedType -> this.rawType.asClass() - this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass() - this is TypeVariable<*> -> this.bounds.first().asClass() - this is WildcardType -> this.upperBounds.first().asClass() - else -> null +internal fun Type.asClass(): Class<*> { + return when(this) { + is Class<*> -> this + is ParameterizedType -> this.rawType.asClass() + is GenericArrayType -> this.genericComponentType.asClass().arrayClass() + is TypeVariable<*> -> this.bounds.first().asClass() + is WildcardType -> this.upperBounds.first().asClass() + // Per https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Type.html, + // there is nothing else that it can be, so this can never happen. + else -> throw UnsupportedOperationException("Cannot convert $this to class") } } internal fun Type.asArray(): Type? { - return when { - this is Class<*> -> this.arrayClass() - this is ParameterizedType -> DeserializedGenericArrayType(this) + return when(this) { + is Class<*> -> this.arrayClass() + is ParameterizedType -> DeserializedGenericArrayType(this) else -> null } } @@ -506,7 +326,7 @@ internal fun Type.isSubClassOf(type: Type): Boolean { // ByteArrays, primitives and boxed primitives are not stored in the object history internal fun suitableForObjectReference(type: Type): Boolean { val clazz = type.asClass() - return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) + return type != ByteArray::class.java && (!clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) } /** @@ -519,7 +339,7 @@ internal enum class CommonPropertyNames { fun ClassWhitelist.requireWhitelisted(type: Type) { - if (!this.isWhitelisted(type.asClass()!!)) { + if (!this.isWhitelisted(type.asClass())) { throw AMQPNotSerializableException( type, "Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 657c72d99b..8c869e2eda 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -1,7 +1,6 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives -import com.google.common.reflect.TypeResolver import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.StubOutForDJVM @@ -54,7 +53,7 @@ open class SerializerFactory( val whitelist: ClassWhitelist, val classCarpenter: ClassCarpenter, private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - val fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, private val serializersByType: MutableMap>, val serializersByDescriptor: MutableMap>, private val customSerializers: MutableList, @@ -66,13 +65,13 @@ open class SerializerFactory( constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, classCarpenter, evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, ConcurrentHashMap(), ConcurrentHashMap(), CopyOnWriteArrayList(), @@ -86,18 +85,16 @@ open class SerializerFactory( carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, onlyCustomSerializers) - init { - fingerPrinter.setOwner(this) - } + val fingerPrinter by lazy { fingerPrinterConstructor(this) } val classloader: ClassLoader get() = classCarpenter.classloader @@ -118,11 +115,9 @@ open class SerializerFactory( // can be useful to enable but will be *extremely* chatty if you do logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" } - val declaredClass = declaredType.asClass() ?: throw AMQPNotSerializableException( - declaredType, - "Declared types of $declaredType are not supported.") - - val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + val declaredClass = declaredType.asClass() + val actualType: Type = if (actualClass == null) declaredType + else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType val serializer = when { // Declared class may not be set to Collection, but actual class could be a collection. @@ -166,78 +161,6 @@ open class SerializerFactory( return serializer } - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, - * based on the declared type. - */ - // TODO: test GenericArrayType - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, - declaredType: Type): Type? = when (declaredType) { - is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) - // Nothing to infer, otherwise we'd have ParameterizedType - is Class<*> -> actualClass - is GenericArrayType -> { - val declaredComponent = declaredType.genericComponentType - inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() - } - is TypeVariable<*> -> actualClass - is WildcardType -> actualClass - else -> null - } - - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared - * type, which must be a [ParameterizedType]. - */ - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { - if (actualClass == null || declaredClass == actualClass) { - return null - } else if (declaredClass.isAssignableFrom(actualClass)) { - return if (actualClass.typeParameters.isNotEmpty()) { - // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them - // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables - val implementationChain: List? = findPathToDeclared(actualClass, declaredType, mutableListOf()) - if (implementationChain != null) { - val start = implementationChain.last() - val rest = implementationChain.dropLast(1).drop(1) - val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> - val newResolved = resolved.resolveType(chainEntry) - TypeResolver().where(chainEntry, newResolved) - } - // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. - val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) - val resolvedType = resolver.resolveType(endType) - resolvedType - } else throw AMQPNotSerializableException(declaredType, - "No inheritance path between actual $actualClass and declared $declaredType.") - } else actualClass - } else throw AMQPNotSerializableException( - declaredType, - "Found object of type $actualClass in a property expecting $declaredType") - } - - // Stop when reach declared type or return null if we don't find it. - private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList): List? { - chain.add(startingType) - val startingClass = startingType.asClass() - if (startingClass == declaredType.asClass()) { - // We're done... - return chain - } - // Now explore potential options of superclass and all interfaces - val superClass = startingClass?.genericSuperclass - val superClassChain = if (superClass != null) { - val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass) - findPathToDeclared(resolved, declaredType, ArrayList(chain)) - } else null - if (superClassChain != null) return superClassChain - for (iface in startingClass?.genericInterfaces ?: emptyArray()) { - val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface) - return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue - } - return null - } - /** * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types * contained in the [Schema]. @@ -349,7 +272,7 @@ open class SerializerFactory( // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, classloader) return get( - type.asClass() ?: throw AMQPNotSerializableException(type, "Unable to build composite type for $type"), + type.asClass(), type) } @@ -402,7 +325,7 @@ open class SerializerFactory( // super type. Could be done, but do we need it? for (customSerializer in customSerializers) { if (customSerializer.isSerializerFor(clazz)) { - val declaredSuperClass = declaredType.asClass()?.superclass + val declaredSuperClass = declaredType.asClass().superclass return if (declaredSuperClass == null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt new file mode 100644 index 0000000000..72720add79 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt @@ -0,0 +1,94 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.reflect.TypeResolver +import java.lang.reflect.* + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, + * based on the declared type. + */ +// TODO: test GenericArrayType +fun inferTypeVariables(actualClass: Class<*>, + declaredClass: Class<*>, + declaredType: Type): Type? = when (declaredType) { + is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) + is GenericArrayType -> { + val declaredComponent = declaredType.genericComponentType + inferTypeVariables(actualClass.componentType, declaredComponent.asClass(), declaredComponent)?.asArray() + } + // Nothing to infer, otherwise we'd have ParameterizedType + is Class<*> -> actualClass + is TypeVariable<*> -> actualClass + is WildcardType -> actualClass + else -> throw UnsupportedOperationException("Cannot infer type variables for type $declaredType") +} + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared + * type, which must be a [ParameterizedType]. + */ +private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { + if (declaredClass == actualClass) { + return null + } + + if (!declaredClass.isAssignableFrom(actualClass)) { + throw AMQPNotSerializableException( + declaredType, + "Found object of type $actualClass in a property expecting $declaredType") + } + + if (actualClass.typeParameters.isEmpty()) { + return actualClass + } + // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them + // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables + val implementationChain: List = findPathToDeclared(actualClass, declaredType)?.toList() + ?: throw AMQPNotSerializableException( + declaredType, + "No inheritance path between actual $actualClass and declared $declaredType.") + + val start = implementationChain.last() + val rest = implementationChain.dropLast(1).drop(1) + val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> + val newResolved = resolved.resolveType(chainEntry) + TypeResolver().where(chainEntry, newResolved) + } + // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. + val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) + return resolver.resolveType(endType) +} + +// Stop when reach declared type or return null if we don't find it. +private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: Sequence = emptySequence()): Sequence? { + val extendedChain = chain + startingType + val startingClass = startingType.asClass() + + if (startingClass == declaredType.asClass()) { + // We're done... + return extendedChain + } + + val resolver = { type: Type -> + TypeResolver().where( + startingClass.asParameterizedType(), + startingType.asParameterizedType()) + .resolveType(type) + } + + // Now explore potential options of superclass and all interfaces + return findPathViaGenericSuperclass(startingClass, resolver, declaredType, extendedChain) + ?: findPathViaInterfaces(startingClass, resolver, declaredType, extendedChain) +} + +private fun findPathViaInterfaces(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence): Sequence? = + startingClass.genericInterfaces.asSequence().map { + findPathToDeclared(resolver(it), declaredType, extendedChain) + }.filterNotNull().firstOrNull() + + +private fun findPathViaGenericSuperclass(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence): Sequence? { + val superClass = startingClass.genericSuperclass ?: return null + return findPathToDeclared(resolver(superClass), declaredType, extendedChain) +} + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 135e93710c..3b4b03800e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -25,7 +25,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy + propertiesForSerializationFromConstructor(constructor, obj.javaClass, factory).forEach { property -> extraProperties[property.serializer.name] = property.serializer.propertyReader.read(obj) } } catch (e: NotSerializableException) { @@ -52,7 +52,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy FingerPrinterTesting() }) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) From dbc10884173f3e9ff846e5186edd92b50ce3ab5f Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Thu, 30 Aug 2018 13:40:02 +0100 Subject: [PATCH 009/119] CORDA-1939 Regression: Liquibase fails to migrate existing pre-liquibase database (#3855) * Bugfix for corda.jar manually deployed without coping the matching corda-finance-VERSION.jar. If the older finance cordapp version is detected (which doesn't have Liquibase migration scripts) fail node at startup and print message: "Could not create the DataSource: Detected incompatible corda-finance cordapp without database migration scripts, replace the existing corda-finance-VERSION.jar with the latest one." Since coda-finance is an optional cordapp,the presence of Liquibase scripts is only checked if corda-finance-VERSION.jar is present in cordapps folder. * Allow to start using Liquibase from any point of 4.0-SNAPSHOT before Liquibase was introduced (not only from 3.0/3.X versions) - 2 database changes introduced after 3.2 but before Liquibase are now conditional database changes. --- .../internal/persistence/SchemaMigration.kt | 60 +++++++++++-------- .../node/services/schema/NodeSchemaService.kt | 2 +- .../node-core.changelog-tx-mapping.xml | 1 + .../migration/vault-schema.changelog-v5.xml | 16 ++--- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index 4cafba895c..e7067a3eac 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -106,10 +106,16 @@ class SchemaMigration( /** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and mark changesets are executed. */ private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean { + //workaround to detect that if Corda finance module is in use then the most recent version with Liquibase migration scripts was deployed + if (schemas.any { schema -> + (schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") + && schema.migrationResource == null + }) + throw DatabaseMigrationException("Detected incompatible corda-finance cordapp without database migration scripts, replace the existing corda-finance-VERSION.jar with the latest one.") + val isExistingDBWithoutLiquibase = dataSource.connection.use { - it.metaData.getTables(null, null, "NODE%", null).next() && - !it.metaData.getTables(null, null, "DATABASECHANGELOG", null).next() && - !it.metaData.getTables(null, null, "DATABASECHANGELOGLOCK", null).next() + (it.metaData.getTables(null, null, "NODE%", null).next() && + !it.metaData.getTables(null, null, "DATABASECHANGELOG%", null).next()) } when { isExistingDBWithoutLiquibase && existingCheckpoints -> throw CheckpointsException() @@ -119,29 +125,31 @@ class SchemaMigration( dataSource.connection.use { connection -> // Schema migrations pre release 4.0 - val preV4Baseline = - listOf("migration/common.changelog-init.xml", - "migration/node-info.changelog-init.xml", - "migration/node-info.changelog-v1.xml", - "migration/node-info.changelog-v2.xml", - "migration/node-core.changelog-init.xml", - "migration/node-core.changelog-v3.xml", - "migration/node-core.changelog-v4.xml", - "migration/node-core.changelog-v5.xml", - "migration/node-core.changelog-pkey.xml", - "migration/vault-schema.changelog-init.xml", - "migration/vault-schema.changelog-v3.xml", - "migration/vault-schema.changelog-v4.xml", - "migration/vault-schema.changelog-pkey.xml", - "migration/cash.changelog-init.xml", - "migration/cash.changelog-v1.xml", - "migration/commercial-paper.changelog-init.xml", - "migration/commercial-paper.changelog-v1.xml") + - if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" }) - listOf("migration/node-notary.changelog-init.xml", - "migration/node-notary.changelog-v1.xml", - "migration/vault-schema.changelog-pkey.xml") - else emptyList() + val preV4Baseline = mutableListOf("migration/common.changelog-init.xml", + "migration/node-info.changelog-init.xml", + "migration/node-info.changelog-v1.xml", + "migration/node-info.changelog-v2.xml", + "migration/node-core.changelog-init.xml", + "migration/node-core.changelog-v3.xml", + "migration/node-core.changelog-v4.xml", + "migration/node-core.changelog-v5.xml", + "migration/node-core.changelog-pkey.xml", + "migration/vault-schema.changelog-init.xml", + "migration/vault-schema.changelog-v3.xml", + "migration/vault-schema.changelog-v4.xml", + "migration/vault-schema.changelog-pkey.xml") + + if (schemas.any { schema -> schema.migrationResource == "cash.changelog-master" }) + preV4Baseline.addAll(listOf("migration/cash.changelog-init.xml", + "migration/cash.changelog-v1.xml")) + + if (schemas.any { schema -> schema.migrationResource == "commercial-paper.changelog-master" }) + preV4Baseline.addAll(listOf("migration/commercial-paper.changelog-init.xml", + "migration/commercial-paper.changelog-v1.xml")) + + if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" }) + preV4Baseline.addAll(listOf("migration/node-notary.changelog-init.xml", + "migration/node-notary.changelog-v1.xml")) val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader) val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection))) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 8116afc681..3fdaa4b798 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -69,7 +69,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap() fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones - schema::class.simpleName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.simpleName == "net.corda.finance.schemas.CommercialPaperSchemaV1" } + schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" } override val schemaOptions: Map = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() }) diff --git a/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml b/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml index 65ff155ac6..6ea7eb39d5 100644 --- a/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml +++ b/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml @@ -3,6 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v5.xml b/node/src/main/resources/migration/vault-schema.changelog-v5.xml index 6ed365a363..86064e9deb 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-v5.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-v5.xml @@ -1,15 +1,15 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + + - - - - - + + + + + From f22d9ad411cbc68766102acdf55faf771149adec Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Fri, 31 Aug 2018 09:34:42 +0100 Subject: [PATCH 010/119] CORDA-1938 Adding version info to the node info submission request (#3854) --- docs/source/permissioning.rst | 2 +- .../main/kotlin/net/corda/node/internal/AbstractNode.kt | 2 +- .../net/corda/node/services/network/NetworkMapClient.kt | 7 +++++-- .../registration/HTTPNetworkRegistrationService.kt | 4 +++- .../corda/node/services/network/NetworkMapClientTest.kt | 4 +++- .../corda/node/services/network/NetworkMapUpdaterTest.kt | 4 +++- .../node/services/network/NetworkParametersReaderTest.kt | 3 ++- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index 0ea28af247..7db30b5c77 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -259,7 +259,7 @@ The protocol is: * If $URL = ``https://some.server.com/some/path`` * Node submits a PKCS#10 certificate signing request using HTTP POST to ``$URL/certificate``. It will have a MIME - type of ``application/octet-stream``. The ``Client-Version`` header is set to be "1.0". + type of ``application/octet-stream``. The ``Platform-Version`` header is set to be "1.0" and the ``Client-Version`` header to reflect the node software version. * The server returns an opaque string that references this request (let's call it ``$requestid``, or an HTTP error if something went wrong. * The returned request ID should be persisted to disk, to handle zones where approval may take a long time due to manual intervention being required. 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 64e5fb7cfc..40f0faf110 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -146,7 +146,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val checkpointStorage = DBCheckpointStorage() @Suppress("LeakingThis") val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() - val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL) } + val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } private val metricRegistry = MetricRegistry() val attachments = NodeAttachmentService(metricRegistry, database, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 69a2af2cd2..00b68c6098 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.utilities.registration.cacheControl import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NetworkMap @@ -23,7 +24,7 @@ import java.security.cert.X509Certificate import java.time.Duration import java.util.* -class NetworkMapClient(compatibilityZoneURL: URL) { +class NetworkMapClient(compatibilityZoneURL: URL, private val versionInfo: VersionInfo) { companion object { private val logger = contextLogger() } @@ -45,7 +46,9 @@ class NetworkMapClient(compatibilityZoneURL: URL) { fun ackNetworkParametersUpdate(signedParametersHash: SignedData) { val ackURL = URL("$networkMapUrl/ack-parameters") logger.trace { "Sending network parameters with hash ${signedParametersHash.raw.deserialize()} approval to $ackURL." } - ackURL.post(signedParametersHash.serialize()) + ackURL.post(signedParametersHash.serialize(), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion) logger.trace { "Sent network parameters approval to $ackURL successfully." } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index ff37ecafad..3e422bc969 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -52,7 +52,9 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: } override fun submitRequest(request: PKCS10CertificationRequest): String { - return String(registrationURL.post(OpaqueBytes(request.encoded), "Client-Version" to "${versionInfo.platformVersion}")) + return String(registrationURL.post(OpaqueBytes(request.encoded), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion)) } } 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 99c915325d..492f2c39d1 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,6 +5,7 @@ import net.corda.core.crypto.sha256 import net.corda.core.internal.sign import net.corda.core.serialization.serialize import net.corda.core.utilities.seconds +import net.corda.node.VersionInfo import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -40,7 +41,8 @@ class NetworkMapClientTest { fun setUp() { server = NetworkMapServer(cacheTimeout) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")).apply { start(DEV_ROOT_CA.certificate) } + networkMapClient = NetworkMapClient(URL("http://$address"), + VersionInfo(1, "TEST", "TEST", "TEST")).apply { start(DEV_ROOT_CA.certificate) } } @After diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index ff4de39797..1c75c13524 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -13,6 +13,7 @@ import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.millis +import net.corda.node.VersionInfo import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY import net.corda.nodeapi.internal.NodeInfoAndSigned @@ -63,7 +64,8 @@ class NetworkMapUpdaterTest { fun setUp() { server = NetworkMapServer(cacheExpiryMs.millis) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")).apply { start(DEV_ROOT_CA.certificate) } + networkMapClient = NetworkMapClient(URL("http://$address"), + VersionInfo(1, "TEST", "TEST", "TEST")).apply { start(DEV_ROOT_CA.certificate) } } @After diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index 03c277fb07..08fa5c3a85 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -9,6 +9,7 @@ import net.corda.core.internal.readObject import net.corda.core.serialization.deserialize import net.corda.core.utilities.days import net.corda.core.utilities.seconds +import net.corda.node.VersionInfo import net.corda.node.internal.NetworkParametersReader import net.corda.nodeapi.internal.network.* import net.corda.testing.common.internal.testNetworkParameters @@ -41,7 +42,7 @@ class NetworkParametersReaderTest { fun setUp() { server = NetworkMapServer(cacheTimeout) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")) + networkMapClient = NetworkMapClient(URL("http://$address"), VersionInfo(1, "TEST", "TEST", "TEST")) networkMapClient.start(DEV_ROOT_CA.certificate) } From d7b85b4928e49161f01979febfa2c05920f80b31 Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Fri, 31 Aug 2018 09:35:06 +0100 Subject: [PATCH 011/119] CORDA-1934 Renaming INTERMEDIATE_CA certificate role to DOORMAN_CA certificate role (#3844) --- .../kotlin/net/corda/core/internal/CertRole.kt | 16 ++++++++-------- .../net/corda/core/internal/CertRoleTests.kt | 2 +- .../nodeapi/internal/crypto/X509Utilities.kt | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index 29d24b80e0..b53f8977e8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -24,22 +24,22 @@ import java.security.cert.X509Certificate // also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications. // TODO: Link to the specification once it has a permanent URL enum class CertRole(val validParents: NonEmptySet, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable { - /** Intermediate CA (Doorman service). */ - INTERMEDIATE_CA(NonEmptySet.of(null), false, false), + /** Signing certificate for the Doorman CA. */ + DOORMAN_CA(NonEmptySet.of(null), false, false), /** Signing certificate for the network map. */ NETWORK_MAP(NonEmptySet.of(null), false, false), /** Well known (publicly visible) identity of a service (such as notary). */ - SERVICE_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA), true, true), + SERVICE_IDENTITY(NonEmptySet.of(DOORMAN_CA), true, true), /** Node level CA from which the TLS and well known identity certificates are issued. */ - NODE_CA(NonEmptySet.of(INTERMEDIATE_CA), false, false), + NODE_CA(NonEmptySet.of(DOORMAN_CA), false, false), /** Transport layer security certificate for a node. */ TLS(NonEmptySet.of(NODE_CA), false, false), /** Well known (publicly visible) identity of a legal entity. */ - // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [INTERMEDIATE_CA] is also added + // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [DOORMAN_CA] is also added // as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its - // own Root CA and Intermediate CA directly issues Legal Identities; thus, there won't be a requirement for - // Node CA). Consider removing [INTERMEDIATE_CA] from [validParents] when the model is finalised. - LEGAL_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA, NODE_CA), true, true), + // own Root CA and Doorman CA directly issues Legal Identities; thus, there won't be a requirement for + // Node CA). Consider removing [DOORMAN_CA] from [validParents] when the model is finalised. + LEGAL_IDENTITY(NonEmptySet.of(DOORMAN_CA, NODE_CA), true, true), /** Confidential (limited visibility) identity of a legal entity. */ CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false); diff --git a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt index 71fba5a51e..60f81927c8 100644 --- a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt +++ b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt @@ -8,7 +8,7 @@ import kotlin.test.assertFailsWith class CertRoleTests { @Test fun `should deserialize valid value`() { - val expected = CertRole.INTERMEDIATE_CA + val expected = CertRole.DOORMAN_CA val actual = CertRole.getInstance(ASN1Integer(1L)) assertEquals(expected, actual) } 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 1582c5d1aa..d2efc05c85 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 @@ -402,7 +402,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true, - role = CertRole.INTERMEDIATE_CA + role = CertRole.DOORMAN_CA ), NETWORK_MAP( From 8e6a9818b704ab628bbf904b181e90f99d08d920 Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Fri, 31 Aug 2018 09:35:36 +0100 Subject: [PATCH 012/119] CORDA-1932 Fixing network map certificate path verification (#3843) * CORDA-1932 Fixing network map certificate path verification to be certificate hierarchy agnostic * Addressing review comments --- .../core/internal/DigitalSignatureWithCert.kt | 10 ++++++++- .../nodeapi/internal/network/NetworkMap.kt | 22 ++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt index 04ca25c6cb..d9414f0c18 100644 --- a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt +++ b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt @@ -7,16 +7,24 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.utilities.OpaqueBytes +import java.security.cert.CertPath import java.security.cert.X509Certificate // TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation // and the correct exceptions will be need to be annotated /** A digital signature with attached certificate of the public key. */ -class DigitalSignatureWithCert(val by: X509Certificate, bytes: ByteArray) : DigitalSignature(bytes) { +open class DigitalSignatureWithCert(val by: X509Certificate, bytes: ByteArray) : DigitalSignature(bytes) { fun verify(content: ByteArray): Boolean = by.publicKey.verify(content, this) fun verify(content: OpaqueBytes): Boolean = verify(content.bytes) } +/** + * A digital signature with attached certificate path. The first certificate in the path corresponds to the data signer key. + * @param path certificate path associated with this signature + * @param bytes signature bytes + */ +class DigitalSignatureWithCertPath(val path: List, bytes: ByteArray): DigitalSignatureWithCert(path.first(), bytes) + /** Similar to [SignedData] but instead of just attaching the public key, the certificate for the key is attached instead. */ @CordaSerializable class SignedDataWithCert(val raw: SerializedBytes, val sig: DigitalSignatureWithCert) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index 6b2da4bfeb..45399ac3e9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt @@ -3,12 +3,11 @@ package net.corda.nodeapi.internal.network import net.corda.core.crypto.SecureHash import net.corda.core.internal.CertRole import net.corda.core.internal.DigitalSignatureWithCert +import net.corda.core.internal.DigitalSignatureWithCertPath import net.corda.core.internal.SignedDataWithCert -import net.corda.core.internal.signWithCert import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializedBytes import net.corda.nodeapi.internal.crypto.X509Utilities import java.security.cert.X509Certificate import java.time.Instant @@ -57,20 +56,13 @@ data class ParametersUpdate( val updateDeadline: Instant ) -/** Verify that a Network Map certificate is issued by Root CA and its [CertRole] is correct. */ -// TODO: Current implementation works under the assumption that there are no intermediate CAs between Root and -// Network Map. Consider a more flexible implementation without the above assumption. - +/** Verify that a Network Map certificate path and its [CertRole] is correct. */ fun SignedDataWithCert.verifiedNetworkMapCert(rootCert: X509Certificate): T { require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" } - X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert) + val path = when (this.sig) { + is DigitalSignatureWithCertPath -> (sig as DigitalSignatureWithCertPath).path + else -> listOf(sig.by, rootCert) + } + X509Utilities.validateCertificateChain(rootCert, path) return verified() } - -class NetworkMapAndSigned private constructor(val networkMap: NetworkMap, val signed: SignedNetworkMap) { - constructor(networkMap: NetworkMap, signer: (SerializedBytes) -> DigitalSignatureWithCert) : this(networkMap, networkMap.signWithCert(signer)) - constructor(signed: SignedNetworkMap) : this(signed.verified(), signed) - - operator fun component1(): NetworkMap = networkMap - operator fun component2(): SignedNetworkMap = signed -} From 9544fac2c018a5cb96d99ebf25dcf151a0fa382e Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 31 Aug 2018 12:44:53 +0100 Subject: [PATCH 013/119] Clean up of MerkleTransaction.kt (#3880) Primarily making the deserialiseComponentGroup method simpler. --- .../core/transactions/MerkleTransaction.kt | 158 +++++++++++------- 1 file changed, 95 insertions(+), 63 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index dbc5ef95fb..aef380e153 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -3,12 +3,15 @@ package net.corda.core.transactions import net.corda.core.CordaException import net.corda.core.KeepForDJVM import net.corda.core.contracts.* +import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey import java.util.function.Predicate +import kotlin.reflect.KClass /** * Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate @@ -18,29 +21,29 @@ import java.util.function.Predicate */ abstract class TraversableTransaction(open val componentGroups: List) : CoreTransaction() { /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ - val attachments: List = deserialiseComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP, { SerializedBytes(it).deserialize() }) + val attachments: List = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP) /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ - override val inputs: List = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes(it).deserialize() }) + override val inputs: List = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP) /** Pointers to reference states, identified by (tx identity hash, output index). */ - override val references: List = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes(it).deserialize() }) + override val references: List = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP) - override val outputs: List> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) + override val outputs: List> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true) /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ val commands: List> = deserialiseCommands() override val notary: Party? = let { - val notaries: List = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes(it).deserialize() }) + val notaries: List = deserialiseComponentGroup(Party::class, NOTARY_GROUP) check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." } - if (notaries.isNotEmpty()) notaries[0] else null + notaries.firstOrNull() } val timeWindow: TimeWindow? = let { - val timeWindows: List = deserialiseComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP, { SerializedBytes(it).deserialize() }) + val timeWindows: List = deserialiseComponentGroup(TimeWindow::class, TIMEWINDOW_GROUP) check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." } - if (timeWindows.isNotEmpty()) timeWindows[0] else null + timeWindows.firstOrNull() } /** @@ -63,12 +66,16 @@ abstract class TraversableTransaction(open val componentGroups: List deserialiseComponentGroup(groupEnum: ComponentGroupEnum, deserialiseBody: (ByteArray) -> T): List { + private fun deserialiseComponentGroup(clazz: KClass, + groupEnum: ComponentGroupEnum, + attachmentsContext: Boolean = false): List { + val factory = SerializationFactory.defaultFactory + val context = factory.defaultContext.let { if (attachmentsContext) it.withAttachmentsClassLoader(attachments) else it } val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal } return if (group != null && group.components.isNotEmpty()) { group.components.mapIndexed { internalIndex, component -> try { - deserialiseBody(component.bytes) + factory.deserialize(component, clazz.java, context) } catch (e: MissingAttachmentsException) { throw e } catch (e: Exception) { @@ -87,11 +94,13 @@ abstract class TraversableTransaction(open val componentGroups: List>(it).deserialize() }) - val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) - val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } + val signersList: List> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP)) + val commandDataList: List = deserialiseComponentGroup(CommandData::class, COMMANDS_GROUP, attachmentsContext = true) + val group = componentGroups.firstOrNull { it.groupIndex == COMMANDS_GROUP.ordinal } return if (group is FilteredComponentGroup) { - check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" } + check(commandDataList.size <= signersList.size) { + "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" + } val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) } val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } if (leafIndices.isNotEmpty()) @@ -100,7 +109,9 @@ abstract class TraversableTransaction(open val componentGroups: List Command(commandData, signersList[index]) } } } @@ -145,47 +156,47 @@ class FilteredTransaction internal constructor( var signersIncluded = false fun filter(t: T, componentGroupIndex: Int, internalIndex: Int) { - if (filtering.test(t)) { - val group = filteredSerialisedComponents[componentGroupIndex] - // Because the filter passed, we know there is a match. We also use first Vs single as the init function - // of WireTransaction ensures there are no duplicated groups. - val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] - if (group == null) { - // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes - // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be - // a match on Map.get ensuring it will never return null. - filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) - filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } else { - group.add(serialisedComponent) - // If the group[componentGroupIndex] existed, then we guarantee that - // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. - filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } - // If at least one command is visible, then all command-signers should be visible as well. - // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. - if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) { - signersIncluded = true - val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal - // There exist commands, thus the signers group is not empty. - val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } - filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() - filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() - filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() - } + if (!filtering.test(t)) return + + val group = filteredSerialisedComponents[componentGroupIndex] + // Because the filter passed, we know there is a match. We also use first Vs single as the init function + // of WireTransaction ensures there are no duplicated groups. + val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] + if (group == null) { + // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes + // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be + // a match on Map.get ensuring it will never return null. + filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) + filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } else { + group.add(serialisedComponent) + // If the group[componentGroupIndex] existed, then we guarantee that + // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. + filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } + // If at least one command is visible, then all command-signers should be visible as well. + // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. + if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) { + signersIncluded = true + val signersGroupIndex = SIGNERS_GROUP.ordinal + // There exist commands, thus the signers group is not empty. + val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } + filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() + filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() + filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() } } fun updateFilteredComponents() { - wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.INPUTS_GROUP.ordinal, internalIndex) } - wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.OUTPUTS_GROUP.ordinal, internalIndex) } - wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) } - wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } - if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) - if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) - wtx.references.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.REFERENCES_GROUP.ordinal, internalIndex) } + wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) } + wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) } + wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) } + wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) } + if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0) + if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0) + wtx.references.forEachIndexed { internalIndex, it -> filter(it, REFERENCES_GROUP.ordinal, internalIndex) } // It is highlighted that because there is no a signers property in TraversableTransaction, // one cannot specifically filter them in or out. // The above is very important to ensure someone won't filter out the signers component group if at least one @@ -195,10 +206,17 @@ class FilteredTransaction internal constructor( // we decide to filter and attach this field to a FilteredTransaction. // An example would be to redact certain contract state types, but otherwise leave a transaction alone, // including the unknown new components. - wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } + wtx.componentGroups + .filter { it.groupIndex >= values().size } + .forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } } - fun createPartialMerkleTree(componentGroupIndex: Int) = PartialMerkleTree.build(MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), filteredComponentHashes[componentGroupIndex]!!) + fun createPartialMerkleTree(componentGroupIndex: Int): PartialMerkleTree { + return PartialMerkleTree.build( + MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), + filteredComponentHashes[componentGroupIndex]!! + ) + } fun createFilteredComponentGroups(): List { updateFilteredComponents() @@ -223,8 +241,11 @@ class FilteredTransaction internal constructor( @Throws(FilteredTransactionVerificationException::class) fun verify() { verificationCheck(groupHashes.isNotEmpty()) { "At least one component group hash is required" } - // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null components in WireTransaction). - verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Top level Merkle tree cannot be verified against transaction's id" } + // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null + // components in WireTransaction). + verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Top level Merkle tree cannot be verified against transaction's id" + } // For completely blind verification (no components are included). if (filteredComponentGroups.isEmpty()) return @@ -233,8 +254,12 @@ class FilteredTransaction internal constructor( filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) -> verificationCheck(groupIndex < groupHashes.size) { "There is no matching component group hash for group $groupIndex" } val groupMerkleRoot = groupHashes[groupIndex] - verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" } - verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" } + verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { + "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" + } + verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { + "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" + } } } @@ -281,7 +306,9 @@ class FilteredTransaction internal constructor( val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" } // Verify the top level Merkle tree from groupHashes. - visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" } + visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" + } } } @@ -296,15 +323,17 @@ class FilteredTransaction internal constructor( */ @Throws(ComponentVisibilityException::class) fun checkCommandVisibility(publicKey: PublicKey) { - val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal } + val commandSigners = componentGroups.firstOrNull { it.groupIndex == SIGNERS_GROUP.ordinal } val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners) val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size - visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" } + visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { + "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" + } } // Function to return number of expected commands to sign. private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int { - checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) + checkAllComponentsVisible(SIGNERS_GROUP) if (commandSigners == null) return 0 fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List { try { @@ -340,7 +369,10 @@ class FilteredTransaction internal constructor( */ @KeepForDJVM @CordaSerializable -data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { +data class FilteredComponentGroup(override val groupIndex: Int, + override val components: List, + val nonces: List, + val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { check(components.size == nonces.size) { "Size of transaction components and nonces do not match" } } From bdc67f453ec4b844b4c9c611e895f0c3c6dfa5c3 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Tue, 21 Aug 2018 17:43:15 +0100 Subject: [PATCH 014/119] Stop bootstrapper errors caused by maxTransactionSize > maxMessageSize --- .../src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt index 4ec5d47f02..c65fa79d27 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt @@ -46,7 +46,7 @@ interface Volume { minimumPlatformVersion = 1, notaries = it, maxMessageSize = 10485760, - maxTransactionSize = Int.MAX_VALUE, + maxTransactionSize = 10485760, modifiedTime = Instant.now(), epoch = 10, whitelistedContractImplementations = emptyMap()) From 91dfa277e3b4fa1f7b7a658e987193e452d6ed3f Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Mon, 3 Sep 2018 14:04:36 +0100 Subject: [PATCH 015/119] CORDA-1938 adding VersionInfo to the node info submission (#3886) --- .../net/corda/node/services/network/NetworkMapClient.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 00b68c6098..91a0e159c6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -39,7 +39,9 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val versionInfo: Versi fun publish(signedNodeInfo: SignedNodeInfo) { val publishURL = URL("$networkMapUrl/publish") logger.trace { "Publishing NodeInfo to $publishURL." } - publishURL.post(signedNodeInfo.serialize()) + publishURL.post(signedNodeInfo.serialize(), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion) logger.trace { "Published NodeInfo to $publishURL successfully." } } From 793a52c57a521f8f2935229049e809154b398046 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 3 Sep 2018 14:27:24 +0100 Subject: [PATCH 016/119] [CORDA-1542]: Exception error codes aren't stable (fixed). (#3887) --- .../net/corda/node/internal/NodeStartup.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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 c73dfea87e..dfa8d113a9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -147,8 +147,8 @@ open class NodeStartup(val args: Array) { val cause = this.cause return when { - cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace, cause.staticLocationBasedHash(visited + cause)) - else -> Objects.hash(this::class.java.name, stackTrace) + cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace.customHashCode(), cause.staticLocationBasedHash(visited + cause)) + else -> Objects.hash(this::class.java.name, stackTrace.customHashCode()) } } @@ -177,6 +177,19 @@ open class NodeStartup(val args: Array) { } } + private fun Array?.customHashCode(): Int { + + if (this == null) { + return 0 + } + return Arrays.hashCode(map { it?.customHashCode() ?: 0 }.toIntArray()) + } + + private fun StackTraceElement.customHashCode(): Int { + + return Objects.hash(StackTraceElement::class.java.name, methodName, lineNumber) + } + private fun configFileNotFoundMessage(configFile: Path): String { return """ Unable to load the node config file from '$configFile'. @@ -581,3 +594,5 @@ open class NodeStartup(val args: Array) { } } } + + From 643ad31736035aa75ae9ff90dc2b42c75111c074 Mon Sep 17 00:00:00 2001 From: LankyDan Date: Sun, 26 Aug 2018 15:36:50 +0100 Subject: [PATCH 017/119] Add tests to confirm that if a Hibernate error occurs the transaction will fail and no states will be persisted Add tests to NodeVaultServiceTest Add VaultFlowTest Add UniqueDummyFungibleContract and UniqueLinearContract to be used in the constraint tests --- .../services/vault/NodeVaultServiceTest.kt | 67 +++++++++++++- .../node/services/vault/VaultFlowTest.kt | 91 +++++++++++++++++++ .../vault/UniqueDummyFungibleContract.kt | 45 +++++++++ .../vault/UniqueDummyLinearContract.kt | 41 +++++++++ 4 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index da7081315a..3e4e2c84b4 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -34,7 +34,7 @@ import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock -import net.corda.testing.internal.vault.VaultFiller +import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -48,13 +48,15 @@ import java.math.BigDecimal import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors +import javax.persistence.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest { private companion object { - val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts") + val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts", + "net.corda.testing.internal.vault") val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) val bankOfCorda = TestIdentity(BOC_NAME) @@ -769,4 +771,65 @@ class NodeVaultServiceTest { // We should never see 2 or 7. } + + @Test + fun `Unique column constraint failing causes linear state to not persist to vault`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyLinearContract.State(listOf(megaCorp.party), "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy().states.size + }) + } + + @Test + fun `Unique column constraint failing causes fungible state to not persist to vault`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyFungibleContract.State(10.DOLLARS `issued by` DUMMY_CASH_ISSUER, megaCorp.party), UNIQUE_DUMMY_FUNGIBLE_CONTRACT_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy().states.size + }) + assertEquals(10.DOLLARS.quantity, database.transaction { + vaultService.queryBy().states.first().state.data.amount.quantity + }) + } + + @Test + fun `Unique column constraint failing causes all states in transaction to fail`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyLinearContract.State(listOf(megaCorp.party), "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addOutputState(DummyDealContract.State(listOf(megaCorp.party), "Dummy linear id"), DUMMY_DEAL_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy().states.size + }) + assertEquals(1, database.transaction { + vaultService.queryBy().states.size + }) + } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt new file mode 100644 index 0000000000..c0edeb6be3 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt @@ -0,0 +1,91 @@ +package net.corda.node.services.vault + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.node.services.queryBy +import net.corda.core.transactions.TransactionBuilder +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.vault.DUMMY_DEAL_PROGRAM_ID +import net.corda.testing.internal.vault.DummyDealContract +import net.corda.testing.internal.vault.UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID +import net.corda.testing.internal.vault.UniqueDummyLinearContract +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetworkNotarySpec +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.StartedMockNode +import org.assertj.core.api.Assertions +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.ExecutionException +import kotlin.test.assertEquals + +class VaultFlowTest { + + private lateinit var mockNetwork: MockNetwork + private lateinit var partyA: StartedMockNode + private lateinit var partyB: StartedMockNode + private lateinit var notaryNode: MockNetworkNotarySpec + + @Before + fun setup() { + notaryNode = MockNetworkNotarySpec(CordaX500Name("Notary", "London", "GB")) + mockNetwork = MockNetwork( + listOf( + "net.corda.node.services.vault", "net.corda.testing.internal.vault" + ), + notarySpecs = listOf(notaryNode), + threadPerNode = true, + networkSendManuallyPumped = false + ) + partyA = + mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyA", "Berlin", "DE"))) + + partyB = + mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyB", "Berlin", "DE"))) + mockNetwork.startNodes() + } + + @After + fun tearDown() { + mockNetwork.stopNodes() + } + + @Test + fun `Unique column constraint failing causes states to not persist to vaults`() { + partyA.startFlow(Initiator(listOf(partyA.info.singleIdentity(), partyB.info.singleIdentity()))).get() + Assertions.assertThatExceptionOfType(ExecutionException::class.java).isThrownBy { + partyA.startFlow(Initiator(listOf(partyA.info.singleIdentity(), partyB.info.singleIdentity()))).get() + } + assertEquals(1, partyA.transaction { + partyA.services.vaultService.queryBy().states.size + }) + assertEquals(1, partyB.transaction { + partyB.services.vaultService.queryBy().states.size + }) + assertEquals(1, partyA.transaction { + partyA.services.vaultService.queryBy().states.size + }) + assertEquals(1, partyB.transaction { + partyB.services.vaultService.queryBy().states.size + }) + } +} + +@InitiatingFlow +class Initiator(private val participants: List) : FlowLogic() { + @Suspendable + override fun call() { + val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID) + addCommand(DummyCommandData, listOf(ourIdentity.owningKey)) + }) + subFlow(FinalityFlow(stx)) + } +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt new file mode 100644 index 0000000000..88be337e35 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt @@ -0,0 +1,45 @@ +package net.corda.testing.internal.vault + +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.transactions.LedgerTransaction +import net.corda.testing.core.DummyCommandData +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +const val UNIQUE_DUMMY_FUNGIBLE_CONTRACT_PROGRAM_ID = "net.corda.testing.internal.vault.UniqueDummyFungibleContract" + +class UniqueDummyFungibleContract : Contract { + override fun verify(tx: LedgerTransaction) {} + + data class State(override val amount: Amount>, + override val owner: AbstractParty) : FungibleAsset, QueryableState { + + override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey) + override val participants = listOf(owner) + + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) + + override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(DummyCommandData, copy(owner = newOwner)) + + override fun supportedSchemas(): Iterable = listOf(UniqueDummyFungibleStateSchema) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return UniqueDummyFungibleStateSchema.UniquePersistentDummyFungibleState(currency = amount.token.product.currencyCode) + } + } +} + +object UniqueDummyFungibleStateSchema : MappedSchema(schemaFamily = UniqueDummyFungibleStateSchema::class.java, version = 1, mappedTypes = listOf(UniquePersistentDummyFungibleState::class.java)) { + @Entity + @Table(name = "unique_dummy_fungible_state") + class UniquePersistentDummyFungibleState( + @Column(unique = true) + val currency: String + ) : PersistentState() +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt new file mode 100644 index 0000000000..07e9673fe1 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt @@ -0,0 +1,41 @@ +package net.corda.testing.internal.vault + +import net.corda.core.contracts.Contract +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.transactions.LedgerTransaction +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +const val UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID = "net.corda.testing.internal.vault.UniqueDummyLinearContract" + +class UniqueDummyLinearContract : Contract { + override fun verify(tx: LedgerTransaction) {} + + data class State( + override val participants: List, + override val linearId: UniqueIdentifier) : LinearState, QueryableState { + constructor(participants: List = listOf(), + ref: String) : this(participants, UniqueIdentifier(ref)) + + override fun supportedSchemas(): Iterable = listOf(UniqueDummyLinearStateSchema) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return UniqueDummyLinearStateSchema.UniquePersistentLinearDummyState(id = linearId.externalId!!) + } + } +} + +object UniqueDummyLinearStateSchema : MappedSchema(schemaFamily = UniqueDummyLinearStateSchema::class.java, version = 1, mappedTypes = listOf(UniquePersistentLinearDummyState::class.java)) { + @Entity + @Table(name = "unique_dummy_linear_state") + class UniquePersistentLinearDummyState( + @Column(unique = true) + val id: String + ) : PersistentState() +} \ No newline at end of file From c3c245e7ac1f167bd9280bd0204974e49b5d6f39 Mon Sep 17 00:00:00 2001 From: LankyDan Date: Fri, 31 Aug 2018 16:56:31 +0100 Subject: [PATCH 018/119] Fix codestyle --- .../kotlin/net/corda/node/services/vault/VaultFlowTest.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt index c0edeb6be3..ea1a78704c 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt @@ -43,11 +43,8 @@ class VaultFlowTest { threadPerNode = true, networkSendManuallyPumped = false ) - partyA = - mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyA", "Berlin", "DE"))) - - partyB = - mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyB", "Berlin", "DE"))) + partyA = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyA", "Berlin", "DE"))) + partyB = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyB", "Berlin", "DE"))) mockNetwork.startNodes() } From 9eae4e530345dd41f142aca51ec6a9e6e106a54b Mon Sep 17 00:00:00 2001 From: Lamar Thomas <38670842+r3ltsupport@users.noreply.github.com> Date: Wed, 13 Jun 2018 12:03:07 -0400 Subject: [PATCH 019/119] Clarify user to create the "corda" users root/sys admin should create the corda user --- docs/source/deploying-a-node.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 7071466df8..81e6854286 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -16,10 +16,11 @@ handling, and ensures the Corda service is run at boot. * Oracle Java 8. The supported versions are listed in :doc:`getting-set-up` -1. Add a system user which will be used to run Corda: +1. As root/sys admin user - add a system user which will be used to run Corda: ``sudo adduser --system --no-create-home --group corda`` + 2. Create a directory called ``/opt/corda`` and change its ownership to the user you want to use to run Corda: ``mkdir /opt/corda; chown corda:corda /opt/corda`` From ceb47c99388bba7ac7a4b87a58247ca579c8143d Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Fri, 20 Jul 2018 14:42:40 +0100 Subject: [PATCH 020/119] RELEASE - Fixed build.gradle issues that break maven central publishing and standardised dependency specification. --- tools/network-bootstrapper/build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle index ef2eab205d..f7bc85a9de 100644 --- a/tools/network-bootstrapper/build.gradle +++ b/tools/network-bootstrapper/build.gradle @@ -31,11 +31,11 @@ dependencies { compile project(':node-api') compile project(':node') - compile group: "com.typesafe", name: "config", version: typesafe_config_version - compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0" - compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+" - compile group: 'info.picocli', name: 'picocli', version: '3.0.1' + compile "com.typesafe:config:$typesafe_config_version" + compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" + compile 'info.picocli:picocli:3.0.1' // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. compile "no.tornado:tornadofx:$tornadofx_version" From f6466cde41a9e8bf5d9fff6766f3c4088fa2d3f6 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Thu, 19 Jul 2018 17:03:38 +0100 Subject: [PATCH 021/119] Make UI better when labels overflow. --- .../net/corda/demobench/ui/PropertyLabel.kt | 29 +++++++++++++------ .../demobench/views/NodeTerminalView.fxml | 4 +-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt index c591ea9f77..87fa12d754 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt @@ -1,30 +1,41 @@ package net.corda.demobench.ui +import javafx.scene.control.ContentDisplay import javafx.scene.control.Label +import javafx.scene.control.Tooltip import javafx.scene.layout.HBox class PropertyLabel : HBox() { + private val nameLabel = Label() + private val myTooltip = Tooltip() - val nameLabel = Label() - val valueLabel = Label() + private var nameText = "" + private var valueText = "" var name: String - get() = nameLabel.text + get() = nameText set(value) { - nameLabel.text = value + nameText = value + updateText() } var value: String - get() = valueLabel.text + get() = valueText set(value) { - valueLabel.text = value + valueText = value + updateText() } + private fun updateText() { + nameLabel.text = "$nameText $valueText" + myTooltip.text = "$nameText $valueText" + } + init { nameLabel.styleClass.add("property-name") - valueLabel.styleClass.add("property-value") - - children.addAll(nameLabel, valueLabel) + myTooltip.contentDisplay = ContentDisplay.CENTER + Tooltip.install(nameLabel, myTooltip) + children.addAll(nameLabel) styleClass.add("property-label") } } diff --git a/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml index b83cd543a5..ef8a2907ac 100644 --- a/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml +++ b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml @@ -5,10 +5,10 @@ - + - + From f856a77c96bd9e8915621e9b6004fb5355c32ef5 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Tue, 21 Aug 2018 17:37:39 +0100 Subject: [PATCH 022/119] Fix demo bench issue stopping nodes starting up with max transaction size > max message size --- .../src/main/kotlin/net/corda/demobench/model/NodeController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aa25cc163c..05e1bdc3e3 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 @@ -146,7 +146,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { notaries = listOf(NotaryInfo(identity, config.nodeConfig.notary!!.validating)), modifiedTime = Instant.now(), maxMessageSize = 10485760, - maxTransactionSize = Int.MAX_VALUE, + maxTransactionSize = 10485760, epoch = 1, whitelistedContractImplementations = emptyMap() )) From 0f8a6e44ea4d01359b88a08b38b649ef2e99bcb5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 27 Aug 2018 21:04:44 +0200 Subject: [PATCH 023/119] RPC: allow trackRpcCallSites to be set from the command line. Add logging. --- .../net/corda/client/rpc/CordaRPCClient.kt | 2 +- .../rpc/internal/RPCClientProxyHandler.kt | 34 +++++++++++++------ .../amqp/RpcClientObservableDeSerializer.kt | 16 +++++---- .../amqp/RpcServerObservableSerializer.kt | 7 ++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index e41a7ed75c..f6faa3d1f3 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -43,7 +43,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( * returned Observable stream the stack trace of the originating RPC will be shown as well. Note that * constructing call stacks is a moderately expensive operation. */ - open val trackRpcCallSites: Boolean = false, + open val trackRpcCallSites: Boolean = java.lang.Boolean.getBoolean("net.corda.client.rpc.trackRpcCallSites"), /** * The interval of unused observable reaping. Leaked Observables (unused ones) are detected using weak references diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index f10607ad97..c45c4671b7 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -97,12 +97,18 @@ class RPCClientProxyHandler( // To check whether toString() is being invoked val toStringMethod: Method = Object::toString.javaMethod!! - private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: Throwable) { + private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) { var currentThrowable = throwable while (true) { val cause = currentThrowable.cause if (cause == null) { - currentThrowable.initCause(callSite) + try { + currentThrowable.initCause(callSite) + } catch (e: IllegalStateException) { + // OK, we did our best, but the first throwable with a null cause was instantiated using + // Throwable(Throwable) or Throwable(String, Throwable) which means initCause can't ever + // be called even if it was passed null. + } break } else { currentThrowable = cause @@ -146,15 +152,17 @@ class RPCClientProxyHandler( private fun createRpcObservableMap(): RpcObservableMap { val onObservableRemove = RemovalListener>> { key, _, cause -> val observableId = key!! - val rpcCallSite = callSiteMap?.remove(observableId) + val rpcCallSite: CallSite? = callSiteMap?.remove(observableId) if (cause == RemovalCause.COLLECTED) { log.warn(listOf( "A hot observable returned from an RPC was never subscribed to.", "This wastes server-side resources because it was queueing observations for retrieval.", "It is being closed now, but please adjust your code to call .notUsed() on the observable", - "to close it explicitly. (Java users: subscribe to it then unsubscribe). This warning", - "will appear less frequently in future versions of the platform and you can ignore it", - "if you want to.").joinToString(" "), rpcCallSite) + "to close it explicitly. (Java users: subscribe to it then unsubscribe). If you aren't sure", + "where the leak is coming from, set -Dnet.corda.client.rpc.trackRpcCallSites=true on the JVM", + "command line and you will get a stack trace with this warning." + ).joinToString(" "), rpcCallSite) + rpcCallSite?.printStackTrace() } observablesToReap.locked { observables.add(observableId) } } @@ -215,6 +223,9 @@ class RPCClientProxyHandler( startSessions() } + /** A throwable that doesn't represent a real error - it's just here to wrap a stack trace. */ + class CallSite(val rpcName: String) : Throwable("") + // This is the general function that transforms a client side RPC to internal Artemis messages. override fun invoke(proxy: Any, method: Method, arguments: Array?): Any? { lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET } @@ -230,7 +241,7 @@ class RPCClientProxyHandler( throw RPCException("RPC server is not available.") val replyId = InvocationId.newInstance() - callSiteMap?.set(replyId, Throwable("")) + callSiteMap?.set(replyId, CallSite(method.name)) try { val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) val request = RPCApi.ClientToServer.RpcRequest( @@ -273,7 +284,7 @@ class RPCClientProxyHandler( // The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) { fun completeExceptionally(id: InvocationId, e: Throwable, future: SettableFuture?) { - val rpcCallSite: Throwable? = callSiteMap?.get(id) + val rpcCallSite: CallSite? = callSiteMap?.get(id) if (rpcCallSite != null) addRpcCallSiteToThrowable(e, rpcCallSite) future?.setException(e.cause ?: e) } @@ -555,13 +566,14 @@ class RPCClientProxyHandler( private typealias RpcObservableMap = Cache>> private typealias RpcReplyMap = ConcurrentHashMap> -private typealias CallSiteMap = ConcurrentHashMap +private typealias CallSiteMap = ConcurrentHashMap /** * Holds a context available during de-serialisation of messages that are expected to contain Observables. * - * @param observableMap holds the Observables that are ultimately exposed to the user. - * @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property observableMap holds the Observables that are ultimately exposed to the user. + * @property hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property callSiteMap keeps stack traces captured when an RPC was invoked, useful for debugging when an observable leaks. */ data class ObservableContext( val callSiteMap: CallSiteMap?, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt index 52e9dc7cab..17ba71e200 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt @@ -2,8 +2,10 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.client.rpc.internal.ObservableContext +import net.corda.client.rpc.internal.RPCClientProxyHandler import net.corda.core.context.Trace import net.corda.core.serialization.SerializationContext +import net.corda.core.utilities.loggerFor import net.corda.nodeapi.RPCApi import net.corda.serialization.internal.amqp.* import org.apache.qpid.proton.codec.Data @@ -17,11 +19,12 @@ import java.util.concurrent.atomic.AtomicInteger import javax.transaction.NotSupportedException /** - * De-serializer for Rx[Observable] instances for the RPC Client library. Can only be used to deserialize such objects, - * just as the corresponding RPC server side code ([RpcServerObservableSerializer]) can only serialize them. Observables are only notionally serialized, - * what is actually sent is a reference to the observable that can then be subscribed to. + * De-serializer for Rx [Observable] instances for the RPC Client library. Can only be used to deserialize such objects, + * just as the corresponding RPC server side class [RpcServerObservableSerializer] can only serialize them. Observables + * are only notionally serialized, what is actually sent is a reference to the observable that can then be subscribed to. */ object RpcClientObservableDeSerializer : CustomSerializer.Implements>(Observable::class.java) { + private val log = loggerFor() private object RpcObservableContextKey fun createContext( @@ -96,22 +99,23 @@ object RpcClientObservableDeSerializer : CustomSerializer.Implements() } - private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): Throwable? { + private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): RPCClientProxyHandler.CallSite? { val rpcRequestOrObservableId = context.properties[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId + // Will only return non-null if the trackRpcCallSites option in the RPC configuration has been specified. return observableContext.callSiteMap?.get(rpcRequestOrObservableId) } diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt index 7cdd638152..e010741d18 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt @@ -2,6 +2,7 @@ package net.corda.node.serialization.amqp import net.corda.core.context.Trace import net.corda.core.serialization.SerializationContext +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import net.corda.node.services.messaging.ObservableContextInterface import net.corda.node.services.messaging.ObservableSubscription @@ -30,8 +31,9 @@ class RpcServerObservableSerializer : CustomSerializer.Implements> fun createContext( serializationContext: SerializationContext, observableContext: ObservableContextInterface - ) = serializationContext.withProperty( - RpcServerObservableSerializer.RpcObservableContextKey, observableContext) + ) = serializationContext.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) + + val log = contextLogger() } override val schemaForDocumentation = Schema( @@ -136,5 +138,6 @@ class RpcServerObservableSerializer : CustomSerializer.Implements> } } observableContext.observableMap.put(observableId, observableWithSubscription) + log.trace("Serialized observable $observableId of type $obj") } } \ No newline at end of file From 33f5aa41906fb4ed4d968346d43dd0cde8c071c6 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 27 Aug 2018 22:41:00 +0200 Subject: [PATCH 024/119] RPC: make the client library require the platform version it is built for. Remove an unnecessary override on the CordaRPCOps interface. --- .ci/api-current.txt | 1 - .../client/rpc/CordaRPCJavaClientTest.java | 2 +- .../net/corda/client/rpc/RPCStabilityTests.kt | 26 ++++++------- .../net/corda/client/rpc/CordaRPCClient.kt | 13 ++++++- .../rpc/ClientRPCInfrastructureTests.kt | 4 +- .../corda/client/rpc/RPCConcurrencyTests.kt | 4 +- .../net/corda/client/rpc/RPCFailureTests.kt | 2 +- .../rpc/RPCHighThroughputObservableTests.kt | 2 +- .../corda/client/rpc/RPCPerformanceTests.kt | 4 +- .../corda/client/rpc/RPCPermissionsTests.kt | 2 +- constants.properties | 2 + .../net/corda/core/messaging/CordaRPCOps.kt | 6 --- docs/source/changelog.rst | 6 ++- docs/source/clientrpc.rst | 39 +++++++++++-------- .../kotlin/net/corda/node/AuthDBTests.kt | 3 +- .../node/services/rpc/ArtemisRpcTests.kt | 2 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 6 +++ .../rpc/proxies/AuthenticatedRpcOpsProxy.kt | 3 +- .../node/internal/InternalMockNetwork.kt | 11 +----- .../testing/node/internal/NodeBasedTest.kt | 5 +-- 20 files changed, 76 insertions(+), 67 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b0e292868f..cafe828a66 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2529,7 +2529,6 @@ public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.mes public abstract void clearNetworkMapCache() @NotNull public abstract java.time.Instant currentNodeTime() - public abstract int getProtocolVersion() @NotNull public abstract Iterable getVaultTransactionNotes(net.corda.core.crypto.SecureHash) @RPCReturnsObservables 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 44f65f0bc0..366fb6802e 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 Exception { - node = startNode(ALICE_NAME, 1, singletonList(rpcUser)); + node = startNode(ALICE_NAME, 1000, singletonList(rpcUser)); client = new CordaRPCClient(requireNonNull(node.getNode().getConfiguration().getRpcOptions().getAddress())); } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 6ceb5c2cad..b05e33c729 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -47,7 +47,7 @@ class RPCStabilityTests { } object DummyOps : RPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 } private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Map> { @@ -107,7 +107,7 @@ class RPCStabilityTests { Try.on { startRpcClient( server.get().broker.hostAndPort!!, - configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1) + configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1000) ).get() } } @@ -203,7 +203,7 @@ class RPCStabilityTests { rpcDriver { val leakObservableOpsImpl = object : LeakObservableOps { val leakedUnsubscribedCount = AtomicInteger(0) - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun leakObservable(): Observable { return PublishSubject.create().doOnUnsubscribe { leakedUnsubscribedCount.incrementAndGet() @@ -234,7 +234,7 @@ class RPCStabilityTests { fun `client reconnects to rebooted server`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -259,7 +259,7 @@ class RPCStabilityTests { fun `connection failover fails, rpc calls throw`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -290,7 +290,7 @@ class RPCStabilityTests { fun `observables error when connection breaks`() { rpcDriver { val ops = object : NoOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun subscribe(): Observable { return PublishSubject.create() } @@ -350,7 +350,7 @@ class RPCStabilityTests { fun `client connects to first available server`() { rpcDriver { val ops = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server" } val serverFollower = shutdownManager.follower() @@ -371,15 +371,15 @@ class RPCStabilityTests { fun `3 server failover`() { rpcDriver { val ops1 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server1" } val ops2 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server2" } val ops3 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server3" } val serverFollower1 = shutdownManager.follower() @@ -443,7 +443,7 @@ class RPCStabilityTests { fun `server cleans up queues after disconnected clients`() { rpcDriver { val trackSubscriberOpsImpl = object : TrackSubscriberOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 val subscriberCount = AtomicInteger(0) val trackSubscriberCountObservable = UnicastSubject.create().share(). doOnSubscribe { subscriberCount.incrementAndGet() }. @@ -486,7 +486,7 @@ class RPCStabilityTests { } class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun streamAtInterval(interval: Duration, size: Int): Observable { val chunk = ByteArray(size) @@ -587,7 +587,7 @@ class RPCStabilityTests { val request = RPCApi.ClientToServer.fromClientMessage(it) when (request) { is RPCApi.ClientToServer.RpcRequest -> { - val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server") + val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(1000), "server") val message = session.createMessage(false) reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement()) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index f6faa3d1f3..e19ab5f554 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -34,9 +34,18 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( open val connectionMaxRetryInterval: Duration = 3.minutes, /** - * The minimum protocol version required from the server. + * The minimum protocol version required from the server. This is equivalent to the node's platform version + * number. If this minimum version is not met, an exception will be thrown at startup. If you use features + * introduced in a later version, you can bump this to match the platform version you need and get an early + * check that runs before you do anything. + * + * If you leave it at the default then things will work but attempting to use an RPC added in a version later + * than the server supports will throw [UnsupportedOperationException]. + * + * The default value is whatever version of Corda this RPC library was shipped as a part of. Therefore if you + * use the RPC library from Corda 4, it will by default only connect to a node of version 4 or above. */ - open val minimumServerProtocolVersion: Int = 0, + open val minimumServerProtocolVersion: Int = 4, /** * If set to true the client will track RPC call sites. If an error occurs subsequently during the RPC or in a diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index d2a0a2c977..caa363908c 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -48,7 +48,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { fun makeComplicatedListenableFuture(): CordaFuture>> - @RPCSinceVersion(2) + @RPCSinceVersion(2000) fun addedLater() fun captureUser(): String @@ -58,7 +58,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { private lateinit var complicatedListenableFuturee: CordaFuture>> inner class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 // do not remove Unit override fun barf(): Unit = throw IllegalArgumentException("Barf!") override fun void() {} diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 0b15cc0a5e..b7492db120 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -33,7 +33,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { @CordaSerializable data class ObservableRose(val value: A, val branches: Observable>) - private interface TestOps : RPCOps { + interface TestOps : RPCOps { fun newLatch(numberOfDowns: Int): Long fun waitLatch(id: Long) fun downLatch(id: Long) @@ -43,7 +43,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { class TestOpsImpl(private val pool: Executor) : TestOps { private val latches = ConcurrentHashMap() - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun newLatch(numberOfDowns: Int): Long { val id = random63BitValue() diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt index 7806bc9b40..f294d68587 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt @@ -26,7 +26,7 @@ class RPCFailureTests { } class OpsImpl : Ops { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun getUnserializable() = Unserializable() override fun getUnserializableAsync(): CordaFuture { return openFuture().apply { capture { getUnserializable() } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt index 72013ca955..4f3a09d507 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt @@ -24,7 +24,7 @@ class RPCHighThroughputObservableTests : AbstractRPCTest() { } internal class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun makeObservable(): Observable = Observable.interval(0, TimeUnit.MICROSECONDS).map { it.toInt() + 1 } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index f6e9a8aa83..9f38487fb9 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -5,8 +5,8 @@ import net.corda.core.messaging.RPCOps import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.internal.performance.div +import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.performance.startPublishingFixedRateInjector import net.corda.testing.node.internal.performance.startReporter import net.corda.testing.node.internal.performance.startTightLoopInjector @@ -34,7 +34,7 @@ class RPCPerformanceTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray { return ByteArray(sizeOfReply) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 21ebac1fbd..dee4c07257 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -25,7 +25,7 @@ class RPCPermissionsTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun validatePermission(method: String, target: String?) { val authorized = if (target == null) { rpcContext().isPermitted(method) diff --git a/constants.properties b/constants.properties index d9f06face2..b0b06c6c9c 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,7 @@ gradlePluginsVersion=4.0.29 kotlinVersion=1.2.51 +# When adjusting platformVersion upwards please also modify CordaRPCClientConfiguration.minimumServerProtocolVersion \ +# if there have been any RPC changes. Also please modify InternalMockNetwork.kt:MOCK_VERSION_INFO and NodeBasedTest.startNode platformVersion=4 guavaVersion=25.1-jre proguardVersion=6.0.3 diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index f59aa49a40..8345b153f8 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -96,12 +96,6 @@ data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRun /** RPC operations that the node exposes to clients. */ interface CordaRPCOps : RPCOps { - /** - * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed - * to be present. - */ - override val protocolVersion: Int get() = nodeInfo().platformVersion - /** Returns a list of currently in-progress state machine infos. */ fun stateMachinesSnapshot(): List diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 51c95b4d1a..e44cd3eac6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,7 +6,11 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- -* Removed experimental feature `CordformDefinition` +* The RPC client library now checks at startup whether the server is of the client libraries major version or higher. + Therefore to connect to a Corda 4 node you must use version 4 or lower of the library. This behaviour can be overridden + by specifying a lower number in the ``CordaRPCClientConfiguration`` class. + +* Removed experimental feature ``CordformDefinition`` * Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index a2db1469a9..b82b38e268 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -18,8 +18,8 @@ object as normal, and the marshalling back and forth is handled for you. .. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node - and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this - `here `_. + and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this using the popular + Spring Boot server `here `_. Connecting to a node via RPC ---------------------------- @@ -291,31 +291,43 @@ would expect. This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you download them. Note that the server side observation buffer is bounded, once it fills up the client is considered -slow and kicked. You are expected to subscribe to all the observables returned, otherwise client-side memory starts -filling up as observations come in. If you don't want an observable then subscribe then unsubscribe immediately to -clear the client-side buffers and to stop the server from streaming. If your app quits then server side resources -will be freed automatically. +slow and will be disconnected. You are expected to subscribe to all the observables returned, otherwise client-side +memory starts filling up as observations come in. If you don't want an observable then subscribe then unsubscribe +immediately to clear the client-side buffers and to stop the server from streaming. For Kotlin users there is a +convenience extension method called ``notUsed()`` which can be called on an observable to automate this step. + +If your app quits then server side resources will be freed automatically. .. warning:: If you leak an observable on the client side and it gets garbage collected, you will get a warning printed to the logs and the observable will be unsubscribed for you. But don't rely on this, as garbage collection - is non-deterministic. + is non-deterministic. If you set ``-Dnet.corda.client.rpc.trackRpcCallSites=true`` on the JVM command line then + this warning comes with a stack trace showing where the RPC that returned the forgotten observable was called from. + This feature is off by default because tracking RPC call sites is moderately slow. .. note:: Observables can only be used as return arguments of an RPC call. It is not currently possible to pass - Observables as parameters to the RPC methods. + Observables as parameters to the RPC methods. In other words the streaming is always server to client and not + the other way around. Futures ------- A method can also return a ``CordaFuture`` in its object graph and it will be treated in a similar manner to -observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release any resources. +observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release +any resources. Versioning ---------- -The client RPC protocol is versioned using the node's Platform Version (see :doc:`versioning`). When a proxy is created +The client RPC protocol is versioned using the node's platform version number (see :doc:`versioning`). When a proxy is created the server is queried for its version, and you can specify your minimum requirement. Methods added in later versions are tagged with the ``@RPCSinceVersion`` annotation. If you try to use a method that the server isn't advertising support of, an ``UnsupportedOperationException`` is thrown. If you want to know the version of the server, just use the ``protocolVersion`` property (i.e. ``getProtocolVersion`` in Java). +The RPC client library defaults to requiring the platform version it was built with. That means if you use the client +library released as part of Corda N, then the node it connects to must be of version N or above. This is checked when +the client first connects. If you want to override this behaviour, you can alter the ``minimumServerProtocolVersion`` +field in the ``CordaRPCClientConfiguration`` object passed to the client. Alternatively, just link your app against +an older version of the library. + Thread safety ------------- A proxy is thread safe, blocking, and allows multiple RPCs to be in flight at once. Any observables that are returned and @@ -338,7 +350,6 @@ such situations: .. sourcecode:: Kotlin fun establishConnectionWithRetry(nodeHostAndPort: NetworkHostAndPort, username: String, password: String): CordaRPCConnection { - val retryInterval = 5.seconds do { @@ -382,7 +393,6 @@ on the ``Observable`` returned by ``CordaRPCOps``. .. sourcecode:: Kotlin fun performRpcReconnect(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { - val connection = establishConnectionWithRetry(nodeHostAndPort, username, password) val proxy = connection.proxy @@ -414,10 +424,6 @@ Client code if fed with instances of ``StateMachineInfo`` using call ``clientCod all the items. Some of these items might have already been delivered to client code prior to failover occurred. It is down to client code in this case handle those duplicate items as appropriate. -Wire protocol -------------- -The client RPC wire protocol is defined and documented in ``net/corda/client/rpc/RPCApi.kt``. - Wire security ------------- ``CordaRPCClient`` has an optional constructor parameter of type ``ClientRpcSslOptions``, defaulted to ``null``, which allows @@ -430,7 +436,6 @@ In order for this to work, the client needs to provide a truststore containing a For the communication to be secure, we recommend using the standard SSL best practices for key management. - Whitelisting classes with the Corda node ---------------------------------------- CorDapps must whitelist any classes used over RPC with Corda's serialization framework, unless they are whitelisted by diff --git a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt index 9c45d852aa..fb71884e8e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt @@ -13,8 +13,8 @@ import net.corda.node.internal.DataSourceFactory import net.corda.node.internal.NodeWithInfo import net.corda.node.services.Permissions import net.corda.node.services.config.PasswordEncryption -import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.core.ALICE_NAME +import net.corda.testing.node.internal.NodeBasedTest import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.shiro.authc.credential.DefaultPasswordService import org.junit.After @@ -33,7 +33,6 @@ import kotlin.test.assertFailsWith */ @RunWith(Parameterized::class) class AuthDBTests : NodeBasedTest() { - private lateinit var node: NodeWithInfo private lateinit var client: CordaRPCClient private lateinit var db: UsersDB diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index 36d7d9bba2..9a6192b4b1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -140,7 +140,7 @@ class ArtemisRpcTests { class TestRpcOpsImpl : TestRpcOps { override fun greet(name: String): String = "Oh, hello $name!" - override val protocolVersion: Int = 1 + override val protocolVersion: Int = 1000 } private fun tempFile(name: String): Path = tempFolder.root.toPath() / name diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index f584a72818..283a1703dc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -49,6 +49,12 @@ internal class CordaRPCOpsImpl( private val flowStarter: FlowStarter, private val shutdownNode: () -> Unit ) : CordaRPCOps { + /** + * Returns the RPC protocol version, which is the same the node's platform Version. Exists since version 1 so guaranteed + * to be present. + */ + override val protocolVersion: Int get() = nodeInfo().platformVersion + override fun networkMapSnapshot(): List { val (snapshot, updates) = networkMapFeed() updates.notUsed() diff --git a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt index e27ce2800f..f8e0366045 100644 --- a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt @@ -16,6 +16,8 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor /** * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed * to be present. + * + * TODO: Why is this logic duplicated vs the actual implementation? */ override val protocolVersion: Int get() = delegate.nodeInfo().platformVersion @@ -31,7 +33,6 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor private companion object { private fun proxy(delegate: CordaRPCOps, context: () -> RpcAuthContext): CordaRPCOps { - val handler = PermissionsEnforcingInvocationHandler(delegate, context) return Proxy.newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 96050e777f..4a75faf696 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -72,7 +72,7 @@ import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") +val MOCK_VERSION_INFO = VersionInfo(4, "Mock release", "Mock revision", "Mock Vendor") data class MockNodeArgs( val config: NodeConfiguration, @@ -209,15 +209,6 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") } - /** - * Return the identity of the default notary node. - * @see defaultNotaryNode - */ - val defaultNotaryIdentityAndCert: PartyAndCertificate - get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } - /** * Because this executor is shared, we need to be careful about nodes shutting it down. */ diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index bce128a107..e1f2c8fd45 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -10,9 +10,8 @@ import net.corda.core.node.NodeInfo import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.node.VersionInfo -import net.corda.node.internal.NodeWithInfo import net.corda.node.internal.Node - +import net.corda.node.internal.NodeWithInfo import net.corda.node.services.config.* import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.network.NetworkParametersCopier @@ -85,7 +84,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi @JvmOverloads fun startNode(legalName: CordaX500Name, - platformVersion: Int = 1, + platformVersion: Int = 4, rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): NodeWithInfo { val baseDirectory = baseDirectory(legalName).createDirectories() From f6ee263db100c36d532a0eecc5ebbcbd44b333b5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 28 Aug 2018 00:03:39 +0200 Subject: [PATCH 025/119] Minor: improve docs for CordaRPCClientConfiguration and deprecate a field that is no longer used. --- .../net/corda/client/rpc/CordaRPCClient.kt | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index e19ab5f554..7dfc797e0e 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -29,7 +29,8 @@ class CordaRPCConnection internal constructor(connection: RPCConnection + * Maximum reconnect attempts on failover or disconnection. The default is -1 which means unlimited. */ open val maxReconnectAttempts: Int = unlimitedReconnectAttempts, /** - * Maximum file size, in bytes. + * Maximum size of RPC responses, in bytes. Default is 10mb. */ open val maxFileSize: Int = 10485760, // 10 MiB maximum allowed file size for attachments, including message headers. // TODO: acquire this value from Network Map when supported. /** - * The cache expiry of a deduplication watermark per client. + * The cache expiry of a deduplication watermark per client. Default is 1 day. */ open val deduplicationCacheExpiry: Duration = 1.days @@ -106,6 +108,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( private const val unlimitedReconnectAttempts = -1 + /** Provides an instance of this class with the parameters set to our recommended defaults. */ @JvmField val DEFAULT: CordaRPCClientConfiguration = CordaRPCClientConfiguration() @@ -113,7 +116,10 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( /** * Create a new copy of a configuration object with zero or more parameters modified. + * + * @suppress */ + @Suppress("DEPRECATION") @JvmOverloads fun copy( connectionMaxRetryInterval: Duration = this.connectionMaxRetryInterval, @@ -178,6 +184,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( return result } + @Suppress("DEPRECATION") override fun toString(): String { return "CordaRPCClientConfiguration(" + "connectionMaxRetryInterval=$connectionMaxRetryInterval, " + @@ -189,7 +196,8 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( "deduplicationCacheExpiry=$deduplicationCacheExpiry)" } - // Left is for backwards compatibility with version 3.1 + // Left in for backwards compatibility with version 3.1 + @Deprecated("Binary compatibility stub") operator fun component1() = connectionMaxRetryInterval } From d01dd22419de5cf985e72c86f8c972bf5e393c09 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 4 Sep 2018 10:26:10 +0100 Subject: [PATCH 026/119] [CORDA-1937]: Refactor NodeConfiguration hierarchy. (#3856) --- .../net/corda/client/rpc/CordaRPCClient.kt | 23 +-- .../rpc/internal/CordaRPCClientUtils.kt | 8 - .../corda/client/rpc/internal/RPCClient.kt | 10 +- .../net/corda/core/internal/InternalUtils.kt | 10 -- .../net/corda/nodeapi/ArtemisTcpTransport.kt | 70 ++------ .../internal/ArtemisMessagingClient.kt | 7 +- .../nodeapi/internal/DevIdentityGenerator.kt | 19 +- .../internal/InternalArtemisTcpTransport.kt | 166 ++++++++++++++++++ .../nodeapi/internal/KeyStoreConfigHelpers.kt | 81 ++++----- .../internal/bridging/AMQPBridgeManager.kt | 18 +- .../bridging/BridgeControlListener.kt | 6 +- .../internal/config/CertificateStore.kt | 82 +++++++++ .../config/CertificateStoreSupplier.kt | 24 +++ ...SLConfiguration.kt => SslConfiguration.kt} | 31 +++- .../nodeapi/internal/crypto/X509KeyStore.kt | 9 + .../protonwrapper/netty/AMQPClient.kt | 2 +- .../protonwrapper/netty/AMQPConfiguration.kt | 14 +- .../protonwrapper/netty/AMQPServer.kt | 2 +- .../internal/protonwrapper/netty/SSLHelper.kt | 21 ++- .../internal/crypto/DevCertificatesTest.kt | 7 +- .../internal/crypto/X509UtilitiesTest.kt | 53 +++--- .../net/corda/node/NodeKeystoreCheckTest.kt | 17 +- .../net/corda/node/amqp/AMQPBridgeTest.kt | 32 ++-- .../CertificateRevocationListNodeTests.kt | 75 ++++---- .../net/corda/node/amqp/ProtonWrapperTests.kt | 111 +++++++----- .../messaging/ArtemisMessagingTest.kt | 13 +- .../node/services/network/NetworkMapTest.kt | 13 +- .../node/services/rpc/ArtemisRpcTests.kt | 16 +- .../messaging/MQSecurityAsNodeTest.kt | 76 +++----- .../services/messaging/MQSecurityTest.kt | 6 +- .../services/messaging/SimpleMQClient.kt | 8 +- .../net/corda/node/internal/AbstractNode.kt | 42 +++-- .../kotlin/net/corda/node/internal/Node.kt | 8 +- .../node/services/config/ConfigUtilities.kt | 30 ++-- .../node/services/config/NodeConfiguration.kt | 32 +++- .../node/services/config/shell/ShellConfig.kt | 5 +- .../messaging/ArtemisMessagingServer.kt | 9 +- .../messaging/InternalRPCMessagingClient.kt | 8 +- .../services/messaging/P2PMessagingClient.kt | 4 +- .../node/services/rpc/ArtemisRpcBroker.kt | 14 +- .../services/rpc/RpcBrokerConfiguration.kt | 8 +- .../transactions/RaftUniquenessProvider.kt | 18 +- .../registration/NetworkRegistrationHelper.kt | 59 ++++--- .../NetworkRegistrationHelperTest.kt | 42 +++-- .../testing/node/internal/DriverDSLImpl.kt | 15 +- .../node/internal/InternalMockNetwork.kt | 19 +- .../corda/testing/node/internal/RPCDriver.kt | 8 +- .../internal/UnsafeCertificatesFactory.kt | 20 ++- .../testing/internal/InternalTestUtils.kt | 54 ++---- .../internal/stubs/CertificateStoreStubs.kt | 101 +++++++++++ .../shell/InteractiveShellIntegrationTest.kt | 2 +- .../net/corda/tools/shell/InteractiveShell.kt | 19 -- .../corda/tools/shell/ShellConfiguration.kt | 2 - .../net/corda/webserver/WebServerConfig.kt | 13 +- .../corda/webserver/internal/NodeWebServer.kt | 4 +- 55 files changed, 961 insertions(+), 605 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt rename node-api/src/main/kotlin/net/corda/nodeapi/internal/config/{SSLConfiguration.kt => SslConfiguration.kt} (54%) create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 7dfc797e0e..c6b9d0556e 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -7,12 +7,11 @@ import net.corda.core.context.Trace import net.corda.core.messaging.CordaRPCOps import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import java.time.Duration @@ -243,10 +242,8 @@ class CordaRPCClient private constructor( private val hostAndPort: NetworkHostAndPort, private val configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, private val sslConfiguration: ClientRpcSslOptions? = null, - private val nodeSslConfiguration: SSLConfiguration? = null, private val classLoader: ClassLoader? = null, - private val haAddressPool: List = emptyList(), - private val internalConnection: Boolean = false + private val haAddressPool: List = emptyList() ) { @JvmOverloads constructor(hostAndPort: NetworkHostAndPort, @@ -260,7 +257,7 @@ class CordaRPCClient private constructor( * @param configuration An optional configuration used to tweak client behaviour. */ @JvmOverloads - constructor(haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool) + constructor(haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, haAddressPool) companion object { fun createWithSsl( @@ -285,16 +282,7 @@ class CordaRPCClient private constructor( sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, sslConfiguration, null, classLoader) - } - - internal fun createWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration?, - classLoader: ClassLoader? = null - ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, null, sslConfiguration, classLoader, internalConnection = true) + return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader) } } @@ -312,9 +300,6 @@ class CordaRPCClient private constructor( private fun getRpcClient(): RPCClient { return when { - // Node->RPC broker, mutually authenticated SSL. This is used when connecting the integrated shell - internalConnection == true -> RPCClient(hostAndPort, nodeSslConfiguration!!) - // Client->RPC broker haAddressPool.isEmpty() -> RPCClient( rpcConnectorTcpTransport(hostAndPort, config = sslConfiguration), diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt index 7781d0f135..9b07c405a9 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt @@ -6,7 +6,6 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.pendingFlowsCount import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration import rx.Observable /** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ @@ -17,13 +16,6 @@ fun createCordaRPCClientWithSslAndClassLoader( classLoader: ClassLoader? = null ) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) -fun createCordaRPCClientWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration? = null, - classLoader: ClassLoader? = null -) = CordaRPCClient.createWithInternalSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) - fun CordaRPCOps.drainAndShutdown(): Observable { setFlowsDrainingModeEnabled(true) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 1d7969caaa..1665aa0159 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -15,11 +15,11 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcInternalClientTcpTransport +import net.corda.nodeapi.internal.config.SslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -43,7 +43,7 @@ class RPCClient( constructor( hostAndPort: NetworkHostAndPort, - sslConfiguration: SSLConfiguration, + sslConfiguration: SslConfiguration, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT ) : this(rpcInternalClientTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 9626cf88a0..078cfcaa11 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -4,21 +4,11 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM -import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappConfig -import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.* -import net.corda.core.flows.FlowLogic -import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger -import org.slf4j.MDC import rx.Observable import rx.Observer import rx.subjects.PublishSubject diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 18fbad6ff2..210f04586a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -1,12 +1,16 @@ package net.corda.nodeapi import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.acceptorFactoryClassName +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.connectorFactoryClassName +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.defaultArtemisOptions +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.defaultSSLOptions import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.TransportConfiguration -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import java.nio.file.Path @@ -38,26 +42,6 @@ class ArtemisTcpTransport { /** Supported TLS versions, currently TLSv1.2 only. */ val TLS_VERSIONS = listOf("TLSv1.2") - private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf( - // Basic TCP target details. - TransportConstants.HOST_PROP_NAME to hostAndPort.host, - TransportConstants.PORT_PROP_NAME to hostAndPort.port, - - // Turn on AMQP support, which needs the protocol jar on the classpath. - // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop. - // It does not use AMQP messages for its own messages e.g. topology and heartbeats. - // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. - TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", - TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), - TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1), - // turn off direct delivery in Artemis - this is latency optimisation that can lead to - //hick-ups under high load (CORDA-1336) - TransportConstants.DIRECT_DELIVER to false) - - private val defaultSSLOptions = mapOf( - TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), - TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(",")) - private fun SSLConfiguration.toTransportOptions() = mapOf( TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", @@ -68,22 +52,6 @@ class ArtemisTcpTransport { TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword, TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) - private fun ClientRpcSslOptions.toTransportOptions() = mapOf( - TransportConstants.SSL_ENABLED_PROP_NAME to true, - TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider, - TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath, - TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword) - - private fun BrokerRpcSslOptions.toTransportOptions() = mapOf( - TransportConstants.SSL_ENABLED_PROP_NAME to true, - TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath, - TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, - TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false) - - private val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory" - private val connectorFactoryClassName = NettyConnectorFactory::class.java.name - fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() @@ -110,27 +78,13 @@ class ArtemisTcpTransport { /** [TransportConfiguration] for RPC TCP communication - server side. */ fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { - val options = defaultArtemisOptions(hostAndPort).toMutableMap() - - if (config != null && enableSSL) { - config.keyStorePath.requireOnDefaultFileSystem() - options.putAll(config.toTransportOptions()) - options.putAll(defaultSSLOptions) - } - return TransportConfiguration(acceptorFactoryClassName, options) + return InternalArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, config, enableSSL) } /** [TransportConfiguration] for RPC TCP communication * This is the Transport that connects the client JVM to the broker. */ fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { - val options = defaultArtemisOptions(hostAndPort).toMutableMap() - - if (config != null && enableSSL) { - config.trustStorePath.requireOnDefaultFileSystem() - options.putAll(config.toTransportOptions()) - options.putAll(defaultSSLOptions) - } - return TransportConfiguration(connectorFactoryClassName, options) + return InternalArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, config, enableSSL) } /** Create as list of [TransportConfiguration]. **/ @@ -138,12 +92,12 @@ class ArtemisTcpTransport { rpcConnectorTcpTransport(it, config, enableSSL) } - fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration { - return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) + fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { + return InternalArtemisTcpTransport.rpcInternalClientTcpTransport(hostAndPort, config) } - fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration { - return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) + fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { + return InternalArtemisTcpTransport.rpcInternalAcceptorTcpTransport(hostAndPort, config) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index 4e0366b9d7..055c96ffe6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -3,9 +3,8 @@ package net.corda.nodeapi.internal import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ClientProducer @@ -18,7 +17,7 @@ interface ArtemisSessionProvider { val started: ArtemisMessagingClient.Started? } -class ArtemisMessagingClient(private val config: SSLConfiguration, +class ArtemisMessagingClient(private val config: MutualSslConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) : ArtemisSessionProvider { companion object { @@ -34,7 +33,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, check(started == null) { "start can't be called twice" } log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config) + val tcpTransport = InternalArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. 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 080bf680c4..ab840ed387 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 @@ -7,7 +7,8 @@ import net.corda.core.identity.Party import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.utilities.trace -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -31,15 +32,15 @@ object DevIdentityGenerator { /** 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" - override val trustStorePassword get() = throw NotImplementedError("Not expected to be called") - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = nodeDir / "certificates" + val signingCertStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "nodekeystore.jks", "cordacadevpass") + val p2pKeyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "sslkeystore.jks", "cordacadevpass") + val p2pTrustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "truststore.jks", "trustpass") + val p2pSslConfig = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) - nodeSslConfig.certificatesDirectory.createDirectories() - val (nodeKeyStore) = nodeSslConfig.createDevKeyStores(legalName) + certificatesDirectory.createDirectories() + val nodeKeyStore = signingCertStore.get(true).also { it.registerDevSigningCertificates(legalName) } + p2pSslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(legalName) } val identity = nodeKeyStore.storeLegalIdentity("$NODE_IDENTITY_ALIAS_PREFIX-private-key") return identity.party diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt new file mode 100644 index 0000000000..85513c9ad1 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt @@ -0,0 +1,166 @@ +package net.corda.nodeapi.internal + +import net.corda.core.messaging.ClientRpcSslOptions +import net.corda.core.serialization.internal.nodeSerializationEnv +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.BrokerRpcSslOptions +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import org.apache.activemq.artemis.api.core.TransportConfiguration +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants +import java.nio.file.Path + +// This avoids internal types from leaking in the public API. The "external" ArtemisTcpTransport delegates to this internal one. +class InternalArtemisTcpTransport { + companion object { + val CIPHER_SUITES = listOf( + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + ) + + val TLS_VERSIONS = listOf("TLSv1.2") + + internal fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf( + // Basic TCP target details. + TransportConstants.HOST_PROP_NAME to hostAndPort.host, + TransportConstants.PORT_PROP_NAME to hostAndPort.port, + + // Turn on AMQP support, which needs the protocol jar on the classpath. + // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop. + // It does not use AMQP messages for its own messages e.g. topology and heartbeats. + // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. + TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", + TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), + TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1), + // turn off direct delivery in Artemis - this is latency optimisation that can lead to + //hick-ups under high load (CORDA-1336) + TransportConstants.DIRECT_DELIVER to false) + + internal val defaultSSLOptions = mapOf( + TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), + TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(",")) + + private fun SslConfiguration.toTransportOptions(): Map { + + val options = mutableMapOf() + (keyStore to trustStore).addToTransportOptions(options) + return options + } + + private fun Pair.addToTransportOptions(options: MutableMap) { + + val keyStore = first + val trustStore = second + keyStore?.let { + with (it) { + path.requireOnDefaultFileSystem() + options.putAll(get().toKeyStoreTransportOptions(path)) + } + } + trustStore?.let { + with (it) { + path.requireOnDefaultFileSystem() + options.putAll(get().toTrustStoreTransportOptions(path)) + } + } + } + + private fun CertificateStore.toKeyStoreTransportOptions(path: Path) = mapOf( + TransportConstants.SSL_ENABLED_PROP_NAME to true, + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", + TransportConstants.KEYSTORE_PATH_PROP_NAME to path, + TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to password, + TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) + + private fun CertificateStore.toTrustStoreTransportOptions(path: Path) = mapOf( + TransportConstants.SSL_ENABLED_PROP_NAME to true, + TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", + TransportConstants.TRUSTSTORE_PATH_PROP_NAME to path, + TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to password, + TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) + + private fun ClientRpcSslOptions.toTransportOptions() = mapOf( + TransportConstants.SSL_ENABLED_PROP_NAME to true, + TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider, + TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath, + TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword) + + private fun BrokerRpcSslOptions.toTransportOptions() = mapOf( + TransportConstants.SSL_ENABLED_PROP_NAME to true, + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", + TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath, + TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, + TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false) + + internal val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory" + internal val connectorFactoryClassName = NettyConnectorFactory::class.java.name + + fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + + return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + } + + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + + return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + } + + fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { + + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + if (enableSSL) { + options.putAll(defaultSSLOptions) + (keyStore to trustStore).addToTransportOptions(options) + } + return TransportConfiguration(acceptorFactoryClassName, options) + } + + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { + + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + if (enableSSL) { + options.putAll(defaultSSLOptions) + (keyStore to trustStore).addToTransportOptions(options) + } + return TransportConfiguration(connectorFactoryClassName, options) + } + + fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + + if (config != null && enableSSL) { + config.keyStorePath.requireOnDefaultFileSystem() + options.putAll(config.toTransportOptions()) + options.putAll(defaultSSLOptions) + } + return TransportConfiguration(acceptorFactoryClassName, options) + } + + fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + + if (config != null && enableSSL) { + config.trustStorePath.requireOnDefaultFileSystem() + options.putAll(config.toTransportOptions()) + options.putAll(defaultSSLOptions) + } + return TransportConfiguration(connectorFactoryClassName, options) + } + + fun rpcConnectorTcpTransportsFromList(hostAndPortList: List, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List = hostAndPortList.map { + rpcConnectorTcpTransport(it, config, enableSSL) + } + + fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { + return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) + } + + fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { + return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) + } + } +} \ No newline at end of file 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 6d2ca5b271..d8a856afa5 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 @@ -5,7 +5,7 @@ import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.toX500Name -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.* import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -20,48 +20,43 @@ import javax.security.auth.x500.X500Principal * 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(legalName: CordaX500Name, - rootCert: X509Certificate = DEV_ROOT_CA.certificate, - intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA): Pair { - val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName) - val nodeKeyStore = loadNodeKeyStore(createNew = true) - nodeKeyStore.update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_CA, - nodeCaKeyPair.private, - listOf(nodeCaCert, intermediateCa.certificate, rootCert)) +fun CertificateStore.registerDevSigningCertificates(legalName: CordaX500Name, + rootCert: X509Certificate = DEV_ROOT_CA.certificate, + intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, + devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { + + update { + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert)) } - - val sslKeyStore = loadSslKeyStore(createNew = true) - sslKeyStore.update { - val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public) - setPrivateKey( - X509Utilities.CORDA_CLIENT_TLS, - tlsKeyPair.private, - listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert)) - } - - return Pair(nodeKeyStore, sslKeyStore) } -fun X509KeyStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate { - val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - // Assume key password = store password. - val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) - // Create new keys and store in keystore. - val identityCert = X509Utilities.createCertificate( - CertificateType.LEGAL_IDENTITY, - nodeCaCertAndKeyPair.certificate, - nodeCaCertAndKeyPair.keyPair, - nodeCaCertAndKeyPair.certificate.subjectX500Principal, - keyPair.public) - // TODO: X509Utilities.validateCertificateChain() - // Assume key password = store password. - val identityCertPath = listOf(identityCert) + nodeCaCertPath - setPrivateKey(alias, keyPair.private, identityCertPath) - save() +fun CertificateStore.registerDevP2pCertificates(legalName: CordaX500Name, + rootCert: X509Certificate = DEV_ROOT_CA.certificate, + intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, + devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { + + update { + val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, devNodeCa.certificate, devNodeCa.keyPair, legalName.x500Principal, tlsKeyPair.public) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, devNodeCa.certificate, intermediateCa.certificate, rootCert)) + } +} + +fun CertificateStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate { + val identityCertPath = query { + val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + // Assume key password = store password. + val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + // Create new keys and store in keystore. + val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCaCertAndKeyPair.certificate, nodeCaCertAndKeyPair.keyPair, nodeCaCertAndKeyPair.certificate.subjectX500Principal, keyPair.public) + // TODO: X509Utilities.validateCertificateChain() + // Assume key password = store password. + listOf(identityCert) + nodeCaCertPath + } + update { + setPrivateKey(alias, keyPair.private, identityCertPath) + } return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath)) } @@ -105,8 +100,10 @@ const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { - // TODO: Should be identity scheme - val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_KEY_STORE_FILE"), DEV_CA_KEY_STORE_PASS) - return caKeyStore.getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS) + return loadDevCaKeyStore().query { getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS) } } } + +fun loadDevCaKeyStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_KEY_STORE_FILE", DEV_CA_KEY_STORE_PASS, classLoader) + +fun loadDevCaTrustStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_TRUST_STORE_FILE", DEV_CA_TRUST_STORE_PASS, classLoader) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 5b6a2d8cd5..7be3844aec 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -14,7 +14,8 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagi import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.ArtemisSessionProvider import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration @@ -25,7 +26,6 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.api.core.client.ClientSession import org.slf4j.MDC import rx.Subscription -import java.security.KeyStore import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -37,26 +37,22 @@ import kotlin.concurrent.withLock * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. */ @VisibleForTesting -class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { +class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { private val lock = ReentrantLock() private val bridgeNameToBridgeMap = mutableMapOf() - private class AMQPConfigurationImpl private constructor(override val keyStore: KeyStore, - override val keyStorePrivateKeyPassword: CharArray, - override val trustStore: KeyStore, + private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore, + override val trustStore: CertificateStore, override val maxMessageSize: Int) : AMQPConfiguration { - constructor(config: NodeSSLConfiguration, maxMessageSize: Int) : this(config.loadSslKeyStore().internal, - config.keyStorePassword.toCharArray(), - config.loadTrustStore().internal, - maxMessageSize) + constructor(config: MutualSslConfiguration, maxMessageSize: Int) : this(config.keyStore.get(), config.trustStore.get(), maxMessageSize) } private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(config, maxMessageSize) private var sharedEventLoopGroup: EventLoopGroup? = null private var artemis: ArtemisSessionProvider? = null - constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) + constructor(config: MutualSslConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) companion object { private const val NUM_BRIDGE_THREADS = 0 // Default sized pool diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index 3dc987e6ec..c9d6e23af7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -11,14 +11,14 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisSessionProvider -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ClientConsumer import org.apache.activemq.artemis.api.core.client.ClientMessage import java.util.* -class BridgeControlListener(val config: NodeSSLConfiguration, +class BridgeControlListener(val config: MutualSslConfiguration, maxMessageSize: Int, val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable { private val bridgeId: String = UUID.randomUUID().toString() @@ -27,7 +27,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, private var artemis: ArtemisSessionProvider? = null private var controlConsumer: ClientConsumer? = null - constructor(config: NodeSSLConfiguration, + constructor(config: MutualSslConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt new file mode 100644 index 0000000000..3ca6be6d6b --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt @@ -0,0 +1,82 @@ +package net.corda.nodeapi.internal.config + +import net.corda.core.internal.outputStream +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.OpenOption +import java.nio.file.Path +import java.security.cert.X509Certificate + +interface CertificateStore : Iterable> { + + companion object { + + fun of(store: X509KeyStore, password: String): CertificateStore = DelegatingCertificateStore(store, password) + + fun fromFile(storePath: Path, password: String, createNew: Boolean): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromFile(storePath, password, createNew), password) + + fun fromInputStream(stream: InputStream, password: String): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromInputStream(stream, password), password) + + fun fromResource(storeResourceName: String, password: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader): CertificateStore = fromInputStream(classLoader.getResourceAsStream(storeResourceName), password) + } + + val value: X509KeyStore + val password: String + + fun writeTo(stream: OutputStream) = value.internal.store(stream, password.toCharArray()) + + fun writeTo(path: Path, vararg options: OpenOption) = path.outputStream(*options) + + fun update(action: X509KeyStore.() -> Unit) { + val result = action.invoke(value) + value.save() + return result + } + + fun query(action: X509KeyStore.() -> RESULT): RESULT { + return action.invoke(value) + } + + operator fun set(alias: String, certificate: X509Certificate) { + + update { + internal.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certificate) + } + } + + override fun iterator(): Iterator> { + + return query { + aliases() + }.asSequence().map { alias -> alias to get(alias) }.iterator() + } + + fun forEach(action: (alias: String, certificate: X509Certificate) -> Unit) { + + forEach { (alias, certificate) -> action.invoke(alias, certificate) } + } + + /** + * @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate]. + */ + operator fun get(alias: String): X509Certificate { + + return query { + getCertificate(alias) + } + } + + operator fun contains(alias: String): Boolean = value.contains(alias) + + fun copyTo(certificateStore: CertificateStore) { + + certificateStore.update { + this@CertificateStore.forEach(::setCertificate) + } + } +} + +private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String) : CertificateStore \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt new file mode 100644 index 0000000000..3703742813 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt @@ -0,0 +1,24 @@ +package net.corda.nodeapi.internal.config + +import java.io.IOException +import java.nio.file.Path + +interface CertificateStoreSupplier { + + fun get(createNew: Boolean = false): CertificateStore + + fun getOptional(): CertificateStore? { + + return try { + get() + } catch (e: IOException) { + null + } + } +} + +// TODO replace reference to FileBasedCertificateStoreSupplier with CertificateStoreSupplier, after coming up with a way of passing certificate stores to Artemis. +class FileBasedCertificateStoreSupplier(val path: Path, val password: String) : CertificateStoreSupplier { + + override fun get(createNew: Boolean) = CertificateStore.fromFile(path, password, createNew) +} \ 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 similarity index 54% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt index e8c63fc1f3..1011d49f9d 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 @@ -4,12 +4,34 @@ import net.corda.core.internal.div import net.corda.nodeapi.internal.crypto.X509KeyStore import java.nio.file.Path +interface SslConfiguration { + + val keyStore: FileBasedCertificateStoreSupplier? + val trustStore: FileBasedCertificateStoreSupplier? + + companion object { + + fun mutual(keyStore: FileBasedCertificateStoreSupplier, trustStore: FileBasedCertificateStoreSupplier): MutualSslConfiguration { + + return MutualSslOptions(keyStore, trustStore) + } + } +} + +interface MutualSslConfiguration : SslConfiguration { + + override val keyStore: FileBasedCertificateStoreSupplier + override val trustStore: FileBasedCertificateStoreSupplier +} + +private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration + +// Don't use this internally. It's still here because it's used by ArtemisTcpTransport, which is in public node-api by mistake. interface SSLConfiguration { val keyStorePassword: String 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" val crlCheckSoftFail: Boolean @@ -25,9 +47,4 @@ interface SSLConfiguration { fun loadSslKeyStore(createNew: Boolean = false): X509KeyStore { return X509KeyStore.fromFile(sslKeystore, keyStorePassword, createNew) } -} - -interface NodeSSLConfiguration : SSLConfiguration { - val baseDirectory: Path - override val certificatesDirectory: Path get() = baseDirectory / "certificates" -} +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt index 1fa56bebea..5c0c4b501b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.crypto.Crypto import net.corda.core.internal.uncheckedCast +import java.io.InputStream import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore @@ -30,6 +31,14 @@ class X509KeyStore private constructor(val internal: KeyStore, private val store val internal: KeyStore = if (createNew) loadOrCreateKeyStore(keyStoreFile, storePassword) else loadKeyStore(keyStoreFile, storePassword) return X509KeyStore(internal, storePassword, keyStoreFile) } + + /** + * Reads a [KeyStore] from an [InputStream]. + */ + fun fromInputStream(stream: InputStream, storePassword: String): X509KeyStore { + val internal = loadKeyStore(stream, storePassword) + return X509KeyStore(internal, storePassword) + } } operator fun contains(alias: String): Boolean = internal.containsAlias(alias) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt index 5c010014be..3be66c2824 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt @@ -116,7 +116,7 @@ class AMQPClient(val targets: List, private val conf = parent.configuration init { - keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword) + keyManagerFactory.init(conf.keyStore) trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt index c2ba54286a..3b7289a8c5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.config.CertificateStore import java.security.KeyStore interface AMQPConfiguration { @@ -21,19 +22,14 @@ interface AMQPConfiguration { get() = ArtemisMessagingComponent.PEER_USER /** - * The keystore used for TLS connections + * The key store used for TLS connections */ - val keyStore: KeyStore + val keyStore: CertificateStore /** - * Password used to unlock TLS private keys in the KeyStore. + * The trust root key store to validate the peer certificates against */ - val keyStorePrivateKeyPassword: CharArray - - /** - * The trust root KeyStore to validate the peer certificates against - */ - val trustStore: KeyStore + val trustStore: CertificateStore /** * Setting crlCheckSoftFail to true allows certificate paths where some leaf certificates do not contain cRLDistributionPoints diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 7c16a0547e..56c8b8bfda 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -60,7 +60,7 @@ class AMQPServer(val hostName: String, private val conf = parent.configuration init { - keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword) + keyManagerFactory.init(conf.keyStore) trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 1f4328a8ee..6ffd49cc2d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -5,14 +5,13 @@ import net.corda.core.crypto.newSecureRandom import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHex -import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.toBc import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.SubjectKeyIdentifier import java.net.Socket -import java.security.KeyStore -import java.security.SecureRandom import java.security.cert.* import java.util.* import javax.net.ssl.* @@ -111,8 +110,8 @@ internal fun createClientSslHelper(target: NetworkHostAndPort, sslContext.init(keyManagers, trustManagers, newSecureRandom()) val sslEngine = sslContext.createSSLEngine(target.host, target.port) sslEngine.useClientMode = true - sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() - sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enabledProtocols = InternalArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true return SslHandler(sslEngine) } @@ -126,13 +125,13 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, val sslEngine = sslContext.createSSLEngine() sslEngine.useClientMode = false sslEngine.needClientAuth = true - sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() - sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enabledProtocols = InternalArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true return SslHandler(sslEngine) } -internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { +internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { val certPathBuilder = CertPathBuilder.getInstance("PKIX") val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker revocationChecker.options = EnumSet.of( @@ -145,7 +144,11 @@ internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlC // the following reasons: The CRL or OCSP response cannot be obtained because of a network error. revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL } - val pkixParams = PKIXBuilderParameters(trustStore, X509CertSelector()) + val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector()) pkixParams.addCertPathChecker(revocationChecker) return CertPathTrustManagerParameters(pkixParams) } + +fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.password.toCharArray()) + +fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt index 4fdc43dd2d..e586a6c642 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt @@ -1,8 +1,7 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.internal.validate -import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE -import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS +import net.corda.nodeapi.internal.loadDevCaTrustStore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -22,8 +21,8 @@ class DevCertificatesTest { @Test fun `create server certificate in keystore for SSL`() { // given - val newTrustStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS) - val newTrustRoot = newTrustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) + val newTrustStore = loadDevCaTrustStore() + val newTrustRoot = newTrustStore[X509Utilities.CORDA_ROOT_CA] val newTrustAnchor = TrustAnchor(newTrustRoot, null) val oldNodeCaKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("regression-test/$OLD_NODE_DEV_KEYSTORE_FILE_NAME"), OLD_DEV_KEYSTORE_PASS) 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 1df89e1070..d389e5b27d 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 @@ -10,8 +10,11 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.serialization.amqp.AMQPServerSerializationScheme -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.createDevNodeCa +import net.corda.nodeapi.internal.protonwrapper.netty.init +import net.corda.nodeapi.internal.registerDevP2pCertificates +import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.SerializationContextImpl import net.corda.serialization.internal.SerializationFactoryImpl @@ -19,6 +22,7 @@ import net.corda.serialization.internal.amqp.amqpMagic import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.createDevIntermediateCaCertPath import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x509.* @@ -31,7 +35,6 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.nio.file.Path -import java.security.SecureRandom import java.security.cert.CertPath import java.security.cert.X509Certificate import java.util.* @@ -180,29 +183,27 @@ class X509UtilitiesTest { @Test fun `create server certificate in keystore for SSL`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = tempFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = tempFolder.root.toPath() + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass") + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa) - + val nodeCa = createDevNodeCa(intermediateCa, MEGA_CORP.name) + signingCertStore.get(createNew = true).also { it.registerDevSigningCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } + p2pSslConfig.keyStore.get(createNew = true).also { it.registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } // Load back server certificate - val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword) - val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword) + val serverKeyStore = signingCertStore.get().value + val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) serverCert.checkValidity() serverCert.verify(intermediateCa.certificate.publicKey) assertThat(CordaX500Name.build(serverCert.subjectX500Principal)).isEqualTo(MEGA_CORP.name) // Load back SSL certificate - val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword) + val sslKeyStoreReloaded = p2pSslConfig.keyStore.get() + val (sslCert) = sslKeyStoreReloaded.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, p2pSslConfig.keyStore.password) } sslCert.checkValidity() sslCert.verify(serverCert.publicKey) @@ -216,25 +217,20 @@ class X509UtilitiesTest { @Test fun `create server cert and use in SSL socket`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = tempFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(tempFolder.root.toPath(), keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa) + sslConfig.keyStore.get(true).registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa) sslConfig.createTrustStore(rootCa.certificate) - val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) + val keyStore = sslConfig.keyStore.get() + val trustStore = sslConfig.trustStore.get() val context = SSLContext.getInstance("TLS") val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray()) + keyManagerFactory.init(keyStore) val keyManagers = keyManagerFactory.keyManagers val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustMgrFactory.init(trustStore) @@ -313,10 +309,9 @@ class X509UtilitiesTest { private fun tempFile(name: String): Path = tempFolder.root.toPath() / name - private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { - val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) - trustStore.save(trustStoreFile, trustStorePassword) + private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) { + val trustStore = this.trustStore.get(true) + trustStore[X509Utilities.CORDA_ROOT_CA] = rootCert } @Test 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 7800bf67b2..9bf90c5a48 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -4,15 +4,14 @@ import net.corda.core.crypto.Crypto import net.corda.core.internal.div import net.corda.core.utilities.getOrThrow import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test -import java.nio.file.Path import javax.security.auth.x500.X500Principal class NodeKeystoreCheckTest { @@ -30,13 +29,11 @@ class NodeKeystoreCheckTest { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { // Create keystores val keystorePassword = "password" - val config = object : SSLConfiguration { - override val keyStorePassword: String = keystorePassword - override val trustStorePassword: String = keystorePassword - override val certificatesDirectory: Path = baseDirectory(ALICE_NAME) / "certificates" - override val crlCheckSoftFail: Boolean = true - } - config.configureDevKeyAndTrustStores(ALICE_NAME) + val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates" + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, keystorePassword) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = keystorePassword, trustStorePassword = keystorePassword) + + p2pSslConfig.configureDevKeyAndTrustStores(ALICE_NAME, signingCertStore, certificatesDirectory) // This should pass with correct keystore. val node = startNode( @@ -48,7 +45,7 @@ class NodeKeystoreCheckTest { node.stop() // Fiddle with node keystore. - config.loadNodeKeyStore().update { + signingCertStore.get().update { // Self signed root val badRootKeyPair = Crypto.generateKeyPair() val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair) diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index 1d7c8a4112..1563abb0c8 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -19,6 +19,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.TestIdentity import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.rigorousMock import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.RoutingType @@ -27,7 +28,6 @@ import org.junit.Assert.assertArrayEquals import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder -import java.security.KeyStore import java.util.* import kotlin.test.assertEquals @@ -168,21 +168,26 @@ class AMQPBridgeTest { } private fun createArtemis(sourceQueueName: String?): Triple { + val baseDir = temporaryFolder.root.toPath() / "artemis" + val certificatesDirectory = baseDir / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val artemisConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(baseDir).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail - doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort } artemisConfig.configureWithDevSSLCertificate() val artemisServer = ArtemisMessagingServer(artemisConfig, artemisAddress.copy(host = "0.0.0.0"), MAX_MESSAGE_SIZE) - val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE) + val artemisClient = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) artemisServer.start() artemisClient.start() - val bridgeManager = AMQPBridgeManager(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE) + val bridgeManager = AMQPBridgeManager(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) bridgeManager.start() val artemis = artemisClient.started!! if (sourceQueueName != null) { @@ -194,18 +199,23 @@ class AMQPBridgeTest { } private fun createAMQPServer(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer { + val baseDir = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDir / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock().also { doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions } serverConfig.configureWithDevSSLCertificate() + val keyStore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverConfig.loadSslKeyStore().internal - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverConfig.loadTrustStore().internal + override val keyStore = keyStore + override val trustStore = serverConfig.p2pSslOptions.trustStore.get() override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt index cb766bbcf8..d2ef1dbe5c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt @@ -13,7 +13,8 @@ import net.corda.core.utilities.seconds import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStoreSupplier +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient @@ -24,6 +25,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.DEV_INTERMEDIATE_CA import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.rigorousMock @@ -50,7 +52,6 @@ import java.io.Closeable import java.math.BigInteger import java.net.InetSocketAddress import java.security.KeyPair -import java.security.KeyStore import java.security.PrivateKey import java.security.Security import java.security.cert.X509CRL @@ -330,22 +331,25 @@ class CertificateRevocationListNodeTests { nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair { + val baseDirectory = temporaryFolder.root.toPath() / "client" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val nodeCert = clientConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val keyStore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = keyStore + override val trustStore = clientConfig.p2pSslOptions.trustStore.get() override val crlCheckSoftFail: Boolean = crlCheckSoftFail override val maxMessageSize: Int = maxMessageSize } @@ -360,21 +364,24 @@ class CertificateRevocationListNodeTests { nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair { + val baseDirectory = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(name).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() - val nodeCert = serverConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) - val serverTruststore = serverConfig.loadTrustStore().internal - val serverKeystore = serverConfig.loadSslKeyStore().internal + val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val keyStore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverKeystore - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverTruststore + override val keyStore = keyStore + override val trustStore = serverConfig.p2pSslOptions.trustStore.get() override val crlCheckSoftFail: Boolean = crlCheckSoftFail override val maxMessageSize: Int = maxMessageSize } @@ -384,22 +391,28 @@ class CertificateRevocationListNodeTests { amqpConfig), nodeCert) } - private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { - val nodeKeyStore = loadNodeKeyStore() - val (nodeCert, nodeKeys) = nodeKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + private fun Pair.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { + + val signingCertificateStore = first + val p2pSslConfiguration = second + val nodeKeyStore = signingCertificateStore.get() + val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) } val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint) - val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(2).toTypedArray()) - nodeKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) - nodeKeyStore.save() + val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.drop(2).toTypedArray()) + nodeKeyStore.update { + internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) + } nodeKeyStore.update { setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain) } - val sslKeyStore = loadSslKeyStore() - val (tlsCert, tlsKeys) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) + val sslKeyStore = p2pSslConfiguration.keyStore.get() + val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) } val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded)) - val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(3).toTypedArray()) - sslKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) - sslKeyStore.save() + val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.drop(3).toTypedArray()) + + sslKeyStore.update { + internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) + } sslKeyStore.update { setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain) } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 93016870ce..5ef36c2372 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -12,16 +12,18 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.ArtemisMessagingServer -import net.corda.nodeapi.ArtemisTcpTransport.Companion.CIPHER_SUITES import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores +import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer +import net.corda.nodeapi.internal.protonwrapper.netty.init +import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME @@ -29,13 +31,13 @@ import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.apache.activemq.artemis.api.core.RoutingType import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertArrayEquals import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder -import java.security.KeyStore import java.security.cert.X509Certificate import javax.net.ssl.* import kotlin.concurrent.thread @@ -102,34 +104,30 @@ class ProtonWrapperTests { } } - private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { - val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) - trustStore.save(trustStoreFile, trustStorePassword) - } + private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) { + trustStore.get(true)[X509Utilities.CORDA_ROOT_CA] = rootCert + } @Test fun `Test AMQP Client with invalid root certificate`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = temporaryFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = temporaryFolder.root.toPath() + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass") + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(ALICE_NAME, rootCa.certificate, intermediateCa) + signingCertificateStore.get(true).also { it.registerDevSigningCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } + sslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } sslConfig.createTrustStore(rootCa.certificate) - val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) + val keyStore = sslConfig.keyStore.get() + val trustStore = sslConfig.trustStore.get() val context = SSLContext.getInstance("TLS") val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray()) + keyManagerFactory.init(keyStore) val keyManagers = keyManagerFactory.keyManagers val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustMgrFactory.init(trustStore) @@ -139,7 +137,7 @@ class ProtonWrapperTests { val serverSocketFactory = context.serverSocketFactory val serverSocket = serverSocketFactory.createServerSocket(serverPort) as SSLServerSocket - val serverParams = SSLParameters(CIPHER_SUITES.toTypedArray(), + val serverParams = SSLParameters(InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray(), arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true @@ -388,11 +386,16 @@ class ProtonWrapperTests { } private fun createArtemisServerAndClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair { + val baseDirectory = temporaryFolder.root.toPath() / "artemis" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val artemisConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(CHARLIE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).crlCheckSoftFail @@ -400,28 +403,32 @@ class ProtonWrapperTests { artemisConfig.configureWithDevSSLCertificate() val server = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), maxMessageSize) - val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), maxMessageSize) + val client = ArtemisMessagingClient(artemisConfig.p2pSslOptions, NetworkHostAndPort("localhost", artemisPort), maxMessageSize) server.start() client.start() return Pair(server, client) } private fun createClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient { + val baseDirectory = temporaryFolder.root.toPath() / "client" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val clientTruststore = clientConfig.p2pSslOptions.trustStore.get() + val clientKeystore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = clientKeystore + override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } @@ -434,21 +441,25 @@ class ProtonWrapperTests { } private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient { + val baseDirectory = temporaryFolder.root.toPath() / "client_%$id" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val clientTruststore = clientConfig.p2pSslOptions.trustStore.get() + val clientKeystore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = clientKeystore + override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } @@ -460,21 +471,25 @@ class ProtonWrapperTests { } private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer { + val baseDirectory = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock().also { - doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(name).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() - val serverTruststore = serverConfig.loadTrustStore().internal - val serverKeystore = serverConfig.loadSslKeyStore().internal + val serverTruststore = serverConfig.p2pSslOptions.trustStore.get() + val serverKeystore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverKeystore - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverTruststore + override val keyStore = serverKeystore + override val trustStore = serverTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 0121d8be47..4f3e80594b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -3,6 +3,7 @@ package net.corda.node.services.messaging import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.generateKeyPair +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.internal.configureDatabase @@ -19,6 +20,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -69,11 +71,18 @@ class ArtemisMessagingTest { @Before fun setUp() { abstract class AbstractNodeConfiguration : NodeConfiguration + + val baseDirectory = temporaryFolder.root.toPath() + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + config = rigorousMock().also { doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(FlowTimeoutConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).flowTimeout 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 91d4299702..3573ba6c9f 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 @@ -12,7 +12,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY -import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.SignedNetworkParameters @@ -21,6 +20,7 @@ import net.corda.testing.core.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.internal.NodeHandleInternal +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.internal.* import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat @@ -247,13 +247,10 @@ private fun DriverDSLImpl.startNode(providedName: CordaX500Name, devMode: Boolea var customOverrides = emptyMap() if (!devMode) { val nodeDir = baseDirectory(providedName) - val nodeSslConfig = object : NodeSSLConfiguration { - override val baseDirectory = nodeDir - override val keyStorePassword = "cordacadevpass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail = true - } - nodeSslConfig.configureDevKeyAndTrustStores(providedName) + val certificatesDirectory = nodeDir / "certificates" + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + p2pSslConfig.configureDevKeyAndTrustStores(providedName, signingCertStore, certificatesDirectory) customOverrides = mapOf("devMode" to "false") } return startNode(providedName = providedName, customOverrides = customOverrides) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index 9a6192b4b1..c64a3773f5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -17,13 +17,13 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation -import net.corda.testing.internal.createNodeSslConfig +import net.corda.testing.internal.p2pSslOptions import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.assertj.core.api.Assertions.assertThat @@ -58,12 +58,12 @@ class ArtemisRpcTests { val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) } @Test fun rpc_with_ssl_disabled() { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), null, false, null) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), null, false, null) } @Test @@ -73,7 +73,7 @@ class ArtemisRpcTests { val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") // here client sslOptions are passed null (as in, do not use SSL) assertThatThrownBy { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, null) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, null) }.isInstanceOf(ActiveMQConnectionTimedOutException::class.java) } @@ -91,11 +91,11 @@ class ArtemisRpcTests { val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") assertThatThrownBy { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) }.isInstanceOf(RPCException::class.java) } - private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, + private fun testSslCommunication(nodeSSlconfig: MutualSslConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index feda5b405f..7fb2986bc4 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -2,7 +2,6 @@ package net.corda.services.messaging import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.copyTo import net.corda.core.internal.createDirectories import net.corda.core.internal.exists import net.corda.core.internal.toX500Name @@ -11,9 +10,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_U import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA import net.corda.nodeapi.internal.DEV_ROOT_CA -import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.loadDevCaTrustStore +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException @@ -84,57 +84,35 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { @Test fun `login with invalid certificate chain`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = Files.createTempDirectory("certs") - override val keyStorePassword: String get() = "cordacadevpass" - override val trustStorePassword: String get() = "trustpass" - override val crlCheckSoftFail: Boolean = true + val certsDir = Files.createTempDirectory("certs") + certsDir.createDirectories() + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certsDir) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certsDir) - init { - val legalName = CordaX500Name("MegaCorp", "London", "GB") - certificatesDirectory.createDirectories() - if (!trustStoreFile.exists()) { - javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").use { it.copyTo(trustStoreFile) } - } - - val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - // Set name constrain to the legal name. - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf()) - val clientCACert = X509Utilities.createCertificate( - CertificateType.INTERMEDIATE_CA, - DEV_INTERMEDIATE_CA.certificate, - DEV_INTERMEDIATE_CA.keyPair, - legalName.x500Principal, - clientKeyPair.public, - nameConstraints = nameConstraints) - - val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - // Using different x500 name in the TLS cert which is not allowed in the name constraints. - val clientTLSCert = X509Utilities.createCertificate( - CertificateType.TLS, - clientCACert, - clientKeyPair, - CordaX500Name("MiniCorp", "London", "GB").x500Principal, - tlsKeyPair.public) - - loadNodeKeyStore(createNew = true).update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_CA, - clientKeyPair.private, - listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) - } - - loadSslKeyStore(createNew = true).update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_TLS, - tlsKeyPair.private, - listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) - } - } + val legalName = CordaX500Name("MegaCorp", "London", "GB") + if (!p2pSslConfig.trustStore.path.exists()) { + val trustStore = p2pSslConfig.trustStore.get(true) + loadDevCaTrustStore().copyTo(trustStore) } - val attacker = clientTo(alice.node.configuration.p2pAddress, sslConfig) + val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Set name constrain to the legal name. + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf()) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, DEV_INTERMEDIATE_CA.certificate, DEV_INTERMEDIATE_CA.keyPair, legalName.x500Principal, clientKeyPair.public, nameConstraints = nameConstraints) + val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Using different x500 name in the TLS cert which is not allowed in the name constraints. + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKeyPair, CordaX500Name("MiniCorp", "London", "GB").x500Principal, tlsKeyPair.public) + + signingCertStore.get(createNew = true).update { + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + } + + p2pSslConfig.keyStore.get(createNew = true).update { + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + } + + val attacker = clientTo(alice.node.configuration.p2pAddress, p2pSslConfig) assertThatExceptionOfType(ActiveMQNotConnectedException::class.java).isThrownBy { attacker.start(PEER_USER, PEER_USER) } 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 0214cd0f48..2f52ad269d 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,12 +18,12 @@ import net.corda.node.internal.NodeWithInfo 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.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.node.User import net.corda.testing.core.singleIdentity import net.corda.testing.internal.configureTestSSL +import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException @@ -94,7 +94,7 @@ abstract class MQSecurityTest : NodeBasedTest() { assertAllQueueCreationAttacksFail(randomQueue) } - fun clientTo(target: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient { + fun clientTo(target: NetworkHostAndPort, sslConfiguration: MutualSslConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient { val client = SimpleMQClient(target, sslConfiguration) clients += client return client 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 58e04948c4..e70e77d6b5 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 @@ -3,8 +3,8 @@ package net.corda.services.messaging import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.ArtemisTcpTransport -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.testing.internal.configureTestSSL import org.apache.activemq.artemis.api.core.client.* @@ -12,7 +12,7 @@ import org.apache.activemq.artemis.api.core.client.* * As the name suggests this is a simple client for connecting to MQ brokers. */ class SimpleMQClient(val target: NetworkHostAndPort, - private val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) { + private val config: MutualSslConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) { companion object { val DEFAULT_MQ_LEGAL_NAME = CordaX500Name(organisation = "SimpleMQClient", locality = "London", country = "GB") } @@ -22,7 +22,7 @@ class SimpleMQClient(val target: NetworkHostAndPort, lateinit var producer: ClientProducer fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) { - val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(target, config, enableSSL = enableSSL) + val tcpTransport = p2pConnectorTcpTransport(target, config, enableSSL = enableSSL) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { isBlockOnNonDurableSend = true threadPoolMaxSize = 1 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 40f0faf110..1f99dc9bf1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -44,6 +44,7 @@ import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* import net.corda.node.services.config.* +import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.shell.toShellConfig import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver @@ -248,7 +249,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val trustRoot = initKeyStore() val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) startDatabase() - val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA) + val nodeCa = configuration.signingCertificateStore.get()[X509Utilities.CORDA_CLIENT_CA] identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) return database.use { it.transaction { @@ -278,7 +279,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.info("Node starting up ...") val trustRoot = initKeyStore() - val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA) + val nodeCa = configuration.signingCertificateStore.get()[X509Utilities.CORDA_CLIENT_CA] initialiseJVMAgents() schemaService.mappedSchemasWarnings().forEach { @@ -368,10 +369,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, open fun startShell() { if (configuration.shouldInitCrashShell()) { val shellConfiguration = configuration.toShellConfig() - shellConfiguration.sshHostKeyDirectory?.let { + shellConfiguration.sshdPort?.let { log.info("Binding Shell SSHD server on port $it.") } - InteractiveShell.startShellInternal(shellConfiguration, cordappLoader.appClassLoader) + InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader) } } @@ -696,8 +697,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun validateKeyStore(): X509Certificate { val containCorrectKeys = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - val sslKeystore = configuration.loadSslKeyStore() - val identitiesKeystore = configuration.loadNodeKeyStore() + val sslKeystore = configuration.p2pSslOptions.keyStore.get() + val identitiesKeystore = configuration.signingCertificateStore.get() X509Utilities.CORDA_CLIENT_TLS in sslKeystore && X509Utilities.CORDA_CLIENT_CA in identitiesKeystore } catch (e: KeyStoreException) { log.warn("Certificate key store found but key store password does not match configuration.") @@ -714,9 +715,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } // Check all cert path chain to the trusted root - val sslCertChainRoot = configuration.loadSslKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).last() - val nodeCaCertChainRoot = configuration.loadNodeKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_CA).last() - val trustRoot = configuration.loadTrustStore().getCertificate(X509Utilities.CORDA_ROOT_CA) + val sslCertChainRoot = configuration.p2pSslOptions.keyStore.get().query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.last() + val nodeCaCertChainRoot = configuration.signingCertificateStore.get().query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.last() + val trustRoot = configuration.p2pSslOptions.trustStore.get()[X509Utilities.CORDA_ROOT_CA] require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." } @@ -760,7 +761,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return notaryConfig.run { when { raft != null -> { - val uniquenessProvider = RaftUniquenessProvider(configuration, database, platformClock, monitoringService.metrics, raft) + val uniquenessProvider = RaftUniquenessProvider(configuration.baseDirectory, configuration.p2pSslOptions, database, platformClock, monitoringService.metrics, raft) (if (validating) ::RaftValidatingNotaryService else ::RaftNonValidatingNotaryService)(services, notaryKey, uniquenessProvider) } bftSMaRt != null -> { @@ -804,7 +805,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkParameters: NetworkParameters) private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { - val keyStore = configuration.loadNodeKeyStore() + val keyStore = configuration.signingCertificateStore.get() val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { // Node's main identity or if it's a single node notary @@ -819,25 +820,25 @@ abstract class AbstractNode(val configuration: NodeConfiguration, if (privateKeyAlias !in keyStore) { singleName ?: throw IllegalArgumentException( "Unable to find in the key store the identity of the distributed notary the node is part of") - log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") + log.info("$privateKeyAlias not found in key store, generating fresh key!") // TODO This check shouldn't be needed check(singleName == configuration.myLegalName) keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair()) } - val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias) + val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) } // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. val compositeKeyAlias = "$id-composite-key" val certificates = if (compositeKeyAlias in keyStore) { // Use composite key instead if it exists - val certificate = keyStore.getCertificate(compositeKeyAlias) + val certificate = keyStore[compositeKeyAlias] // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + // the tail of the private key certificates, as they are both signed by the same certificate chain. - listOf(certificate) + keyStore.getCertificateChain(privateKeyAlias).drop(1) + listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1) } else { - keyStore.getCertificateChain(privateKeyAlias).let { + keyStore.query { getCertificateChain(privateKeyAlias) }.let { check(it[0] == x509Cert) { "Certificates from key store do not line up!" } it } @@ -1027,4 +1028,13 @@ fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfi else -> throw CouldNotCreateDataSourceException("Could not create the DataSource: ${ex.message}", ex) } } +} + +fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? { + + if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) { + return null + } + // Here we're using the node's RPC key store as the RPC client's trust store. + return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword) } \ No newline at end of file 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 383bc11ba2..c135093517 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -227,12 +227,12 @@ open class Node(configuration: NodeConfiguration, startLocalRpcBroker(securityManager) } - val bridgeControlListener = BridgeControlListener(configuration, network.serverAddress, networkParameters.maxMessageSize) + val bridgeControlListener = BridgeControlListener(configuration.p2pSslOptions, network.serverAddress, networkParameters.maxMessageSize) printBasicNodeInfo("Advertised P2P messaging addresses", nodeInfo.addresses.joinToString()) val rpcServerConfiguration = RPCServerConfiguration.DEFAULT rpcServerAddresses?.let { - internalRpcMessagingClient = InternalRPCMessagingClient(configuration, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.loadSslKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal), rpcServerConfiguration) + internalRpcMessagingClient = InternalRPCMessagingClient(configuration.p2pSslOptions, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.p2pSslOptions.keyStore.get()[X509Utilities.CORDA_CLIENT_TLS].subjectX500Principal), rpcServerConfiguration) printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString()) } @@ -271,9 +271,9 @@ open class Node(configuration: NodeConfiguration, val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(configuration, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) + ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) } else { - ArtemisRpcBroker.withoutSsl(configuration, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) + ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) } } rpcBroker!!.closeOnStop() 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 051097ff3f..d5ba9fd453 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 @@ -8,11 +8,10 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists import net.corda.nodeapi.internal.* -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.toProperties import net.corda.nodeapi.internal.crypto.X509KeyStore -import net.corda.nodeapi.internal.crypto.loadKeyStore -import net.corda.nodeapi.internal.crypto.save import org.slf4j.LoggerFactory import java.nio.file.Path @@ -68,22 +67,33 @@ object ConfigHelper { * 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) +fun NodeConfiguration.configureWithDevSSLCertificate() = p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory) // TODO Move this to KeyStoreConfigHelpers -fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { +fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path) { + + val specifiedTrustStore = trustStore.getOptional() + + val specifiedKeyStore = keyStore.getOptional() + val specifiedSigningStore = signingCertificateStore.getOptional() + + if (specifiedTrustStore != null && specifiedKeyStore != null && specifiedSigningStore != null) return certificatesDirectory.createDirectories() - if (!trustStoreFile.exists()) { - loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS).save(trustStoreFile, trustStorePassword) + + if (specifiedTrustStore == null) { + loadDevCaTrustStore().copyTo(trustStore.get(true)) } - if (!sslKeystore.exists() || !nodeKeystore.exists()) { - val (nodeKeyStore) = createDevKeyStores(myLegalName) + + if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) { + val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.password).get(true).also { it.registerDevSigningCertificates(myLegalName) } + + FileBasedCertificateStoreSupplier(keyStore.path, keyStore.password).get(true).also { it.registerDevP2pCertificates(myLegalName) } // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" if (distributedServiceKeystore.exists()) { val serviceKeystore = X509KeyStore.fromFile(distributedServiceKeystore, DEV_CA_KEY_STORE_PASS) - nodeKeyStore.update { + signingKeyStore.update { serviceKeystore.aliases().forEach { if (serviceKeystore.internal.isKeyEntry(it)) { setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_PRIVATE_KEY_PASS), serviceKeystore.getCertificateChain(it)) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index c095fbadf3..2754629f75 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -11,7 +11,9 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -30,7 +32,7 @@ private val DEFAULT_FLOW_MONITOR_PERIOD_MILLIS: Duration = Duration.ofMinutes(1) private val DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS: Duration = Duration.ofMinutes(1) private const val CORDAPPS_DIR_NAME_DEFAULT = "cordapps" -interface NodeConfiguration : NodeSSLConfiguration { +interface NodeConfiguration { val myLegalName: CordaX500Name val emailAddress: String val jmxMonitoringHttpPort: Int? @@ -69,9 +71,16 @@ interface NodeConfiguration : NodeSSLConfiguration { val effectiveH2Settings: NodeH2Settings? val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS - val cordappDirectories: List get() = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT) + val crlCheckSoftFail: Boolean val jmxReporterType : JmxReporterType? get() = defaultJmxReporterType + val baseDirectory: Path + val certificatesDirectory: Path + val signingCertificateStore: FileBasedCertificateStoreSupplier + val p2pSslOptions: MutualSslConfiguration + + val cordappDirectories: List + fun validate(): List companion object { @@ -176,8 +185,8 @@ data class NodeConfigurationImpl( override val myLegalName: CordaX500Name, override val jmxMonitoringHttpPort: Int? = null, override val emailAddress: String, - override val keyStorePassword: String, - override val trustStorePassword: String, + private val keyStorePassword: String, + private val trustStorePassword: String, override val crlCheckSoftFail: Boolean, override val dataSourceProperties: Properties, override val compatibilityZoneURL: URL? = null, @@ -243,6 +252,17 @@ data class NodeConfigurationImpl( } } + override val certificatesDirectory = baseDirectory / "certificates" + + private val signingCertificateStorePath = certificatesDirectory / "nodekeystore.jks" + override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword) + + private val p2pKeystorePath: Path get() = certificatesDirectory / "sslkeystore.jks" + private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword) + private val p2pTrustStoreFilePath: Path get() = certificatesDirectory / "truststore.jks" + private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword) + override val p2pSslOptions: MutualSslConfiguration = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) + override val rpcOptions: NodeRpcOptions get() { return actualRpcSettings.asOptions() @@ -356,8 +376,6 @@ data class NodeConfigurationImpl( } } - - data class NodeRpcSettings( val address: NetworkHostAndPort?, val adminAddress: NetworkHostAndPort?, diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt index 5a0eaa4ac9..3138a29934 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config.shell import net.corda.core.internal.div +import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER import net.corda.tools.shell.ShellConfiguration @@ -14,8 +15,8 @@ fun NodeConfiguration.toShellConfig() = ShellConfiguration( cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, user = INTERNAL_SHELL_USER, password = INTERNAL_SHELL_USER, - hostAndPort = this.rpcOptions.adminAddress, - nodeSslConfig = this, + hostAndPort = this.rpcOptions.address, + ssl = clientSslOptionsCompatibleWith(this.rpcOptions), sshdPort = this.sshd?.port, sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR, noLocalShell = this.noLocalShell) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 170dfd1b37..bb46278787 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import io.netty.channel.unix.Errors import net.corda.core.internal.ThreadBox import net.corda.core.internal.div import net.corda.core.serialization.SingletonSerializeAsToken @@ -12,13 +11,13 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_P2P_ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.PEER_ROLE import net.corda.core.internal.errors.AddressBindingException import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pAcceptorTcpTransport import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -120,7 +119,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, bindingsDirectory = (artemisDir / "bindings").toString() journalDirectory = (artemisDir / "journal").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString() - acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config)) + acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config.p2pSslOptions)) // Enable built in message deduplication. Note we still have to do our own as the delayed commits // and our own definition of commit mean that the built in deduplication cannot remove all duplicates. idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess @@ -162,8 +161,8 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { - val keyStore = config.loadSslKeyStore().internal - val trustStore = config.loadTrustStore().internal + val keyStore = config.p2pSslOptions.keyStore.get().value.internal + val trustStore = config.p2pSslOptions.trustStore.get().value.internal val securityConfig = object : SecurityConfiguration() { // Override to make it work with our login module diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt index 9b5191b312..abb07763d1 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt @@ -6,9 +6,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManager -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -16,13 +16,13 @@ import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl /** * Used by the Node to communicate with the RPC broker. */ -class InternalRPCMessagingClient(val sslConfig: SSLConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable { +class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable { private var locator: ServerLocator? = null private var rpcServer: RPCServer? = null fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) { - val tcpTransport = ArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig) + val tcpTransport = InternalArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 44667c43b8..95c340ead1 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -26,7 +26,6 @@ import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.SenderDeduplicationId import net.corda.node.utilities.AffinityExecutor -import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL @@ -34,6 +33,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.bridging.BridgeControl import net.corda.nodeapi.internal.bridging.BridgeEntry import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -149,7 +149,7 @@ class P2PMessagingClient(val config: NodeConfiguration, started = true log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = p2pConnectorTcpTransport(serverAddress, config) + val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt index 5fc3c98bc8..af6158e33a 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt @@ -8,7 +8,7 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECU import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG import net.corda.node.internal.security.RPCSecurityManager import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration import org.apache.activemq.artemis.core.server.ActiveMQServer @@ -28,17 +28,17 @@ class ArtemisRpcBroker internal constructor( private val maxMessageSize: Int, private val jmxEnabled: Boolean = false, private val baseDirectory: Path, - private val nodeConfiguration: SSLConfiguration, + private val nodeConfiguration: MutualSslConfiguration, private val shouldStartLocalShell: Boolean) : ArtemisBroker { companion object { private val logger = loggerFor() - fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { + fun withSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { return ArtemisRpcBroker(address, adminAddress, sslOptions, true, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell) } - fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { + fun withoutSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { return ArtemisRpcBroker(address, adminAddress, null, false, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell) } } @@ -83,14 +83,14 @@ class ArtemisRpcBroker internal constructor( @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(loginListener: LoginListener): ActiveMQJAASSecurityManager { - val keyStore = nodeConfiguration.loadSslKeyStore().internal - val trustStore = nodeConfiguration.loadTrustStore().internal + val keyStore = nodeConfiguration.keyStore.get() + val trustStore = nodeConfiguration.trustStore.get() val securityConfig = object : SecurityConfiguration() { override fun getAppConfigurationEntry(name: String): Array { val options = mapOf( RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl), - NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore) + NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore.value.internal, trustStore.value.internal) ) return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)) } diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index 3859e7cd8c..f17dcee8aa 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -4,12 +4,12 @@ import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.artemis.BrokerJaasLoginModule import net.corda.node.internal.artemis.SecureArtemisConfiguration -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcAcceptorTcpTransport +import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.security.Role @@ -17,7 +17,7 @@ import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings import java.nio.file.Path -internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: SSLConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() { +internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: MutualSslConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() { val loginListener: (String) -> Unit init { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 1e5c46d28b..1265461d17 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -28,8 +28,7 @@ import net.corda.core.utilities.debug import net.corda.node.services.config.RaftConfig import net.corda.node.services.transactions.RaftTransactionCommitLog.Commands.CommitTransaction import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.nodeapi.internal.config.NodeSSLConfiguration -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.nio.file.Path @@ -51,7 +50,8 @@ import javax.persistence.Table */ @ThreadSafe class RaftUniquenessProvider( - private val transportConfiguration: NodeSSLConfiguration, + private val storagePath: Path, + private val transportConfiguration: MutualSslConfiguration, private val db: CordaPersistence, private val clock: Clock, private val metrics: MetricRegistry, @@ -96,8 +96,6 @@ class RaftUniquenessProvider( var index: Long = 0 ) - /** Directory storing the Raft log and state machine snapshots */ - private val storagePath: Path = transportConfiguration.baseDirectory private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer @@ -155,14 +153,14 @@ class RaftUniquenessProvider( .build() } - private fun buildTransport(config: SSLConfiguration): Transport? { + private fun buildTransport(config: MutualSslConfiguration): Transport? { return NettyTransport.builder() .withSsl() .withSslProtocol(SslProtocol.TLSv1_2) - .withKeyStorePath(config.sslKeystore.toString()) - .withKeyStorePassword(config.keyStorePassword) - .withTrustStorePath(config.trustStoreFile.toString()) - .withTrustStorePassword(config.trustStorePassword) + .withKeyStorePath(config.keyStore.path.toString()) + .withKeyStorePassword(config.keyStore.password) + .withTrustStorePath(config.trustStore.path.toString()) + .withTrustStorePassword(config.trustStore.password) .build() } 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 4ec5319c28..b3186b8d7b 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 @@ -6,7 +6,8 @@ import net.corda.core.internal.* import net.corda.core.utilities.contextLogger import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.CertificateStoreSupplier import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -33,7 +34,8 @@ import javax.security.auth.x500.X500Principal * needed. */ // TODO: Use content signer instead of keypairs. -open class NetworkRegistrationHelper(private val config: SSLConfiguration, +open class NetworkRegistrationHelper(private val certificatesDirectory: Path, + private val signingCertificateStore: CertificateStoreSupplier, private val myLegalName: CordaX500Name, private val emailAddress: String, private val certService: NetworkRegistrationService, @@ -48,9 +50,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, val logger = contextLogger() } - private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt" - // TODO: Use different password for private key. - private val privateKeyPassword = config.keyStorePassword + private val requestIdStore = certificatesDirectory / "certificate-request-id.txt" private val rootTrustStore: X509KeyStore protected val rootCert: X509Certificate @@ -75,12 +75,14 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, * @throws CertificateRequestException if the certificate retrieved by doorman is invalid. */ fun buildKeystore() { - config.certificatesDirectory.createDirectories() - val nodeKeyStore = config.loadNodeKeyStore(createNew = true) + certificatesDirectory.createDirectories() + val nodeKeyStore = signingCertificateStore.get(createNew = true) if (keyAlias in nodeKeyStore) { println("Certificate already exists, Corda node will now terminate...") return } + // TODO: Use different password for private key. + val privateKeyPassword = nodeKeyStore.password val tlsCrlIssuerCert = validateAndGetTlsCrlIssuerCert() if (tlsCrlIssuerCert == null && isTlsCrlIssuerCertRequired()) { System.err.println("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer. @@ -89,7 +91,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, throw IllegalArgumentException("TLS CRL issuer certificate not found in the trust store.") } - val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY) + val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) val requestId = try { submitOrResumeCertificateSigningRequest(keyPair) @@ -110,7 +112,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, throw certificateRequestException } validateCertificates(keyPair.public, certificates) - storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias) + storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias, privateKeyPassword) onSuccess(keyPair, certificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) // All done, clean up temp files. requestIdStore.deleteIfExists() @@ -148,25 +150,29 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, println("Certificate signing request approved, storing private key with the certificate chain.") } - private fun storePrivateKeyWithCertificates(nodeKeystore: X509KeyStore, keyPair: KeyPair, certificates: List, keyAlias: String) { + private fun storePrivateKeyWithCertificates(nodeKeystore: CertificateStore, keyPair: KeyPair, certificates: List, keyAlias: String, keyPassword: String) { // Save private key and certificate chain to the key store. - nodeKeystore.setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = config.keyStorePassword) - nodeKeystore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - nodeKeystore.save() - println("Private key '$keyAlias' and certificate stored in ${config.nodeKeystore}.") + with(nodeKeystore.value) { + setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = keyPassword) + internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + save() + } + println("Private key '$keyAlias' and certificate stored in node signing keystore.") } - private fun X509KeyStore.loadOrCreateKeyPair(alias: String): KeyPair { + private fun CertificateStore.loadOrCreateKeyPair(alias: String, privateKeyPassword: String = password): KeyPair { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (alias !in this) { val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) // Save to the key store. - setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) - save() + with(value) { + setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) + save() + } } - return getCertificateAndKeyPair(alias, privateKeyPassword).keyPair + return query { getCertificateAndKeyPair(alias, privateKeyPassword) }.keyPair } /** @@ -243,7 +249,9 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause) class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : - NetworkRegistrationHelper(config, + NetworkRegistrationHelper( + config.certificatesDirectory, + config.signingCertificateStore, config.myLegalName, config.emailAddress, certService, @@ -263,7 +271,7 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: } private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List, tlsCertCrlIssuer: X500Name?) { - config.loadSslKeyStore(createNew = true).update { + config.p2pSslOptions.keyStore.get(createNew = true).update { println("Generating SSL certificate for node messaging service.") val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val sslCert = X509Utilities.createCertificate( @@ -277,17 +285,17 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: logger.info("Generated TLS certificate: $sslCert") setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) } - println("SSL private key and certificate stored in ${config.sslKeystore}.") + println("SSL private key and certificate stored in ${config.p2pSslOptions.keyStore.path}.") } private fun createTruststore(rootCertificate: X509Certificate) { // Save root certificates to trust store. - config.loadTrustStore(createNew = true).update { + config.p2pSslOptions.trustStore.get(createNew = true).update { println("Generating trust store for corda node.") // Assumes certificate chain always starts with client certificate and end with root certificate. setCertificate(CORDA_ROOT_CA, rootCertificate) } - println("Node trust store stored in ${config.trustStoreFile}.") + println("Node trust store stored in ${config.p2pSslOptions.trustStore.path}.") } override fun validateAndGetTlsCrlIssuerCert(): X509Certificate? { @@ -296,8 +304,9 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: if (principalMatchesCertificatePrincipal(tlsCertCrlIssuer, rootCert)) { return rootCert } - return if (config.trustStoreFile.exists()) { - findMatchingCertificate(tlsCertCrlIssuer, config.loadTrustStore()) + val trustStore = config.p2pSslOptions.trustStore.getOptional() + return if (trustStore != null) { + findMatchingCertificate(tlsCertCrlIssuer, trustStore.value) } else { null } 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 eaae962161..09d61e64a6 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 @@ -22,6 +22,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.rigorousMock import org.assertj.core.api.Assertions.* @@ -52,10 +53,12 @@ class NetworkRegistrationHelperTest { val baseDirectory = fs.getPath("/baseDir").createDirectories() abstract class AbstractNodeConfiguration : NodeConfiguration + val certificatesDirectory = baseDirectory / "certificates" config = rigorousMock().also { doReturn(baseDirectory).whenever(it).baseDirectory - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)).whenever(it).p2pSslOptions + doReturn(CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)).whenever(it).signingCertificateStore doReturn(nodeLegalName).whenever(it).myLegalName doReturn("").whenever(it).emailAddress doReturn(null).whenever(it).tlsCertCrlDistPoint @@ -71,30 +74,30 @@ class NetworkRegistrationHelperTest { @Test fun `successful registration`() { - assertThat(config.nodeKeystore).doesNotExist() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.signingCertificateStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(it.first.certificate) } createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA).buildKeystore() - val nodeKeystore = config.loadNodeKeyStore() - val sslKeystore = config.loadSslKeyStore() - val trustStore = config.loadTrustStore() + val nodeKeystore = config.signingCertificateStore.get() + val sslKeystore = config.p2pSslOptions.keyStore.get() + val trustStore = config.p2pSslOptions.trustStore.get() nodeKeystore.run { assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) - assertThat(CertRole.extract(getCertificate(X509Utilities.CORDA_CLIENT_CA))).isEqualTo(CertRole.NODE_CA) + assertThat(CertRole.extract(this[X509Utilities.CORDA_CLIENT_CA])).isEqualTo(CertRole.NODE_CA) } sslKeystore.run { assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) - val nodeTlsCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + val nodeTlsCertChain = query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) } assertThat(nodeTlsCertChain).hasSize(4) // The TLS cert has the same subject as the node CA cert assertThat(CordaX500Name.build(nodeTlsCertChain[0].subjectX500Principal)).isEqualTo(nodeLegalName) @@ -104,7 +107,7 @@ class NetworkRegistrationHelperTest { trustStore.run { assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(rootAndIntermediateCA.first.certificate) + assertThat(this[X509Utilities.CORDA_ROOT_CA]).isEqualTo(rootAndIntermediateCA.first.certificate) } } @@ -152,18 +155,18 @@ class NetworkRegistrationHelperTest { @Test fun `create service identity cert`() { - assertThat(config.nodeKeystore).doesNotExist() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.signingCertificateStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(it.first.certificate) } createRegistrationHelper(CertRole.SERVICE_IDENTITY, rootAndIntermediateCA).buildKeystore() - val nodeKeystore = config.loadNodeKeyStore() + val nodeKeystore = config.signingCertificateStore.get() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key" @@ -172,7 +175,7 @@ class NetworkRegistrationHelperTest { assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) - assertThat(CertRole.extract(getCertificate(serviceIdentityAlias))).isEqualTo(CertRole.SERVICE_IDENTITY) + assertThat(CertRole.extract(this[serviceIdentityAlias])).isEqualTo(CertRole.SERVICE_IDENTITY) } } @@ -223,7 +226,8 @@ class NetworkRegistrationHelperTest { return when (certRole) { CertRole.NODE_CA -> NodeRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) CertRole.SERVICE_IDENTITY -> NetworkRegistrationHelper( - config, + config.certificatesDirectory, + config.signingCertificateStore, config.myLegalName, config.emailAddress, certService, 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 10f6392bd1..25f9f01371 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 @@ -5,7 +5,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory -import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader +import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name @@ -23,6 +23,7 @@ import net.corda.node.NodeRegistrationOption import net.corda.node.VersionInfo import net.corda.node.internal.Node import net.corda.node.internal.NodeWithInfo +import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService @@ -160,7 +161,8 @@ class DriverDSLImpl( private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.corda.rpcOptions.address - val client = createCordaRPCClientWithInternalSslAndClassLoader(config.corda.rpcOptions.adminAddress, sslConfiguration = config.corda) + val clientRpcSslOptions = clientSslOptionsCompatibleWith(config.corda.rpcOptions) + val client = createCordaRPCClientWithSslAndClassLoader(rpcAddress, sslConfiguration = clientRpcSslOptions) val connectionFuture = poll(executorService, "RPC connection") { try { config.corda.rpcUsers[0].run { client.start(username, password) } @@ -798,8 +800,13 @@ class DriverDSLImpl( config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers") config += "useHTTPS" to useHTTPS config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString() - config += "keyStorePassword" to configuration.keyStorePassword - config += "trustStorePassword" to configuration.trustStorePassword + + config += "keyStorePath" to configuration.p2pSslOptions.keyStore.path.toString() + config += "keyStorePassword" to configuration.p2pSslOptions.keyStore.password + + config += "trustStorePath" to configuration.p2pSslOptions.trustStore.path.toString() + config += "trustStorePassword" to configuration.p2pSslOptions.trustStore.password + return config } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 4a75faf696..e571ee1cfd 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -53,6 +53,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.TestCorDapp +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.testThreadFactory @@ -456,8 +457,11 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe private fun createNodeImpl(parameters: InternalMockNodeParameters, nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode, start: Boolean): MockNode { val id = parameters.forcedID ?: nextNodeId++ - val config = mockNodeConfiguration().also { - doReturn(baseDirectory(id).createDirectories()).whenever(it).baseDirectory + val baseDirectory = baseDirectory(id) + val certificatesDirectory = baseDirectory / "certificates" + certificatesDirectory.createDirectories() + val config = mockNodeConfiguration(certificatesDirectory).also { + doReturn(baseDirectory).whenever(it).baseDirectory doReturn(parameters.legalName ?: CordaX500Name("Mock Company $id", "London", "GB")).whenever(it).myLegalName doReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")).whenever(it).dataSourceProperties doReturn(emptyList()).whenever(it).extraNetworkMapKeys @@ -561,12 +565,17 @@ abstract class MessagingServiceSpy { abstract fun send(message: Message, target: MessageRecipients, sequenceKey: Any) } -private fun mockNodeConfiguration(): NodeConfiguration { +private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguration { @DoNotImplement abstract class AbstractNodeConfiguration : NodeConfiguration + + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + return rigorousMock().also { - doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn("trustpass").whenever(it).trustStorePassword + doReturn(certificatesDirectory.createDirectories()).whenever(it).certificatesDirectory + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(emptyList()).whenever(it).rpcUsers doReturn(null).whenever(it).notary doReturn(DatabaseConfig()).whenever(it).database 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 1ce8494c99..94b38ddd2c 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 @@ -21,8 +21,8 @@ import net.corda.core.utilities.seconds import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.internal.InternalArtemisTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE @@ -220,14 +220,14 @@ data class RPCDriverDSL( bindingsDirectory = "$artemisDir/bindings" journalDirectory = "$artemisDir/journal" largeMessagesDirectory = "$artemisDir/large-messages" - acceptorConfigurations = setOf(ArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null)) + acceptorConfigurations = setOf(InternalArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null)) configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { - return ArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, null) + return InternalArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, null) } } @@ -339,7 +339,7 @@ data class RPCDriverDSL( configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT ): CordaFuture { return driverDSL.executorService.fork { - val client = RPCClient(ArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration) + val client = RPCClient(InternalArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration) val connection = client.start(rpcOpsClass, username, password, externalTrace) driverDSL.shutdownManager.registerShutdown { connection.close() diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt index 4ce15b78af..bce2152d57 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt @@ -4,7 +4,9 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createFile import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.crypto.* import org.apache.commons.io.FileUtils import sun.security.tools.keytool.CertAndKeyGen @@ -65,7 +67,7 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { val keyStoreFile = keyStore.toTemporaryFile("sslkeystore", directory = directory) val trustStoreFile = trustStore.toTemporaryFile("truststore", directory = directory) - val sslConfiguration = sslConfiguration(directory) + val sslConfiguration = sslConfiguration(keyStoreFile, trustStoreFile) return object : AutoClosableSSLConfiguration { override val value = sslConfiguration @@ -77,16 +79,16 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { } } - data class TestSslOptions(override val certificatesDirectory: Path, - override val keyStorePassword: String, - override val trustStorePassword: String, - override val crlCheckSoftFail: Boolean) : SSLConfiguration + private fun sslConfiguration(keyStoreFile: TemporaryFile, trustStoreFile: TemporaryFile): MutualSslConfiguration { - private fun sslConfiguration(directory: Path) = TestSslOptions(directory, keyStore.password, trustStore.password, true) + val keyStore = FileBasedCertificateStoreSupplier(keyStoreFile.file, keyStore.password) + val trustStore = FileBasedCertificateStoreSupplier(trustStoreFile.file, trustStore.password) + return SslConfiguration.mutual(keyStore, trustStore) + } } interface AutoClosableSSLConfiguration : AutoCloseable { - val value: SSLConfiguration + val value: MutualSslConfiguration } typealias KeyStoreEntry = Pair @@ -189,7 +191,7 @@ private fun newKeyStore(type: String, password: String): KeyStore { return keyStore } -fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: SSLConfiguration, clientSslOptions: SSLConfiguration) -> Unit) { +fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: MutualSslConfiguration, clientSslOptions: MutualSslConfiguration) -> Unit) { val serverDir = Files.createTempDirectory(null) FileUtils.forceDeleteOnExit(serverDir.toFile()) 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 5b4555e547..4ddca172af 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 @@ -9,15 +9,15 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.serialization.internal.amqp.AMQP_ENABLED +import net.corda.testing.internal.stubs.CertificateStoreStubs import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair @@ -37,17 +37,17 @@ inline fun T.amqpSpecific(reason: String, function: () -> Unit loggerFor().info("Ignoring AMQP specific test, reason: $reason") } -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" - override val crlCheckSoftFail: Boolean = true +fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration { - init { - configureDevKeyAndTrustStores(legalName) - } + val certificatesDirectory = Files.createTempDirectory("certs") + val config = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + if (config.trustStore.getOptional() == null) { + loadDevCaTrustStore().copyTo(config.trustStore.get(true)) } + if (config.keyStore.getOptional() == null) { + config.keyStore.get(true).registerDevP2pCertificates(legalName) + } + return config } private val defaultRootCaName = X500Principal("CN=Corda Root CA,O=R3 Ltd,L=London,C=GB") @@ -103,17 +103,6 @@ fun BrokerRpcSslOptions.useSslRpcOverrides(): Map { ) } -fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map { - return mapOf( - "rpcSettings.adminAddress" to rpcAdminAddress.toString(), - "rpcSettings.useSsl" to "false", - "rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), - "rpcSettings.ssl.keyStorePassword" to keyStorePassword, - "rpcSettings.ssl.trustStorePassword" to trustStorePassword, - "rpcSettings.ssl.crlCheckSoftFail" to true - ) -} - /** * Until we have proper handling of multiple identities per node, for tests we use the first identity as special one. * TODO: Should be removed after multiple identities are introduced. @@ -127,19 +116,12 @@ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe */ fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party -fun createNodeSslConfig(path: Path, name: CordaX500Name = CordaX500Name("MegaCorp", "London", "GB")): SSLConfiguration { - val sslConfig = object : SSLConfiguration { - override val crlCheckSoftFail = true - override val certificatesDirectory = path - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - } +fun p2pSslOptions(path: Path, name: CordaX500Name = CordaX500Name("MegaCorp", "London", "GB")): MutualSslConfiguration { + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(path, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() - sslConfig.createDevKeyStores(name, rootCa.certificate, intermediateCa) - val trustStore = loadOrCreateKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate) - trustStore.save(sslConfig.trustStoreFile, sslConfig.trustStorePassword) - + sslConfig.keyStore.get(true).registerDevP2pCertificates(name, rootCa.certificate, intermediateCa) + val trustStore = sslConfig.trustStore.get(true) + trustStore[X509Utilities.CORDA_ROOT_CA] = rootCa.certificate return sslConfig } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt new file mode 100644 index 0000000000..1eba7b9aa5 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -0,0 +1,101 @@ +package net.corda.testing.internal.stubs + +import net.corda.core.internal.div +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import java.nio.file.Path + +class CertificateStoreStubs { + + companion object { + + const val DEFAULT_CERTIFICATES_DIRECTORY_NAME = "certificates" + + @JvmStatic + fun withStoreAt(certificateStorePath: Path, password: String): FileBasedCertificateStoreSupplier = FileBasedCertificateStoreSupplier(certificateStorePath, password) + } + + class Signing { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "nodekeystore.jks" + const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + + class P2P { + + companion object { + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + + val keyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / keyStoreFileName, keyStorePassword) + val trustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / trustStoreFileName, trustStorePassword) + return SslConfiguration.mutual(keyStore, trustStore) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + + return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStorePassword, trustStorePassword, keyStoreFileName, trustStoreFileName) + } + } + + class KeyStore { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "sslkeystore.jks" + const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + + class TrustStore { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "truststore.jks" + const val DEFAULT_STORE_PASSWORD = "trustpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + } +} \ No newline at end of file diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt index 8d382d2604..623fcd5670 100644 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -131,7 +131,7 @@ class InteractiveShellIntegrationTest { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode().getOrThrow().use { node -> val conf = (node as NodeHandleInternal).configuration.toShellConfig() - InteractiveShell.startShellInternal(conf) + InteractiveShell.startShell(conf) assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java) } } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index ed1b5e94cd..9b3f926c97 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -9,7 +9,6 @@ import net.corda.client.jackson.StringToMethodCallParser import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.PermissionException -import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture @@ -93,24 +92,6 @@ object InteractiveShell { _startShell(configuration, classLoader) } - /** - * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node - * internals. - */ - fun startShellInternal(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { - rpcOps = { username: String, credentials: String -> - val client = createCordaRPCClientWithInternalSslAndClassLoader(hostAndPort = configuration.hostAndPort, - configuration = CordaRPCClientConfiguration.DEFAULT.copy( - maxReconnectAttempts = 1 - ), - sslConfiguration = configuration.nodeSslConfig, - classLoader = classLoader) - this.connection = client.start(username, credentials) - connection.proxy - } - _startShell(configuration, classLoader) - } - private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { shellConfiguration = configuration InteractiveShell.classLoader = classLoader diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt index 90d5f8407c..4877b520cc 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt @@ -2,7 +2,6 @@ package net.corda.tools.shell import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration import java.nio.file.Path data class ShellConfiguration( @@ -12,7 +11,6 @@ data class ShellConfiguration( var password: String = "", val hostAndPort: NetworkHostAndPort, val ssl: ClientRpcSslOptions? = null, - val nodeSslConfig: SSLConfiguration? = null, val sshdPort: Int? = null, val sshHostKeyDirectory: Path? = null, val noLocalShell: Boolean = false) { diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 5ba08b691d..69569e8dbf 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -1,8 +1,8 @@ package net.corda.webserver import com.typesafe.config.Config +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.getValue import net.corda.nodeapi.internal.config.parseAs @@ -11,10 +11,13 @@ import java.nio.file.Path /** * [baseDirectory] is not retrieved from the config file but rather from a command line argument. */ -class WebServerConfig(override val baseDirectory: Path, val config: Config) : NodeSSLConfiguration { - override val keyStorePassword: String by config - override val trustStorePassword: String by config - override val crlCheckSoftFail: Boolean by config +class WebServerConfig(val baseDirectory: Path, val config: Config) { + + val keyStorePath: String by config + val keyStorePassword: String by config + val trustStorePath: String by config + val trustStorePassword: String by config + val useHTTPS: Boolean by config val myLegalName: String by config val rpcAddress: NetworkHostAndPort by lazy { diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 5bf16e4a7a..9ca4e03546 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -66,10 +66,10 @@ class NodeWebServer(val config: WebServerConfig) { httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.addCustomizer(SecureRequestCustomizer()) val sslContextFactory = SslContextFactory() - sslContextFactory.keyStorePath = config.sslKeystore.toString() + sslContextFactory.keyStorePath = config.keyStorePath sslContextFactory.setKeyStorePassword(config.keyStorePassword) sslContextFactory.setKeyManagerPassword(config.keyStorePassword) - sslContextFactory.setTrustStorePath(config.trustStoreFile.toString()) + sslContextFactory.setTrustStorePath(config.trustStorePath) sslContextFactory.setTrustStorePassword(config.trustStorePassword) sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1") sslContextFactory.setIncludeProtocols("TLSv1.2") From a28fa69865e5080ead23e1c3eb3b4173eb0a16e7 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 3 Sep 2018 18:21:58 +0100 Subject: [PATCH 027/119] Updated repository lists to reduce dependency on Jitpack and removed unused repositories. --- build.gradle | 7 +------ docs/source/example-code/build.gradle | 9 --------- experimental/build.gradle | 9 --------- tools/explorer/build.gradle | 4 ---- tools/explorer/capsule/build.gradle | 9 --------- 5 files changed, 1 insertion(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index 669311b082..b5e034bfaf 100644 --- a/build.gradle +++ b/build.gradle @@ -210,6 +210,7 @@ allprojects { mavenLocal() mavenCentral() jcenter() + maven { url "$artifactory_contextUrl/corda-dependencies" } maven { url 'https://jitpack.io' } } @@ -258,12 +259,6 @@ allprojects { if (!JavaVersion.current().java8Compatible) throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion") -repositories { - mavenLocal() - mavenCentral() - jcenter() -} - // Required for building out the fat JAR. dependencies { compile project(':node') diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 9a2d0ed5a4..eeeb489eb4 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -3,15 +3,6 @@ apply plugin: 'application' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'net.corda.plugins.quasar-utils' -repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } -} - configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime diff --git a/experimental/build.gradle b/experimental/build.gradle index 2aa7d36615..1be6ecc6c5 100644 --- a/experimental/build.gradle +++ b/experimental/build.gradle @@ -3,15 +3,6 @@ version '1.0-SNAPSHOT' apply plugin: 'kotlin' -repositories { - mavenLocal() - mavenCentral() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } - jcenter() -} - compileKotlin { kotlinOptions.suppressWarnings = true } diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index 065625a789..05517cc7ca 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -1,7 +1,3 @@ -repositories { - mavenCentral() -} - apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'application' diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index aba8ca33ff..757c9b503a 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -11,15 +11,6 @@ configurations { runtimeArtifacts.extendsFrom runtime } -repositories { - mavenLocal() - mavenCentral() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } - jcenter() -} - // Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards // will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get // a sensible error message telling you what to do rather than a bytecode version exception that doesn't. From a7f9320985002a6a6f9259780189072c7f6fcd60 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 4 Sep 2018 17:31:42 +0100 Subject: [PATCH 028/119] Fixedd a mistake in CertificateStoreStubs. (#3893) --- .../net/corda/testing/internal/stubs/CertificateStoreStubs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt index 1eba7b9aa5..62f871ca8c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -52,7 +52,7 @@ class CertificateStoreStubs { @JvmStatic fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { - return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStorePassword, trustStorePassword, keyStoreFileName, trustStoreFileName) + return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, trustStoreFileName, trustStorePassword) } } From 5b255c81a894a3dc32e279442d3478db02981585 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 5 Sep 2018 10:12:48 +0100 Subject: [PATCH 029/119] ENT-1906: Trivial tweaks to DJVM code. (#3890) * Trivial tweaks to DJVM code. - Use ASM Type.getInternalName() - Use @JvmDefault annotation - Declare test base class as abstract - Ensure test Log4J configuration has precedence - Replace assert() with require() - Replace simple lambdas with function references * Publish corda-djvm artifact. * Migrate Utilities class into the CLI tool. * Configure unit tests for console logging. --- build.gradle | 1 + djvm/build.gradle | 22 ++-- .../net/corda/djvm/tools/cli/BuildCommand.kt | 3 - .../net/corda/djvm/tools/cli/ClassCommand.kt | 3 - .../corda/djvm/tools/cli/InspectionCommand.kt | 1 - .../net/corda/djvm/tools/cli/NewCommand.kt | 4 - .../net/corda/djvm/tools/cli/TreeCommand.kt | 1 - .../net/corda/djvm/tools/cli/Utilities.kt | 104 ++++++++++++++++ .../kotlin/net/corda/djvm/code/Emitter.kt | 1 + .../djvm/references/MemberInformation.kt | 1 + .../net/corda/djvm/rewiring/LoadedClass.kt | 4 +- .../corda/djvm/rewiring/SandboxClassWriter.kt | 3 +- .../corda/djvm/source/SourceClassLoader.kt | 2 +- .../kotlin/net/corda/djvm/tools/Utilities.kt | 114 ------------------ .../net/corda/djvm/utilities/Discovery.kt | 3 +- .../kotlin/foo/bar/sandbox/StrictFloat.kt | 2 +- .../test/kotlin/net/corda/djvm/TestBase.kt | 8 +- .../djvm/execution/SandboxExecutorTest.kt | 2 +- .../corda/djvm/references/MemberModuleTest.kt | 3 +- .../djvm/rules/ReferenceExtractorTest.kt | 5 +- djvm/src/test/resources/log4j2-test.xml | 17 +++ djvm/src/test/resources/log4j2.xml | 39 ------ 22 files changed, 158 insertions(+), 185 deletions(-) create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt delete mode 100644 djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt create mode 100644 djvm/src/test/resources/log4j2-test.xml delete mode 100644 djvm/src/test/resources/log4j2.xml diff --git a/build.gradle b/build.gradle index b5e034bfaf..c7a1428814 100644 --- a/build.gradle +++ b/build.gradle @@ -331,6 +331,7 @@ bintrayConfig { 'corda-rpc', 'corda-core', 'corda-core-deterministic', + 'corda-djvm', 'corda', 'corda-finance', 'corda-node', diff --git a/djvm/build.gradle b/djvm/build.gradle index 270bfc2375..642601f0e4 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -1,6 +1,10 @@ plugins { id 'com.github.johnrengelman.shadow' version '2.0.4' } +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' + +description 'Corda deterministic JVM sandbox' ext { // Shaded version of ASM to avoid conflict with root project. @@ -30,8 +34,8 @@ dependencies { jar.enabled = false shadowJar { - baseName = "djvm" - classifier = "" + baseName 'corda-djvm' + classifier '' dependencies { exclude(dependency('com.jcabi:.*:.*')) exclude(dependency('org.apache.*:.*:.*')) @@ -40,10 +44,14 @@ shadowJar { exclude(dependency('io.github.lukehutch:.*:.*')) } relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' - artifacts { - shadow(tasks.shadowJar.archivePath) { - builtBy shadowJar - } - } } assemble.dependsOn shadowJar + +artifacts { + publish shadowJar +} + +publish { + disableDefaultJar true + name shadowJar.baseName +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt index 298ebcb1ce..7fafd5d743 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt @@ -1,8 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFileNames -import net.corda.djvm.tools.Utilities.jarPath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt index e3538535d0..8f5f1cb911 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt @@ -7,9 +7,6 @@ import net.corda.djvm.execution.* import net.corda.djvm.references.ClassModule import net.corda.djvm.source.ClassSource import net.corda.djvm.source.SourceClassLoader -import net.corda.djvm.tools.Utilities.find -import net.corda.djvm.tools.Utilities.onEmpty -import net.corda.djvm.tools.Utilities.userClassPath import net.corda.djvm.utilities.Discovery import djvm.org.objectweb.asm.ClassReader import picocli.CommandLine.Option diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt index f6d779ceb2..32ce08ec6e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt @@ -1,7 +1,6 @@ package net.corda.djvm.tools.cli import net.corda.djvm.source.ClassSource -import net.corda.djvm.tools.Utilities.createCodePath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt index e2df2f4d0f..8e6302de4e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt @@ -1,9 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.baseName -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFiles -import net.corda.djvm.tools.Utilities.openOptions import picocli.CommandLine.* import java.nio.file.Files import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt index 47b24ffa44..26b5dbc7d8 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt @@ -1,6 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.workingDirectory import picocli.CommandLine.Command import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt new file mode 100644 index 0000000000..66d5d4d918 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt @@ -0,0 +1,104 @@ +@file:JvmName("Utilities") +package net.corda.djvm.tools.cli + +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import java.lang.reflect.Modifier +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +/** + * Get the expanded file name of each path in the provided array. + */ +fun Array?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { + val pathString = it.toString() + val path = map(it) + when { + '/' in pathString || '\\' in pathString -> + throw Exception("Please provide a pathless file name") + pathString.endsWith(".java", true) -> path + else -> Paths.get("$path.java") + } +} + +/** + * Get the string representation of each expanded file name in the provided array. + */ +fun Array?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { + it.toString() +}.toTypedArray() + +/** + * Execute inlined action if the collection is empty. + */ +inline fun List.onEmpty(action: () -> Unit): List { + if (!this.any()) { + action() + } + return this +} + +/** + * Execute inlined action if the array is empty. + */ +inline fun Array?.onEmpty(action: () -> Unit): Array { + return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() +} + +/** + * Derive the set of [StandardOpenOption]'s to use for a file operation. + */ +fun openOptions(force: Boolean) = if (force) { + arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) +} else { + arrayOf(StandardOpenOption.CREATE_NEW) +} + +/** + * Get the path of where any generated code will be placed. Create the directory if it does not exist. + */ +fun createCodePath(): Path { + return Paths.get("tmp", "net", "corda", "djvm").let { + Files.createDirectories(it) + } +} + +/** + * Return the base name of a file (i.e., its name without extension) + */ +val Path.baseName: String + get() = this.fileName.toString() + .replaceAfterLast('.', "") + .removeSuffix(".") + +/** + * The path of the executing JAR. + */ +val jarPath: String = object {}.javaClass.protectionDomain.codeSource.location.toURI().path + + +/** + * The path of the current working directory. + */ +val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) + +/** + * The class path for the current execution context. + */ +val userClassPath: String = System.getProperty("java.class.path") + +/** + * Get a reference of each concrete class that implements interface or class [T]. + */ +inline fun find(scanSpec: String = "net/corda/djvm"): List> { + val references = mutableListOf>() + FastClasspathScanner(scanSpec) + .matchClassesImplementing(T::class.java) { clazz -> + if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + references.add(clazz) + } + } + .scan() + return references +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index 7d953a28e1..f904d276b7 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -20,6 +20,7 @@ interface Emitter { /** * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. */ + @JvmDefault val isTracer: Boolean get() = false diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt index f890fe7c89..09ccb21836 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt @@ -12,5 +12,6 @@ interface MemberInformation { val className: String val memberName: String val signature: String + @JvmDefault val reference: String get() = "$className.$memberName:$signature" } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt index 8db11e85d1..fdbeed7161 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt @@ -1,5 +1,7 @@ package net.corda.djvm.rewiring +import org.objectweb.asm.Type + /** * A class or interface running in a Java application, together with its raw byte code representation and all references * made from within the type. @@ -16,7 +18,7 @@ class LoadedClass( * The name of the loaded type. */ val name: String - get() = type.name.replace('.', '/') + get() = Type.getInternalName(type) override fun toString(): String { return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})" diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt index 9704421cd5..55f7cde501 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt @@ -4,6 +4,7 @@ import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import org.objectweb.asm.Type /** * Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class @@ -52,7 +53,7 @@ open class SandboxClassWriter( do { clazz = clazz.superclass } while (!clazz.isAssignableFrom(class2)) - clazz.name.replace('.', '/') + Type.getInternalName(clazz) } } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 47d0544bcd..60c43ada43 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -80,7 +80,7 @@ open class SourceClassLoader( when { !Files.exists(it) -> throw FileNotFoundException("File not found; $it") Files.isDirectory(it) -> { - listOf(it.toURL()) + Files.list(it).filter { isJarFile(it) }.map { it.toURL() }.toList() + listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { it.toURL() }.toList() } Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL()) else -> throw IllegalArgumentException("Expected JAR or class file, but found $it") diff --git a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt b/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt deleted file mode 100644 index f66b14d1cd..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt +++ /dev/null @@ -1,114 +0,0 @@ -package net.corda.djvm.tools - -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner -import java.lang.reflect.Modifier -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption - -/** - * Various utility functions. - */ -@Suppress("unused") -object Utilities { - - /** - * Get the expanded file name of each path in the provided array. - */ - fun Array?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { - val pathString = it.toString() - val path = map(it) - when { - '/' in pathString || '\\' in pathString -> - throw Exception("Please provide a pathless file name") - pathString.endsWith(".java", true) -> path - else -> Paths.get("$path.java") - } - } - - /** - * Get the string representation of each expanded file name in the provided array. - */ - fun Array?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { - it.toString() - }.toTypedArray() - - /** - * Execute inlined action if the collection is empty. - */ - inline fun List.onEmpty(action: () -> Unit): List { - if (!this.any()) { - action() - } - return this - } - - /** - * Execute inlined action if the array is empty. - */ - inline fun Array?.onEmpty(action: () -> Unit): Array { - return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() - } - - /** - * Derive the set of [StandardOpenOption]'s to use for a file operation. - */ - fun openOptions(force: Boolean) = if (force) { - arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) - } else { - arrayOf(StandardOpenOption.CREATE_NEW) - } - - /** - * Get the path of where any generated code will be placed. Create the directory if it does not exist. - */ - fun createCodePath(): Path { - val root = Paths.get("tmp") - .resolve("net") - .resolve("corda") - .resolve("djvm") - Files.createDirectories(root) - return root - } - - /** - * Return the base name of a file (i.e., its name without extension) - */ - val Path.baseName: String - get() = this.fileName.toString() - .replaceAfterLast('.', "") - .removeSuffix(".") - - /** - * The path of the executing JAR. - */ - val jarPath: String = Utilities::class.java.protectionDomain.codeSource.location.toURI().path - - - /** - * The path of the current working directory. - */ - val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) - - /** - * The class path for the current execution context. - */ - val userClassPath: String = System.getProperty("java.class.path") - - /** - * Get a reference of each concrete class that implements interface or class [T]. - */ - inline fun find(scanSpec: String = "net/corda/djvm"): List> { - val references = mutableListOf>() - FastClasspathScanner(scanSpec) - .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { - references.add(clazz) - } - } - .scan() - return references - } - -} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt index 8deadf7d0b..9092e5c044 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt @@ -7,6 +7,7 @@ import java.lang.reflect.Modifier * Find and instantiate types that implement a certain interface. */ object Discovery { + const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT) /** * Get an instance of each concrete class that implements interface or class [T]. @@ -15,7 +16,7 @@ object Discovery { val instances = mutableListOf() FastClasspathScanner("net/corda/djvm") .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) { try { instances.add(clazz.newInstance()) } catch (exception: Throwable) { diff --git a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt index 0d30565a98..82f721ab9a 100644 --- a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt +++ b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt @@ -4,7 +4,7 @@ class StrictFloat : Callable { override fun call() { val d = java.lang.Double.MIN_VALUE val x = d / 2 * 2 - assert(x.toString() == "0.0") + require(x.toString() == "0.0") } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index 333516c06a..dfe50ea07c 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -18,9 +18,10 @@ import net.corda.djvm.utilities.Discovery import net.corda.djvm.validation.RuleValidator import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Type import java.lang.reflect.InvocationTargetException -open class TestBase { +abstract class TestBase { companion object { @@ -38,8 +39,7 @@ open class TestBase { /** * Get the full name of type [T]. */ - inline fun nameOf(prefix: String = "") = - "$prefix${T::class.java.name.replace('.', '/')}" + inline fun nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}" } @@ -115,7 +115,7 @@ open class TestBase { var thrownException: Throwable? = null Thread { try { - val pinnedTestClasses = pinnedClasses.map { it.name.replace('.', '/') }.toSet() + val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet() val analysisConfiguration = AnalysisConfiguration( whitelist = whitelist, additionalPinnedClasses = pinnedTestClasses, diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a677b4fd35..2b2b8e7290 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -63,7 +63,7 @@ class SandboxExecutorTest : TestBase() { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - assert(hash1 == hash2) + require(hash1 == hash2) return Object().hashCode() } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt index 616a7ca7b6..71cd99cbf7 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt @@ -4,6 +4,7 @@ import net.corda.djvm.annotations.NonDeterministic import org.assertj.core.api.Assertions.assertThat import org.jetbrains.annotations.NotNull import org.junit.Test +import org.objectweb.asm.Type class MemberModuleTest { @@ -132,7 +133,7 @@ class MemberModuleTest { } private val java.lang.Class<*>.descriptor: String - get() = "L${name.replace('.', '/')};" + get() = Type.getDescriptor(this) private fun member(member: String) = MemberReference("", member, "") diff --git a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt index 3ea655c24f..bf4485caf5 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt @@ -4,6 +4,7 @@ import foo.bar.sandbox.Callable import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat import org.junit.Test +import org.objectweb.asm.Type import java.util.* class ReferenceExtractorTest : TestBase() { @@ -32,7 +33,7 @@ class ReferenceExtractorTest : TestBase() { @Test fun `can find field references`() = validate { context -> assertThat(context.references) - .hasMember(B::class.java.name.replace('.', '/'), "foo", "Ljava/lang/String;") + .hasMember(Type.getInternalName(B::class.java), "foo", "Ljava/lang/String;") } class B { @@ -47,7 +48,7 @@ class ReferenceExtractorTest : TestBase() { @Test fun `can find class references`() = validate { context -> assertThat(context.references) - .hasClass(A::class.java.name.replace('.', '/')) + .hasClass(Type.getInternalName(A::class.java)) } class C { diff --git a/djvm/src/test/resources/log4j2-test.xml b/djvm/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..b12cea5b2d --- /dev/null +++ b/djvm/src/test/resources/log4j2-test.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/djvm/src/test/resources/log4j2.xml b/djvm/src/test/resources/log4j2.xml deleted file mode 100644 index 93e84b6252..0000000000 --- a/djvm/src/test/resources/log4j2.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From ce65a58c7cf6713bd34e0af85b8267534de35546 Mon Sep 17 00:00:00 2001 From: Florian Friemel Date: Wed, 5 Sep 2018 11:12:35 +0100 Subject: [PATCH 030/119] CorDapp minimum and target version design doc. (#3798) --- docs/source/design/targetversion/design.md | 90 ++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/source/design/targetversion/design.md diff --git a/docs/source/design/targetversion/design.md b/docs/source/design/targetversion/design.md new file mode 100644 index 0000000000..a0de10d1db --- /dev/null +++ b/docs/source/design/targetversion/design.md @@ -0,0 +1,90 @@ +# CorDapp Minimum and Target Platform Version + +## Overview + +We want to give CorDapps the ability to specify which versions of the platform they support. This will make it easier for CorDapp developers to support multiple platform versions, and enable CorDapp developers to tweak behaviour and opt in to changes that might be breaking (e.g. sandboxing). Corda developers gain the ability to introduce changes to the implementation of the API that would otherwise break existing CorDapps. + +This document proposes that CorDapps will have metadata associated with them specifying a minimum platform version and a target platform Version. The minimum platform version of a CorDapp would indicate that a Corda node would have to be running at least this version of the Corda platform in order to be able to run this CorDapp. The target platform version of a CorDapp would indicate that it was tested for this version of the Corda platform. + +## Background + +> Introduce target version and min platform version as app attributes +> +> This is probably as simple as a couple of keys in a MANIFEST.MF file. +> We should document what it means, make sure API implementations can always access the target version of the calling CorDapp (i.e. by examining the flow, doing a stack walk or using Reflection.getCallerClass()) and do a simple test of an API that acts differently depending on the target version of the app. +> We should also implement checking at CorDapp load time that min platform version <= current platform version. + +([from CORDA-470](https://r3-cev.atlassian.net/browse/CORDA-470)) + +### Definitions + +* *Platform version (Corda)* An integer representing the API version of the Corda platform + +> It starts at 1 and will increment by exactly 1 for each release which changes any of the publicly exposed APIs in the entire platform. This includes public APIs on the node itself, the RPC system, messaging, serialisation, etc. API backwards compatibility will always be maintained, with the use of deprecation to migrate away from old APIs. In rare situations APIs may have to be removed, for example due to security issues. There is no relationship between the Platform Version and the release version - a change in the major, minor or patch values may or may not increase the Platform Version. + +([from the docs](https://docs.corda.net/head/versioning.html#versioning)). + +* *Platform version (Node)* The value of the Corda platform version that a node is running and advertising to the network. + +* *Minimum platform version (Network)* The minimum platform version that the nodes must run in order to be able to join the network. Set by the network zone operator. The minimum platform version is distributed with the network parameters as `minimumPlatformVersion`. + ([see docs:](https://docs.corda.net/network-map.html#network-parameters)) + +* *Target platform version (CorDapp)* Introduced in this document. Indicates that a CorDapp was tested with this version of the Corda Platform and should be run at this API level if possible. + +* *Minimum platform version (CorDapp)* Introduced in this document. Indicates the minimum version of the Corda platform that a Corda Node has to run in order to be able to run a CorDapp. + + +## Goals + +Define the semantics of target platform version and minimum platform version attributes for CorDapps, and the minimum platform version for the Corda network. Describe how target and platform versions would be specified by CorDapp developers. Define how these values can be accessed by the node and the CorDapp itself. + +## Non-goals + +In the future it might make sense to integrate the minimum and target versions into a Corda gradle plugin. Such a plugin is out of scope of this document. + +## Timeline + +This is intended as a long-term solution. The first iteration of the implementation will be part of platform version 4 and contain the minimum and target platform version. + +## Requirements + +* The CorDapp's minimum and target platform version must be accessible to nodes at CorDapp load time. + +* At CorDapp load time there should be a check that the node's platform version is greater or equal to the CorDapp's Minimum Platform version. + +* API implementations must be able to access the target version of the calling CorDapp. + +* The node's platform version must be accessible to CorDapps. + +* The CorDapp's target platform version must be accessible to the node when running CorDapps. + +## Design + +### Testing + +When a new platform version is released, CorDapp developers can increase their CorDapp's target version and re-test their app. If the tests are successful, they can then release their CorDapp with the increased target version. This way they would opt-in to potentially breaking changes that were introduced in that version. If they choose to keep their current target version, their CorDapp will continue to work. + +### Implications for platform developers + +When new features or changes are introduced that require all nodes on the network to understand them (e.g. changes in the wire transaction format), they must be version-gated on the network level. This means that the new behaviour should only take effect if the minimum platform version of the network is equal to or greater than the version in which these changes were introduced. Failing that, the old behaviour must be used instead. + +Changes that risk breaking apps must be gated on targetVersion>=X where X is the version where the change was made, and the old behaviour must be preserved if that condition isn't met. + +## Technical Design + +The minimum- and target platform version will be written to the manifest of the CorDapp's JAR, in fields called `Min-Platform-Version` and `Target-Platform-Version`. +The node's CorDapp loader reads these values from the manifest when loading the CorDapp. If the CorDapp's minimum platform version is greater than the node's platform version, the node will not load the CorDapp and log a warning. The CorDapp loader sets the minimum and target version in `net.corda.core.cordapp.Cordapp`, which can be obtained via the `CorDappContext` from the service hub. + +To make APIs caller-sensitive in cases where the service hub is not available a different approach has to be used. It would possible to do a stack walk, and parse the manifest of each class on the stack to determine if it belongs to a CorDapp, and if yes, what its target version is. Alternatively, the mapping of classes to `Cordapp`s obtained by the CorDapp loader could be stored in a global singleton. This singleton would expose a lambda returning the current CorDapp's version information (e.g. `() -> Cordapp.Info`). + +Let's assume that we want to change `TimeWindow.Between` to make it inclusive, i.e. change `contains(instant: Instant) = instant >= fromTime && instant < untilTime` to `contains(instant: Instant) = instant >= fromTime && instant <= untilTime`. However, doing so will break existing CorDapps. We could then version-guard the change such that the new behaviour is only used if the target version of the CorDapp calling `contains` is equal to or greater than the platform version that contains this change. It would look similar to this: + + ``` + fun contains(instant: Instant) { + if (CorDappVersionResolver.resolve().targetVersion > 42) { + return instant >= fromTime && instant <= untilTime + } else { + return instant >= fromTime && instant < untilTime + } + ``` +Version-gating API changes when the service hub is available would look similar to the above example, in that case the service hub's CorDapp provider would be used to determine if this code is being called from a CorDapp and to obtain its target version information. From be45096082bd4fbd1ef5c5ab4cf1c41757552856 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 5 Sep 2018 11:17:13 +0100 Subject: [PATCH 031/119] CORDA-1864: Added getter for network parameters to RPC (#3892) --- .../src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt | 6 +++++- docs/source/changelog.rst | 2 ++ .../main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 8345b153f8..edffb1ad21 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -76,7 +76,8 @@ sealed class StateMachineUpdate { // DOCSTART 1 /** * Data class containing information about the scheduled network parameters update. The info is emitted every time node - * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] and [CordaRPCOps.acceptNewNetworkParameters]. + * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] + * and [CordaRPCOps.acceptNewNetworkParameters]. * @property hash new [NetworkParameters] hash * @property parameters new [NetworkParameters] data structure * @property description description of the update @@ -227,6 +228,9 @@ interface CordaRPCOps : RPCOps { @RPCReturnsObservables fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> + /** Returns the network parameters the node is operating under. */ + val networkParameters: NetworkParameters + /** * Returns [DataFeed] object containing information on currently scheduled parameters update (null if none are currently scheduled) * and observable with future update events. Any update that occurs before the deadline automatically cancels the current one. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e44cd3eac6..48ab176c8d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,8 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Getter added to ``CordaRPCOps`` for the node's network parameters. + * The RPC client library now checks at startup whether the server is of the client libraries major version or higher. Therefore to connect to a Corda 4 node you must use version 4 or lower of the library. This behaviour can be overridden by specifying a lower number in the ``CordaRPCClientConfiguration`` class. diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 283a1703dc..014b72e494 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -19,6 +19,7 @@ import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.STRUCTURAL_STEP_PREFIX import net.corda.core.internal.sign import net.corda.core.messaging.* +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache @@ -61,6 +62,8 @@ internal class CordaRPCOpsImpl( return snapshot } + override val networkParameters: NetworkParameters get() = services.networkParameters + override fun networkParametersFeed(): DataFeed { return services.networkMapUpdater.trackParametersUpdate() } From 7f3bcbe7c391f2707d71832796a391dc351def26 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 5 Sep 2018 12:14:35 +0100 Subject: [PATCH 032/119] Tidy up DemoBench's fallback logging configuration for JUL. (#3894) --- .../java/net/corda/demobench/config/LoggingConfig.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java index 55e8369df1..181ea09d7c 100644 --- a/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java +++ b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java @@ -10,6 +10,7 @@ import java.util.logging.*; * to be added to the JVM's command line. */ public class LoggingConfig { + private static final String LOGGING_CONFIG = "logging.properties"; public LoggingConfig() throws IOException { try (InputStream input = getLoggingProperties()) { @@ -20,10 +21,11 @@ public class LoggingConfig { private static InputStream getLoggingProperties() throws IOException { ClassLoader classLoader = LoggingConfig.class.getClassLoader(); - InputStream input = classLoader.getResourceAsStream("logging.properties"); + InputStream input = classLoader.getResourceAsStream(LOGGING_CONFIG); if (input == null) { - Path javaHome = Paths.get(System.getProperty("java.home")); - input = Files.newInputStream(javaHome.resolve("lib").resolve("logging.properties")); + // Use the default JUL logging configuration properties instead. + Path logging = Paths.get(System.getProperty("java.home"), "lib", LOGGING_CONFIG); + input = Files.newInputStream(logging, StandardOpenOption.READ); } return input; } From f2784197c71502989ca108d1b19444180fbea216 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 5 Sep 2018 13:22:56 +0200 Subject: [PATCH 033/119] Minor: fix old warning in Corda 1.0 docs, don't recommend use of Jigsaw yet. --- docs/source/corda-api.rst | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index 9e79e9d8a7..84c243173b 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -26,21 +26,8 @@ Before reading this page, you should be familiar with the :doc:`key concepts of Internal APIs and stability guarantees -------------------------------------- -.. warning:: For Corda 1.0 we do not currently provide a stable wire protocol or support for database upgrades. - Additionally, the JSON format produced by the client-jackson module may change in future. - Therefore, you should not expect to be able to migrate persisted data from 1.0 to future versions. - - Additionally, it may be necessary to recompile applications against future versions of the API until we begin offering - ABI stability as well. We plan to do this soon after the release of Corda 1.0. - - Finally, please note that the 1.0 release has not yet been security audited. You should not run it in situations - where security is required. - -Corda artifacts can be required from Java 9 Jigsaw modules. -From within a ``module-info.java``, you can reference one of the modules e.g., ``requires net.corda.core;``. - -.. warning:: while Corda artifacts can be required from ``module-info.java`` files, they are still not proper Jigsaw modules, - because they rely on the automatic module mechanism and declare no module descriptors themselves. We plan to integrate Jigsaw more thoroughly in the future. +Corda makes certain commitments about what parts of the API will preserve backwards compatibility as they change and +which will not. Over time, more of the API will fall under the stability guarantees. Corda stable modules -------------------- From 2fbeab13659933b9537d88c0d73a73e1e0ab8060 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 5 Sep 2018 13:06:05 +0100 Subject: [PATCH 034/119] Fixed compilation error for case-insensitive operating systems. (#3896) --- .../net/corda/client/rpc/CordaRPCClient.kt | 2 +- .../corda/client/rpc/internal/RPCClient.kt | 6 +- .../net/corda/nodeapi/ArtemisTcpTransport.kt | 105 ------------------ .../net/corda/nodeapi/BrokerRpcSslOptions.kt | 5 + .../internal/ArtemisMessagingClient.kt | 2 +- ...TcpTransport.kt => ArtemisTcpTransport.kt} | 2 +- .../internal/config/SslConfiguration.kt | 29 +---- .../internal/protonwrapper/netty/SSLHelper.kt | 10 +- .../net/corda/node/amqp/ProtonWrapperTests.kt | 4 +- .../node/services/rpc/ArtemisRpcTests.kt | 2 +- .../services/messaging/SimpleMQClient.kt | 2 +- .../messaging/ArtemisMessagingServer.kt | 2 +- .../messaging/InternalRPCMessagingClient.kt | 4 +- .../services/messaging/P2PMessagingClient.kt | 2 +- .../services/rpc/RpcBrokerConfiguration.kt | 4 +- .../corda/testing/node/internal/RPCDriver.kt | 8 +- 16 files changed, 31 insertions(+), 158 deletions(-) delete mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt rename node-api/src/main/kotlin/net/corda/nodeapi/internal/{InternalArtemisTcpTransport.kt => ArtemisTcpTransport.kt} (99%) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index c6b9d0556e..a003ea1c16 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -11,7 +11,7 @@ import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import java.time.Duration diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 1665aa0159..f34da0a84a 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -16,9 +16,9 @@ import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcInternalClientTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport import net.corda.nodeapi.internal.config.SslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt deleted file mode 100644 index 210f04586a..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.corda.nodeapi - -import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.InternalArtemisTcpTransport -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.acceptorFactoryClassName -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.connectorFactoryClassName -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.defaultArtemisOptions -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.defaultSSLOptions -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.config.SslConfiguration -import net.corda.nodeapi.internal.requireOnDefaultFileSystem -import org.apache.activemq.artemis.api.core.TransportConfiguration -import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants -import java.nio.file.Path - -/** Class to set Artemis TCP configuration options. */ -class ArtemisTcpTransport { - companion object { - /** - * Corda supported TLS schemes. - *

    - *
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - *
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - *
  • TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 - *

- * As shown above, current version restricts enabled TLS cipher suites to: - * AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream. - * SHA256 as message authentication algorithm. - * Ephemeral Diffie Hellman key exchange for advanced forward secrecy. ECDHE is preferred, but DHE is also - * supported in case one wants to completely avoid the use of ECC for TLS. - * ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes, - * but we allow classical RSA certificates to work in case one uses external tools or cloud providers or HSMs - * that do not support ECC certificates. - */ - val CIPHER_SUITES = listOf( - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" - ) - - /** Supported TLS versions, currently TLSv1.2 only. */ - val TLS_VERSIONS = listOf("TLSv1.2") - - private fun SSLConfiguration.toTransportOptions() = mapOf( - TransportConstants.SSL_ENABLED_PROP_NAME to true, - TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.KEYSTORE_PATH_PROP_NAME to sslKeystore, - TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, - TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStoreFile, - TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword, - TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) - - fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { - val options = defaultArtemisOptions(hostAndPort).toMutableMap() - - if (config != null && enableSSL) { - config.sslKeystore.requireOnDefaultFileSystem() - config.trustStoreFile.requireOnDefaultFileSystem() - options.putAll(defaultSSLOptions) - options.putAll(config.toTransportOptions()) - } - return TransportConfiguration(acceptorFactoryClassName, options) - } - - fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { - val options = defaultArtemisOptions(hostAndPort).toMutableMap() - - if (config != null && enableSSL) { - config.sslKeystore.requireOnDefaultFileSystem() - config.trustStoreFile.requireOnDefaultFileSystem() - options.putAll(defaultSSLOptions) - options.putAll(config.toTransportOptions()) - } - return TransportConfiguration(connectorFactoryClassName, options) - } - - /** [TransportConfiguration] for RPC TCP communication - server side. */ - fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { - return InternalArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, config, enableSSL) - } - - /** [TransportConfiguration] for RPC TCP communication - * This is the Transport that connects the client JVM to the broker. */ - fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { - return InternalArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, config, enableSSL) - } - - /** Create as list of [TransportConfiguration]. **/ - fun rpcConnectorTcpTransportsFromList(hostAndPortList: List, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List = hostAndPortList.map { - rpcConnectorTcpTransport(it, config, enableSSL) - } - - fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { - return InternalArtemisTcpTransport.rpcInternalClientTcpTransport(hostAndPort, config) - } - - fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { - return InternalArtemisTcpTransport.rpcInternalAcceptorTcpTransport(hostAndPort, config) - } - } -} - -data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt new file mode 100644 index 0000000000..2509b83617 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt @@ -0,0 +1,5 @@ +package net.corda.nodeapi + +import java.nio.file.Path + +data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index 055c96ffe6..4af839bc13 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -33,7 +33,7 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration, check(started == null) { "start can't be called twice" } log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = InternalArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config) + val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt similarity index 99% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt index 85513c9ad1..18762cf959 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/InternalArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt @@ -14,7 +14,7 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import java.nio.file.Path // This avoids internal types from leaking in the public API. The "external" ArtemisTcpTransport delegates to this internal one. -class InternalArtemisTcpTransport { +class ArtemisTcpTransport { companion object { val CIPHER_SUITES = listOf( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 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 1011d49f9d..d8349bc80e 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 @@ -1,9 +1,5 @@ package net.corda.nodeapi.internal.config -import net.corda.core.internal.div -import net.corda.nodeapi.internal.crypto.X509KeyStore -import java.nio.file.Path - interface SslConfiguration { val keyStore: FileBasedCertificateStoreSupplier? @@ -24,27 +20,4 @@ interface MutualSslConfiguration : SslConfiguration { override val trustStore: FileBasedCertificateStoreSupplier } -private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration - -// Don't use this internally. It's still here because it's used by ArtemisTcpTransport, which is in public node-api by mistake. -interface SSLConfiguration { - val keyStorePassword: String - val trustStorePassword: String - val certificatesDirectory: Path - val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" - val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" - val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" - val crlCheckSoftFail: Boolean - - fun loadTrustStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(trustStoreFile, trustStorePassword, createNew) - } - - fun loadNodeKeyStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(nodeKeystore, keyStorePassword, createNew) - } - - fun loadSslKeyStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(sslKeystore, keyStorePassword, createNew) - } -} \ No newline at end of file +private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 6ffd49cc2d..c54eed8ef5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -5,7 +5,7 @@ import net.corda.core.crypto.newSecureRandom import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHex -import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.toBc import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier @@ -110,8 +110,8 @@ internal fun createClientSslHelper(target: NetworkHostAndPort, sslContext.init(keyManagers, trustManagers, newSecureRandom()) val sslEngine = sslContext.createSSLEngine(target.host, target.port) sslEngine.useClientMode = true - sslEngine.enabledProtocols = InternalArtemisTcpTransport.TLS_VERSIONS.toTypedArray() - sslEngine.enabledCipherSuites = InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true return SslHandler(sslEngine) } @@ -125,8 +125,8 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, val sslEngine = sslContext.createSSLEngine() sslEngine.useClientMode = false sslEngine.needClientAuth = true - sslEngine.enabledProtocols = InternalArtemisTcpTransport.TLS_VERSIONS.toTypedArray() - sslEngine.enabledCipherSuites = InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray() + sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() + sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true return SslHandler(sslEngine) } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 5ef36c2372..d89481abb8 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -14,7 +14,7 @@ import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.crypto.* @@ -137,7 +137,7 @@ class ProtonWrapperTests { val serverSocketFactory = context.serverSocketFactory val serverSocket = serverSocketFactory.createServerSocket(serverPort) as SSLServerSocket - val serverParams = SSLParameters(InternalArtemisTcpTransport.CIPHER_SUITES.toTypedArray(), + val serverParams = SSLParameters(ArtemisTcpTransport.CIPHER_SUITES.toTypedArray(), arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index c64a3773f5..c1e3cc935b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -18,7 +18,7 @@ import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.core.SerializationEnvironmentRule 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 e70e77d6b5..44e59852a9 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 @@ -3,7 +3,7 @@ package net.corda.services.messaging import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.testing.internal.configureTestSSL import org.apache.activemq.artemis.api.core.client.* diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index bb46278787..2014ca46c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -17,7 +17,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_P import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pAcceptorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt index abb07763d1..a484261ba5 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt @@ -7,7 +7,7 @@ import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManager import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER -import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ServerLocator @@ -22,7 +22,7 @@ class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serv fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) { - val tcpTransport = InternalArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig) + val tcpTransport = ArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 95c340ead1..b78e971e78 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -33,7 +33,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.bridging.BridgeControl import net.corda.nodeapi.internal.bridging.BridgeEntry import net.corda.nodeapi.internal.persistence.CordaPersistence diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index f17dcee8aa..9e1ace3c2a 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -7,8 +7,8 @@ import net.corda.node.internal.artemis.SecureArtemisConfiguration import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcAcceptorTcpTransport -import net.corda.nodeapi.internal.InternalArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.core.config.CoreQueueConfiguration 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 94b38ddd2c..05b5662a83 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 @@ -22,7 +22,7 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.InternalArtemisTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE @@ -220,14 +220,14 @@ data class RPCDriverDSL( bindingsDirectory = "$artemisDir/bindings" journalDirectory = "$artemisDir/journal" largeMessagesDirectory = "$artemisDir/large-messages" - acceptorConfigurations = setOf(InternalArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null)) + acceptorConfigurations = setOf(ArtemisTcpTransport.rpcAcceptorTcpTransport(hostAndPort, null)) configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { - return InternalArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, null) + return ArtemisTcpTransport.rpcConnectorTcpTransport(hostAndPort, null) } } @@ -339,7 +339,7 @@ data class RPCDriverDSL( configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT ): CordaFuture { return driverDSL.executorService.fork { - val client = RPCClient(InternalArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration) + val client = RPCClient(ArtemisTcpTransport.rpcConnectorTcpTransport(rpcAddress, null), configuration) val connection = client.start(rpcOpsClass, username, password, externalTrace) driverDSL.shutdownManager.registerShutdown { connection.close() From 541a0d7d5b8f7f6e85a9ae8ae829796fcebe6623 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 5 Sep 2018 14:05:52 +0100 Subject: [PATCH 035/119] ENT-1906: Remove SLF4J's JUL logging bridge from DJVM. (#3895) * Remove SLF4J's JUL logging bridge from DJVM. * Refactor SLF4J's Log4J back end into the CLI. --- djvm/build.gradle | 7 ++----- djvm/cli/build.gradle | 1 - djvm/{ => cli}/src/main/resources/log4j2.xml | 0 3 files changed, 2 insertions(+), 6 deletions(-) rename djvm/{ => cli}/src/main/resources/log4j2.xml (100%) diff --git a/djvm/build.gradle b/djvm/build.gradle index 642601f0e4..6b1c255d6c 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -14,9 +14,7 @@ ext { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" - compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + compile "org.slf4j:slf4j-api:$slf4j_version" // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" @@ -29,6 +27,7 @@ dependencies { // Test utilities testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:$assertj_version" + testCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" } jar.enabled = false @@ -37,8 +36,6 @@ shadowJar { baseName 'corda-djvm' classifier '' dependencies { - exclude(dependency('com.jcabi:.*:.*')) - exclude(dependency('org.apache.*:.*:.*')) exclude(dependency('org.jetbrains.*:.*:.*')) exclude(dependency('org.slf4j:.*:.*')) exclude(dependency('io.github.lukehutch:.*:.*')) diff --git a/djvm/cli/build.gradle b/djvm/cli/build.gradle index a3543bcc54..617ea9fc12 100644 --- a/djvm/cli/build.gradle +++ b/djvm/cli/build.gradle @@ -15,7 +15,6 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" diff --git a/djvm/src/main/resources/log4j2.xml b/djvm/cli/src/main/resources/log4j2.xml similarity index 100% rename from djvm/src/main/resources/log4j2.xml rename to djvm/cli/src/main/resources/log4j2.xml From 9373c0fb8eeab7c8d18236721a525f4cea78eb85 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 5 Sep 2018 14:10:37 +0100 Subject: [PATCH 036/119] Removed the superfluous NetworkMapCacheImpl class (#3882) NetworkMapCacheImpl and NetworkMapCacheBaseInternal interface merged into PersistentNetworkMapCache and NetworkMapCacheInternal interface respectively. --- .../messaging/ArtemisMessagingTest.kt | 6 +-- .../network/PersistentNetworkMapCacheTest.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 15 +++--- .../node/services/api/ServiceHubInternal.kt | 4 +- .../services/network/NetworkMapCacheImpl.kt | 51 ------------------- .../network/PersistentNetworkMapCache.kt | 41 ++++++++++++++- 6 files changed, 53 insertions(+), 68 deletions(-) delete mode 100644 node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 4f3e80594b..c2d8de19d2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -10,7 +10,6 @@ import net.corda.node.internal.configureDatabase import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor @@ -66,7 +65,7 @@ class ArtemisMessagingTest { private var messagingClient: P2PMessagingClient? = null private var messagingServer: ArtemisMessagingServer? = null - private lateinit var networkMapCache: NetworkMapCacheImpl + private lateinit var networkMapCache: PersistentNetworkMapCache @Before fun setUp() { @@ -89,8 +88,7 @@ class ArtemisMessagingTest { } LogHelper.setLevel(PersistentUniquenessProvider::class) database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }) - val persistentNetworkMapCache = PersistentNetworkMapCache(database, ALICE_NAME).apply { start(emptyList()) } - networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, rigorousMock(), database).apply { start() } + networkMapCache = PersistentNetworkMapCache(database, rigorousMock(), ALICE_NAME).apply { start(emptyList()) } } @After diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 1cc3ff5539..7033b3efa9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -5,6 +5,8 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.configureDatabase import net.corda.node.internal.schemas.NodeInfoSchemaV1 +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -27,7 +29,7 @@ class PersistentNetworkMapCacheTest { private var portCounter = 1000 private val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }) - private val charlieNetMapCache = PersistentNetworkMapCache(database, CHARLIE.name) + private val charlieNetMapCache = PersistentNetworkMapCache(database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate), CHARLIE.name) @After fun cleanUp() { 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 1f99dc9bf1..a43cb8d8ba 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -53,7 +53,10 @@ import net.corda.node.services.keys.KeyManagementServiceInternal import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.network.* +import net.corda.node.services.network.NetworkMapClient +import net.corda.node.services.network.NetworkMapUpdater +import net.corda.node.services.network.NodeInfoWatcher +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.* import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.* @@ -142,8 +145,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // TODO Break cyclic dependency identityService.database = database } - private val persistentNetworkMapCache = PersistentNetworkMapCache(database, configuration.myLegalName) - val networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, identityService, database).tokenize() + val networkMapCache = PersistentNetworkMapCache(database, identityService, configuration.myLegalName).tokenize() val checkpointStorage = DBCheckpointStorage() @Suppress("LeakingThis") val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() @@ -253,7 +255,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) return database.use { it.transaction { - persistentNetworkMapCache.start(notaries = emptyList()) val (_, nodeInfoAndSigned) = updateNodeInfo(identity, identityKeyPair, publish = false) nodeInfoAndSigned.nodeInfo } @@ -265,8 +266,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.info("Starting clearing of network map cache entries...") startDatabase() database.use { - persistentNetworkMapCache.start(notaries = emptyList()) - persistentNetworkMapCache.clearNetworkMapCache() + networkMapCache.clearNetworkMapCache() } } @@ -307,8 +307,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction { - persistentNetworkMapCache.start(netParams.notaries) - networkMapCache.start() + networkMapCache.start(netParams.notaries) updateNodeInfo(identity, identityKeyPair, publish = true) } 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 e8b7aa8d15..cb555fb671 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 @@ -25,9 +25,9 @@ import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence +import java.security.PublicKey -interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBaseInternal -interface NetworkMapCacheBaseInternal : NetworkMapCacheBase { +interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { val allNodeHashes: List fun getNodeByHash(nodeHash: SecureHash): NodeInfo? diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt deleted file mode 100644 index c31f089a07..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.corda.node.services.network - -import net.corda.core.identity.AbstractParty -import net.corda.core.node.NodeInfo -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkMapCache -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.contextLogger -import net.corda.node.services.api.NetworkMapCacheBaseInternal -import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.nodeapi.internal.persistence.CordaPersistence - -class NetworkMapCacheImpl( - private val networkMapCacheBase: NetworkMapCacheBaseInternal, - private val identityService: IdentityService, - private val database: CordaPersistence -) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal, SingletonSerializeAsToken() { - companion object { - private val logger = contextLogger() - } - - fun start() { - for (nodeInfo in networkMapCacheBase.allNodes) { - for (identity in nodeInfo.legalIdentitiesAndCerts) { - identityService.verifyAndRegisterIdentity(identity) - } - } - networkMapCacheBase.changed.subscribe { mapChange -> - // TODO how should we handle network map removal - if (mapChange is NetworkMapCache.MapChange.Added) { - mapChange.node.legalIdentitiesAndCerts.forEach { - try { - identityService.verifyAndRegisterIdentity(it) - } catch (ignore: Exception) { - // Log a warning to indicate node info is not added to the network map cache. - logger.warn("Node info for :'${it.name}' is not added to the network map due to verification error.") - } - } - } - } - } - - override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { - return database.transaction { - val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party) - wellKnownParty?.let { - getNodesByLegalIdentityKey(it.owningKey).firstOrNull() - } - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index c2fbf40dac..75943d7704 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -3,6 +3,7 @@ package net.corda.node.services.network import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toStringShort +import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -11,15 +12,17 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.NotaryInfo +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.internal.schemas.NodeInfoSchemaV1 -import net.corda.node.services.api.NetworkMapCacheBaseInternal +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NonInvalidatingCache import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit @@ -34,7 +37,8 @@ import javax.annotation.concurrent.ThreadSafe /** Database-based network map cache. */ @ThreadSafe open class PersistentNetworkMapCache(private val database: CordaPersistence, - private val myLegalName: CordaX500Name) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { + private val identityService: IdentityService, + private val myLegalName: CordaX500Name) : NetworkMapCacheInternal, SingletonSerializeAsToken() { companion object { private val logger = contextLogger() } @@ -78,6 +82,15 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, } } + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + return database.transaction { + val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party) + wellKnownParty?.let { + getNodesByLegalIdentityKey(it.owningKey).firstOrNull() + } + } + } + override fun getNodeByHash(nodeHash: SecureHash): NodeInfo? { return database.transaction { val builder = session.criteriaBuilder @@ -160,6 +173,7 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, val previousNode = getNodesByLegalIdentityKey(node.legalIdentities.first().owningKey).firstOrNull() if (previousNode == null) { logger.info("No previous node found") + if (!verifyAndRegisterIdentities(node)) return database.transaction { updateInfoDB(node, session) changePublisher.onNext(MapChange.Added(node)) @@ -169,6 +183,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, return } else if (previousNode != node) { logger.info("Previous node was found as: $previousNode") + // TODO We should be adding any new identities as well + if (!verifyIdentities(node)) return database.transaction { updateInfoDB(node, session) changePublisher.onNext(MapChange.Modified(node, previousNode)) @@ -183,6 +199,27 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, logger.debug { "Done adding node with info: $node" } } + private fun verifyIdentities(node: NodeInfo): Boolean { + val failures = node.legalIdentitiesAndCerts.mapNotNull { Try.on { it.verify(identityService.trustAnchor) } as? Try.Failure } + if (failures.isNotEmpty()) { + logger.warn("$node has ${failures.size} invalid identities:") + failures.forEach { logger.warn("", it) } + } + return failures.isEmpty() + } + + private fun verifyAndRegisterIdentities(node: NodeInfo): Boolean { + // First verify all the node's identities are valid before registering any of them + return if (verifyIdentities(node)) { + for (identity in node.legalIdentitiesAndCerts) { + identityService.verifyAndRegisterIdentity(identity) + } + true + } else { + false + } + } + override fun removeNode(node: NodeInfo) { logger.info("Removing node with info: $node") synchronized(_changed) { From e3ece00bea86fcb33a97b40f647535d3bd24a376 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 5 Sep 2018 15:57:12 +0100 Subject: [PATCH 037/119] ENT-1906: Publish DJVM artifact with its dependency information. (#3897) Also migrate enclavelet code into testing:common jar. --- .../corda/deterministic/common/Enclavelet.kt | 21 ++++++++++++++++ .../deterministic/txverify/EnclaveletTest.kt | 25 +------------------ djvm/build.gradle | 18 ++++++------- djvm/cli/build.gradle | 1 - 4 files changed, 31 insertions(+), 34 deletions(-) create mode 100644 core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/Enclavelet.kt diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/Enclavelet.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/Enclavelet.kt new file mode 100644 index 0000000000..720a2bc4ff --- /dev/null +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/Enclavelet.kt @@ -0,0 +1,21 @@ +@file:JvmName("Enclavelet") +package net.corda.deterministic.common + +import net.corda.core.serialization.deserialize +import net.corda.core.transactions.LedgerTransaction + +/** + * We assume the signatures were already checked outside the sandbox: the purpose of this code + * is simply to check the sensitive, app-specific parts of a transaction. + * + * TODO: Transaction data is meant to be encrypted under an enclave-private key. + */ +@Throws(Exception::class) +fun verifyInEnclave(reqBytes: ByteArray) { + deserialize(reqBytes).verify() +} + +private fun deserialize(reqBytes: ByteArray): LedgerTransaction { + return reqBytes.deserialize() + .toLedgerTransaction() +} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt index 88125a4072..84bab1d1b7 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt @@ -1,11 +1,8 @@ -@file:JvmName("Enclavelet") package net.corda.deterministic.txverify -import net.corda.core.serialization.deserialize -import net.corda.core.transactions.LedgerTransaction import net.corda.deterministic.bytesOfResource import net.corda.deterministic.common.LocalSerializationRule -import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.deterministic.common.verifyInEnclave import net.corda.finance.contracts.asset.Cash.Commands.* import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule @@ -30,23 +27,3 @@ class EnclaveletTest { assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") } } - -/** - * Returns either null to indicate success when the transactions are validated, or a string with the - * contents of the error. Invoked via JNI in response to an enclave RPC. The argument is a serialised - * [TransactionVerificationRequest]. - * - * Note that it is assumed the signatures were already checked outside the sandbox: the purpose of this code - * is simply to check the sensitive, app specific parts of a transaction. - * - * TODO: Transaction data is meant to be encrypted under an enclave-private key. - */ -@Throws(Exception::class) -private fun verifyInEnclave(reqBytes: ByteArray) { - deserialize(reqBytes).verify() -} - -private fun deserialize(reqBytes: ByteArray): LedgerTransaction { - return reqBytes.deserialize() - .toLedgerTransaction() -} diff --git a/djvm/build.gradle b/djvm/build.gradle index 6b1c255d6c..b3447b63ac 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -11,10 +11,14 @@ ext { asm_version = '6.1.1' } +configurations { + testCompile.extendsFrom shadow +} + dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:slf4j-api:$slf4j_version" + shadow "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + shadow "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + shadow "org.slf4j:slf4j-api:$slf4j_version" // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" @@ -22,7 +26,7 @@ dependencies { compile "org.ow2.asm:asm-commons:$asm_version" // Classpath scanner - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + shadow "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" // Test utilities testCompile "junit:junit:$junit_version" @@ -35,11 +39,6 @@ jar.enabled = false shadowJar { baseName 'corda-djvm' classifier '' - dependencies { - exclude(dependency('org.jetbrains.*:.*:.*')) - exclude(dependency('org.slf4j:.*:.*')) - exclude(dependency('io.github.lukehutch:.*:.*')) - } relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' } assemble.dependsOn shadowJar @@ -49,6 +48,7 @@ artifacts { } publish { + dependenciesFrom configurations.shadow disableDefaultJar true name shadowJar.baseName } diff --git a/djvm/cli/build.gradle b/djvm/cli/build.gradle index 617ea9fc12..d72a4a74c0 100644 --- a/djvm/cli/build.gradle +++ b/djvm/cli/build.gradle @@ -19,7 +19,6 @@ dependencies { compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" compile "info.picocli:picocli:$picocli_version" - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" compile project(path: ":djvm", configuration: "shadow") // Deterministic runtime - used in whitelist generation From 304dba704ef8e9ac516a36cc53e399027326413b Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Wed, 5 Sep 2018 17:46:46 +0100 Subject: [PATCH 038/119] Support HA without load balancer (#3889) Allow configuration in node for additional advertised addresses. fix logic error Use empty list as default config not null Allow multiple addresses in NodeInfo Describe new additionalP2PAddresses property in docs. Add integration test of additionalP2PAddress feature Fixup after rebase Address PR comment Address PR comments by removing unused element of NodeAddress --- docs/source/corda-configuration-file.rst | 3 + .../internal/ArtemisMessagingComponent.kt | 12 ++-- .../internal/bridging/AMQPBridgeManager.kt | 66 ++++++++--------- .../bridging/BridgeControlListener.kt | 6 +- .../internal/bridging/BridgeManager.kt | 9 +-- .../net/corda/node/amqp/AMQPBridgeTest.kt | 2 +- .../messaging/AdditionP2PAddressModeTest.kt | 70 +++++++++++++++++++ .../kotlin/net/corda/node/internal/Node.kt | 12 ++-- .../node/services/config/NodeConfiguration.kt | 2 + .../services/messaging/P2PMessagingClient.kt | 14 ++-- node/src/main/resources/reference.conf | 1 + .../testing/node/internal/DriverDSLImpl.kt | 11 +-- 12 files changed, 133 insertions(+), 75 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index d21076eed4..8c6e003ee0 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -96,6 +96,9 @@ absolute path to the node's base directory. note that the host is the included as the advertised entry in the network map. As a result the value listed here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable, the node will try to auto-discover its public one. + +:additionalP2PAddresses: An array of additional host:port values, which will be included in the advertised NodeInfo in the network map in addition to the ``p2pAddress``. + Nodes can use this configuration option to advertise HA endpoints and aliases to external parties. If not specified the default value is an empty list. :flowTimeout: When a flow implementing the ``TimedFlow`` interface does not complete in time, it is restarted from the initial checkpoint. Currently only used for notarisation requests: if a notary replica dies while processing a notarisation request, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index a369aa710a..fb1034f1ba 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -6,7 +6,6 @@ import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString import java.security.PublicKey @@ -77,9 +76,7 @@ class ArtemisMessagingComponent { val queueName: String } - interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient { - val hostAndPort: NetworkHostAndPort - } + interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient /** * This is the class used to implement [SingleMessageRecipient], for now. Note that in future this class @@ -90,12 +87,11 @@ class ArtemisMessagingComponent { * an advertised service's queue. * * @param queueName The name of the queue this address is associated with. - * @param hostAndPort The address of the node. */ @CordaSerializable - data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { - constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) : - this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort) + data class NodeAddress(override val queueName: String) : ArtemisPeerAddress { + constructor(peerIdentity: PublicKey) : + this("$PEERS_PREFIX${peerIdentity.toStringShort()}") } /** diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 7be3844aec..c696999c95 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -4,16 +4,13 @@ import io.netty.channel.EventLoopGroup import io.netty.channel.nio.NioEventLoopGroup import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting -import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.ArtemisMessagingClient -import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.ArtemisSessionProvider -import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus @@ -40,7 +37,7 @@ import kotlin.concurrent.withLock class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { private val lock = ReentrantLock() - private val bridgeNameToBridgeMap = mutableMapOf() + private val queueNamesToBridgesMap = mutableMapOf>() private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore, override val trustStore: CertificateStore, @@ -66,14 +63,13 @@ class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, pri * If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery, * however Artemis and the remote Corda instanced will deduplicate these messages. */ - private class AMQPBridge(private val queueName: String, - private val target: NetworkHostAndPort, + private class AMQPBridge(val queueName: String, + val targets: List, private val legalNames: Set, private val amqpConfig: AMQPConfiguration, sharedEventGroup: EventLoopGroup, private val artemis: ArtemisSessionProvider) { companion object { - fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" private val log = contextLogger() } @@ -81,8 +77,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, pri val oldMDC = MDC.getCopyOfContextMap() try { MDC.put("queueName", queueName) - MDC.put("target", target.toString()) - MDC.put("bridgeName", bridgeName) + MDC.put("targets", targets.joinToString(separator = ";") { it.toString() }) MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() }) MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString()) block() @@ -101,8 +96,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, pri private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) } - val amqpClient = AMQPClient(listOf(target), legalNames, amqpConfig, sharedThreadPool = sharedEventGroup) - val bridgeName: String get() = getBridgeName(queueName, target) + val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup) private val lock = ReentrantLock() // lock to serialise session level access private var session: ClientSession? = null private var consumer: ClientConsumer? = null @@ -194,39 +188,37 @@ class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, pri } } - private fun gatherAddresses(node: NodeInfo): List { - return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, node.addresses[0]) } - } - - override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { - if (bridgeExists(getBridgeName(queueName, target))) { - return - } - val newBridge = AMQPBridge(queueName, target, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!) - lock.withLock { - bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge + override fun deployBridge(queueName: String, targets: List, legalNames: Set) { + val newBridge = lock.withLock { + val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() } + for (target in targets) { + if (bridges.any { it.targets.contains(target) }) { + return + } + } + val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!) + bridges += newBridge + newBridge } newBridge.start() } - override fun destroyBridges(node: NodeInfo) { + override fun destroyBridge(queueName: String, targets: List) { lock.withLock { - gatherAddresses(node).forEach { - val bridge = bridgeNameToBridgeMap.remove(getBridgeName(it.queueName, it.hostAndPort)) - bridge?.stop() + val bridges = queueNamesToBridgesMap[queueName] ?: mutableListOf() + for (target in targets) { + val bridge = bridges.firstOrNull { it.targets.contains(target) } + if (bridge != null) { + bridges -= bridge + if (bridges.isEmpty()) { + queueNamesToBridgesMap.remove(queueName) + } + bridge.stop() + } } } } - override fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort) { - lock.withLock { - val bridge = bridgeNameToBridgeMap.remove(getBridgeName(queueName, hostAndPort)) - bridge?.stop() - } - } - - override fun bridgeExists(bridgeName: String): Boolean = lock.withLock { bridgeNameToBridgeMap.containsKey(bridgeName) } - override fun start() { sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) val artemis = artemisMessageClientFactory() @@ -238,13 +230,13 @@ class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, pri override fun close() { lock.withLock { - for (bridge in bridgeNameToBridgeMap.values) { + for (bridge in queueNamesToBridgesMap.values.flatten()) { bridge.stop() } sharedEventLoopGroup?.shutdownGracefully() sharedEventLoopGroup?.terminationFuture()?.sync() sharedEventLoopGroup = null - bridgeNameToBridgeMap.clear() + queueNamesToBridgesMap.clear() artemis?.stop() } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index c9d6e23af7..74e5f4e783 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -98,7 +98,7 @@ class BridgeControlListener(val config: MutualSslConfiguration, return } for (outQueue in controlMessage.sendQueues) { - bridgeManager.deployBridge(outQueue.queueName, outQueue.targets.first(), outQueue.legalNames.toSet()) + bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet()) } validInboundQueues.addAll(controlMessage.inboxQueues) } @@ -110,14 +110,14 @@ class BridgeControlListener(val config: MutualSslConfiguration, log.error("Invalid queue names in control message $controlMessage") return } - bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first(), controlMessage.bridgeInfo.legalNames.toSet()) + bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet()) } is BridgeControl.Delete -> { if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) { log.error("Invalid queue names in control message $controlMessage") return } - bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first()) + bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt index cd7db964cf..69b1509550 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt @@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.bridging import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting -import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort /** @@ -10,13 +9,9 @@ import net.corda.core.utilities.NetworkHostAndPort */ @VisibleForTesting interface BridgeManager : AutoCloseable { - fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) + fun deployBridge(queueName: String, targets: List, legalNames: Set) - fun destroyBridges(node: NodeInfo) - - fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort) - - fun bridgeExists(bridgeName: String): Boolean + fun destroyBridge(queueName: String, targets: List) fun start() diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index 1563abb0c8..7cc77b2d96 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -193,7 +193,7 @@ class AMQPBridgeTest { if (sourceQueueName != null) { // Local queue for outgoing messages artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) - bridgeManager.deployBridge(sourceQueueName, amqpAddress, setOf(BOB.name)) + bridgeManager.deployBridge(sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) } return Triple(artemisServer, artemisClient, bridgeManager) } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt new file mode 100644 index 0000000000..e4087fe3f7 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt @@ -0,0 +1,70 @@ +package net.corda.services.messaging + +import com.typesafe.config.ConfigValueFactory +import junit.framework.TestCase.assertEquals +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.withoutIssuer +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.node.services.Permissions.Companion.all +import net.corda.testing.core.DUMMY_BANK_A_NAME +import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.core.expect +import net.corda.testing.core.expectEvents +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import java.util.* + +class AdditionP2PAddressModeTest { + private val portAllocation = PortAllocation.Incremental(27182) + @Test + fun `runs nodes with one configured to use additionalP2PAddresses`() { + val testUser = User("test", "test", setOf(all())) + driver(DriverParameters(startNodesInProcess = true, inMemoryDB = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) { + val mainAddress = portAllocation.nextHostAndPort().toString() + val altAddress = portAllocation.nextHostAndPort().toString() + val haConfig = mutableMapOf() + haConfig["detectPublicIp"] = false + haConfig["p2pAddress"] = mainAddress //advertise this as primary + haConfig["messagingServerAddress"] = altAddress // but actually host on the alternate address + haConfig["messagingServerExternal"] = false + haConfig["additionalP2PAddresses"] = ConfigValueFactory.fromIterable(listOf(altAddress)) // advertise this secondary address + + val (nodeA, nodeB) = listOf( + startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(testUser), customOverrides = haConfig), + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(testUser), customOverrides = mapOf("p2pAddress" to portAllocation.nextHostAndPort().toString())) + ).map { it.getOrThrow() } + val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { + val client = CordaRPCClient(it.rpcAddress) + client.start(testUser.username, testUser.password).proxy + } + + val nodeBVaultUpdates = nodeBRpc.vaultTrack(Cash.State::class.java).updates + + val issueRef = OpaqueBytes.of(0.toByte()) + nodeARpc.startFlowDynamic( + CashIssueAndPaymentFlow::class.java, + DOLLARS(1234), + issueRef, + nodeB.nodeInfo.legalIdentities.get(0), + true, + defaultNotaryIdentity + ).returnValue.getOrThrow() + nodeBVaultUpdates.expectEvents { + expect { update -> + println("Bob got vault update of $update") + val amount: Amount> = update.produced.first().state.data.amount + assertEquals(1234.DOLLARS, amount.withoutIssuer()) + } + } + } + } +} 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 c135093517..e8e966e181 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -42,11 +42,7 @@ import net.corda.node.services.Permissions import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.SecurityConfiguration -import net.corda.node.services.config.shouldInitCrashShell -import net.corda.node.services.config.shouldStartLocalShell -import net.corda.node.services.config.JmxReporterType +import net.corda.node.services.config.* import net.corda.node.services.messaging.* import net.corda.node.services.rpc.ArtemisRpcBroker import net.corda.node.utilities.AddressUtils @@ -58,8 +54,8 @@ import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.serialization.internal.* import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.serialization.internal.* import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -71,10 +67,10 @@ import java.net.InetAddress import java.nio.file.Path import java.nio.file.Paths import java.time.Clock +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName import kotlin.system.exitProcess -import java.util.concurrent.TimeUnit class NodeWithInfo(val node: Node, val info: NodeInfo) { val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} @@ -282,7 +278,7 @@ open class Node(configuration: NodeConfiguration, } } - override fun myAddresses(): List = listOf(getAdvertisedAddress()) + override fun myAddresses(): List = listOf(getAdvertisedAddress()) + configuration.additionalP2PAddresses private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 2754629f75..1d7f89f7bc 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -50,6 +50,7 @@ interface NodeConfiguration { val notary: NotaryConfig? val additionalNodeInfoPollingFrequencyMsec: Long val p2pAddress: NetworkHostAndPort + val additionalP2PAddresses: List val rpcOptions: NodeRpcOptions val messagingServerAddress: NetworkHostAndPort? val messagingServerExternal: Boolean @@ -198,6 +199,7 @@ data class NodeConfigurationImpl( override val verifierType: VerifierType, override val flowTimeout: FlowTimeoutConfiguration, override val p2pAddress: NetworkHostAndPort, + override val additionalP2PAddresses: List = emptyList(), private val rpcAddress: NetworkHostAndPort? = null, private val rpcSettings: NodeRpcSettings, override val messagingServerAddress: NetworkHostAndPort?, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index b78e971e78..d4babbc13a 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -120,7 +120,7 @@ class P2PMessagingClient(val config: NodeConfiguration, private lateinit var advertisedAddress: NetworkHostAndPort private var maxMessageSize: Int = -1 - override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity, advertisedAddress) + override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity) override val ourSenderUUID = UUID.randomUUID().toString() private val state = ThreadBox(InnerState()) @@ -233,7 +233,7 @@ class P2PMessagingClient(val config: NodeConfiguration, fun gatherAddresses(node: NodeInfo): Sequence { return state.locked { node.legalIdentitiesAndCerts.map { - val messagingAddress = NodeAddress(it.party.owningKey, node.addresses.first()) + val messagingAddress = NodeAddress(it.party.owningKey) BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }) }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() } @@ -242,14 +242,14 @@ class P2PMessagingClient(val config: NodeConfiguration, fun deployBridges(node: NodeInfo) { gatherAddresses(node) .forEach { - sendBridgeControl(BridgeControl.Create(myIdentity.toStringShort(), it)) + sendBridgeControl(BridgeControl.Create(config.myLegalName.toString(), it)) } } fun destroyBridges(node: NodeInfo) { gatherAddresses(node) .forEach { - sendBridgeControl(BridgeControl.Delete(myIdentity.toStringShort(), it)) + sendBridgeControl(BridgeControl.Delete(config.myLegalName.toString(), it)) } } @@ -289,7 +289,7 @@ class P2PMessagingClient(val config: NodeConfiguration, delayStartQueues += queue.toString() } } - val startupMessage = BridgeControl.NodeToBridgeSnapshot(myIdentity.toStringShort(), inboxes, requiredBridges) + val startupMessage = BridgeControl.NodeToBridgeSnapshot(config.myLegalName.toString(), inboxes, requiredBridges) sendBridgeControl(startupMessage) } @@ -495,7 +495,7 @@ class P2PMessagingClient(val config: NodeConfiguration, val peers = networkMap.getNodesByOwningKeyIndex(keyHash) for (node in peers) { val bridge = BridgeEntry(queueName, node.addresses, node.legalIdentities.map { it.name }) - val createBridgeMessage = BridgeControl.Create(myIdentity.toStringShort(), bridge) + val createBridgeMessage = BridgeControl.Create(config.myLegalName.toString(), bridge) sendBridgeControl(createBridgeMessage) } } @@ -540,7 +540,7 @@ class P2PMessagingClient(val config: NodeConfiguration, override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey, partyInfo.addresses.single()) + is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey) is PartyInfo.DistributedNode -> ServiceAddress(partyInfo.party.owningKey) } } diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index e1ba8a3ed0..a09a38df1d 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -3,6 +3,7 @@ keyStorePassword = "cordacadevpass" trustStorePassword = "trustpass" crlCheckSoftFail = true lazyBridgeStart = true +additionalP2PAddresses = [] dataSourceProperties = { dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0;LOCK_TIMEOUT=10000" 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 25f9f01371..233a362d68 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 @@ -197,7 +197,8 @@ 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("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") + 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 @@ -604,7 +605,8 @@ class DriverDSLImpl( } } } - val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process) + val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress + val p2pReadyFuture = addressMustBeBoundFuture(executorService, effectiveP2PAddress, process) return p2pReadyFuture.flatMap { val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") { if (process.isAlive) null else process @@ -614,7 +616,7 @@ class DriverDSLImpl( val networkMapFuture = executorService.fork { visibilityHandle.listen(rpc) }.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { - throw ListenProcessDeathException(config.corda.p2pAddress, process) + throw ListenProcessDeathException(effectiveP2PAddress, process) } // Will interrupt polling for process death as this is no longer relevant since the process been // successfully started and reflected itself in the NetworkMap. @@ -689,6 +691,7 @@ class DriverDSLImpl( executorService: ScheduledExecutorService, config: NodeConfig ): CordaFuture> { + val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress return executorService.fork { log.info("Starting in-process Node ${config.corda.myLegalName.organisation}") if (!(ManagementFactory.getRuntimeMXBean().inputArguments.any { it.contains("quasar") })) { @@ -705,7 +708,7 @@ class DriverDSLImpl( } nodeWithInfo to nodeThread }.flatMap { nodeAndThread -> - addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread } + addressMustBeBoundFuture(executorService, effectiveP2PAddress).map { nodeAndThread } } } From 3284a61afd8b9825ba77bfadf72038edc6c22763 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Thu, 6 Sep 2018 09:37:04 +0100 Subject: [PATCH 039/119] CORDA-1755: Modify the node to run using picocli (#3872) * Modify Corda Node to use picocli * Make --sshd parameter actually work * * Some refactoring * Fixing the issue with the --confg-file parameter * Updating the tests * Restore original devMode behaviour * Update documentation * Add return code to network bootstrapper * Use the root jar for the shell alias for jars packaged with capsule * Update Corda jar description * Fix issue with logging not initialising early enough in node Make initLogging overridable Combine --verbose and --log-to-console options * Tidy up * Make sure all command line options are documented properly * Fix compilation error * Remove code that's no longer needed (single slash options no longer supported unless explicitly specified) * Remove comment * Remove pointless comment * Log commandline arguments * Address review comments * Address more review comments * Remove ConfigFilePathArgsParser * Remove some unused importss * Only display config when in dev mode * Force Ansi ON if on Windows else set to AUTO. * Make ExitCodes class open --- docs/source/running-a-node.rst | 79 ++++++-- node/build.gradle | 4 +- node/src/main/java/CordaCaplet.java | 8 - node/src/main/kotlin/net/corda/node/Corda.kt | 4 +- .../kotlin/net/corda/node/NodeArgsParser.kt | 142 ------------- .../net/corda/node/NodeCmdLineOptions.kt | 135 +++++++++++++ .../net/corda/node/internal/NodeStartup.kt | 132 +++++------- .../node/services/config/ConfigUtilities.kt | 13 +- .../net/corda/node/NodeArgsParserTest.kt | 189 ------------------ .../net/corda/node/NodeCmdLineOptionsTest.kt | 44 ++++ .../kotlin/net/corda/bootstrapper/Main.kt | 5 +- tools/cliutils/build.gradle | 3 + .../cliutils/ConfigFilePathArgsParser.kt | 25 --- .../net/corda/cliutils/CordaCliWrapper.kt | 66 ++++-- .../corda/cliutils/CordaVersionProvider.kt | 10 +- .../kotlin/net/corda/cliutils/ExitCodes.kt | 8 + .../cliutils/InstallShellExtensionsParser.kt | 10 +- 17 files changed, 382 insertions(+), 495 deletions(-) delete mode 100644 node/src/main/kotlin/net/corda/node/NodeArgsParser.kt create mode 100644 node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt delete mode 100644 node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt create mode 100644 node/src/test/kotlin/net/corda/node/NodeCmdLineOptionsTest.kt delete mode 100644 tools/cliutils/src/main/kotlin/net/corda/cliutils/ConfigFilePathArgsParser.kt create mode 100644 tools/cliutils/src/main/kotlin/net/corda/cliutils/ExitCodes.kt diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index 14c6f91268..2caf9e64c5 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -47,22 +47,75 @@ Command-line options ~~~~~~~~~~~~~~~~~~~~ The node can optionally be started with the following command-line options: -* ``--base-directory``: The node working directory where all the files are kept (default: ``.``) +* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``). * ``--bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer - addresses), acting as a seed for other nodes to join the cluster -* ``--config-file``: The path to the config file (default: ``node.conf``) -* ``--help`` + addresses), acting as a seed for other nodes to join the cluster. +* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system. +* ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``. +* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. +* ``--help``, ``-h``: Displays the help message and exits. * ``--initial-registration``: Start initial node registration with Corda network to obtain certificate from the permissioning - server + server. +* ``--install-shell-extensions``: Installs an alias and auto-completion for users of ``bash`` or ``zsh``. See below for more information. * ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then - quit -* ``--log-to-console``: If set, prints logging to the console as well as to a file -* ``--logging-level <[ERROR,WARN,INFO, DEBUG,TRACE]>``: Enable logging at this level and higher (default: INFO) -* ``--network-root-truststore``: Network root trust store obtained from network operator -* ``--network-root-truststore-password``: Network root trust store password obtained from network operator -* ``--no-local-shell``: Do not start the embedded shell locally -* ``--sshd``: Enables SSHD server for node administration -* ``--version``: Print the version and exit + quit. +* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection. +* ``--log-to-console``, ``--verbose``, ``-v``: If set, prints logging to the console as well as to a file. +* ``--logging-level <[ERROR,WARN,INFO,DEBUG,TRACE]>``: Enable logging at this level and higher. Defaults to INFO. +* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator. +* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. +* ``--no-local-shell``, ``-n``: Do not start the embedded shell locally. +* ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL. +* ``--sshd``: Enables SSH server for node administration. +* ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222. +* ``--version``, ``-V``: Prints the version and exits. + +.. _installing-shell-extensions: + +Installing shell extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users of ``bash`` or ``zsh`` can install an alias and command line completion for Corda. Run: + +.. code-block:: shell + + java -jar corda.jar --install-shell-extensions + +Then, either restart your shell, or for ``bash`` users run: + +.. code-block:: shell + + . ~/.bashrc + +Or, for ``zsh`` run: + +.. code-block:: shell + + . ~/.zshrc + +You will now be able to run a Corda node from anywhere by running the following: + +.. code-block:: shell + + corda --