diff --git a/.ci/dev/publish-branch/Jenkinsfile.nightly b/.ci/dev/publish-branch/Jenkinsfile.nightly index cfb17f0209..b560329b81 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.nightly +++ b/.ci/dev/publish-branch/Jenkinsfile.nightly @@ -34,6 +34,10 @@ pipeline { // in the name ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish Nightly to Artifactory" .replaceAll("/", " :: ") + BUILD_CACHE_CREDENTIALS = credentials('gradle-ent-cache-credentials') + BUILD_CACHE_PASSWORD = "${env.BUILD_CACHE_CREDENTIALS_PSW}" + BUILD_CACHE_USERNAME = "${env.BUILD_CACHE_CREDENTIALS_USR}" + USE_CACHE = 'corda-remotes' DOCKER_URL = "https://index.docker.io/v1/" JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto" } diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 9d299ea1a2..832a7ce580 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -329,7 +329,7 @@ pipeline { './gradlew', COMMON_GRADLE_PARAMS, 'docker:pushDockerImage', - '-Pdocker.image.repository=corda/community', + '-Pdocker.image.repository=corda/open-source', '--image OFFICIAL' ].join(' ') } diff --git a/build.gradle b/build.gradle index 656c963722..2c02bfeedd 100644 --- a/build.gradle +++ b/build.gradle @@ -165,7 +165,9 @@ buildscript { } } mavenCentral() - jcenter() + maven { + url "${publicArtifactURL}/jcenter-backup" + } } } dependencies { @@ -406,7 +408,9 @@ allprojects { } } mavenCentral() - jcenter() + maven { + url "${publicArtifactURL}/jcenter-backup" + } } } diff --git a/buildSrc/src/main/groovy/corda.common-publishing.gradle b/buildSrc/src/main/groovy/corda.common-publishing.gradle index 11e35d45cc..20b2d4be8d 100644 --- a/buildSrc/src/main/groovy/corda.common-publishing.gradle +++ b/buildSrc/src/main/groovy/corda.common-publishing.gradle @@ -5,6 +5,38 @@ import groovy.transform.CompileStatic if (System.getenv('CORDA_ARTIFACTORY_USERNAME') != null || project.hasProperty('cordaArtifactoryUsername')) { logger.info("Internal R3 user - resolving publication build dependencies from internal plugins") pluginManager.apply('com.r3.internal.gradle.plugins.r3Publish') + afterEvaluate { + publishing { + publications { + configureEach { + def repo = "https://github.com/corda/corda" + pom { + description = project.description + name = project.name + url = repo + scm { + url = repo + } + licenses { + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + distribution = 'repo' + } + } + + developers { + developer { + id = 'R3' + name = 'R3' + email = 'dev@corda.net' + } + } + } + } + } + } + } } else { logger.info("External user - using standard maven publishing") pluginManager.apply('maven-publish') diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 0668b3a58a..948efa0c26 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -3,6 +3,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' apply plugin: 'corda.api-scanner' +description 'Corda Jackson module' + dependencies { api project(':core') 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 09c8c5a4a9..4ae3373aac 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 @@ -552,7 +552,7 @@ class RPCStabilityTests { // Construct an RPC session manually so that we can hang in the message handler val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val session = startArtemisSession(server.broker.hostAndPort!!) - session.createQueue(QueueConfiguration(myQueue) + session.createQueue(QueueConfiguration.of(myQueue) .setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType()) .setAddress(myQueue) .setTemporary(true) @@ -569,7 +569,7 @@ class RPCStabilityTests { val message = session.createMessage(false) val request = RPCApi.ClientToServer.RpcRequest( - clientAddress = SimpleString(myQueue), + clientAddress = SimpleString.of(myQueue), methodName = SlowConsumerRPCOps::streamAtInterval.name, serialisedArguments = listOf(100.millis, 1234).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), replyId = Trace.InvocationId.newInstance(), @@ -593,7 +593,7 @@ class RPCStabilityTests { // Construct an RPC client session manually val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val session = startArtemisSession(server.broker.hostAndPort!!) - session.createQueue(QueueConfiguration(myQueue) + session.createQueue(QueueConfiguration.of(myQueue) .setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType()) .setAddress(myQueue) .setTemporary(true) @@ -612,7 +612,7 @@ class RPCStabilityTests { val message = session.createMessage(false) val request = RPCApi.ClientToServer.RpcRequest( - clientAddress = SimpleString(myQueue), + clientAddress = SimpleString.of(myQueue), methodName = DummyOps::protocolVersion.name, serialisedArguments = emptyList().serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), replyId = Trace.InvocationId.newInstance(), 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 ba8e70786d..141ad76b79 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 @@ -652,9 +652,9 @@ internal class RPCClientProxyHandler( producerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) rpcProducer = producerSession!!.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME) consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, 16384) - clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$rpcUsername.${random63BitValue()}") + clientAddress = SimpleString.of("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$rpcUsername.${random63BitValue()}") log.debug { "Client address: $clientAddress" } - consumerSession!!.createQueue(QueueConfiguration(clientAddress).setAddress(clientAddress).setRoutingType(RoutingType.ANYCAST) + consumerSession!!.createQueue(QueueConfiguration.of(clientAddress).setAddress(clientAddress).setRoutingType(RoutingType.ANYCAST) .setTemporary(true).setDurable(false)) rpcConsumer = consumerSession!!.createConsumer(clientAddress) rpcConsumer!!.setMessageHandler(this::artemisMessageHandler) diff --git a/common/configuration-parsing/build.gradle b/common/configuration-parsing/build.gradle index 4a7a7b05d1..7092bd2e81 100644 --- a/common/configuration-parsing/build.gradle +++ b/common/configuration-parsing/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'corda.common-publishing' +description 'Corda common-configuration-parsing module' + dependencies { implementation group: "org.jetbrains.kotlin", name: "kotlin-reflect", version: kotlin_version diff --git a/common/logging/build.gradle b/common/logging/build.gradle index b706a19432..5c1c6760c7 100644 --- a/common/logging/build.gradle +++ b/common/logging/build.gradle @@ -3,6 +3,8 @@ import org.apache.tools.ant.filters.ReplaceTokens apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'corda.common-publishing' +description 'Corda common-logging module' + dependencies { implementation group: "org.jetbrains.kotlin", name: "kotlin-reflect", version: kotlin_version diff --git a/common/validation/build.gradle b/common/validation/build.gradle index 56bab1a8d8..5b14f50640 100644 --- a/common/validation/build.gradle +++ b/common/validation/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'corda.common-publishing' +description 'Corda common-validation module' + dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index 4c7cd9e050..616fffc0c5 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -47,6 +47,7 @@ publishing { maven(MavenPublication) { artifactId 'corda-confidential-identities' from components.cordapp + artifact javadocJar } } } diff --git a/constants.properties b/constants.properties index fd957f4c62..ba81c9c19e 100644 --- a/constants.properties +++ b/constants.properties @@ -36,7 +36,7 @@ openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 detektVersion=1.0.1 tcnativeVersion=2.0.48.Final -commonsConfiguration2Version=2.10.1 +commonsConfiguration2Version=2.11.0 commonsTextVersion=1.10.0 # ENT-6607 all third party version in here now @@ -45,9 +45,9 @@ commonsTextVersion=1.10.0 # We must configure it manually to use the latest capsule version. capsuleVersion=1.0.4_r3 asmVersion=9.5 -artemisVersion=2.32.0 +artemisVersion=2.36.0 # TODO Upgrade Jackson only when corda is using kotlin 1.3.10 -jacksonVersion=2.17.0 +jacksonVersion=2.17.2 jacksonKotlinVersion=2.17.0 jettyVersion=12.0.7 jerseyVersion=3.1.6 diff --git a/core-tests/build.gradle b/core-tests/build.gradle index 08823f41ee..8c82496a2a 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -108,6 +108,7 @@ dependencies { testImplementation "org.bouncycastle:bcprov-lts8on:${bouncycastle_version}" testImplementation "io.netty:netty-common:$netty_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testImplementation "io.dropwizard.metrics:metrics-jmx:$metrics_version" testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}" diff --git a/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt index c955e830d2..1dacd76c47 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractState +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.NoConstraintPropagation import net.corda.core.contracts.SignatureAttachmentConstraint @@ -341,52 +342,53 @@ class ConstraintsPropagationTests { // propagation check // TODO - enable once the logic to transition has been added. - assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentSigned)) + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentSigned, CordaRotatedKeys.keys)) } @Test(timeout=300_000) fun `Attachment canBeTransitionedFrom behaves as expected`() { // signed attachment (for signature constraint) + val rotatedKeys = CordaRotatedKeys.keys val attachment = mock() whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey)) whenever(attachment.allContracts).thenReturn(setOf(propagatingContractClassName)) // Exhaustive positive check - assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) - assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys)) + assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys)) - assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) - assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys)) + assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys)) - assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys)) - assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys)) // Exhaustive negative check - assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) - assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) - assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys)) + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys)) + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys)) - assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys)) - assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) - assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys)) + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys)) - assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) - assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) - assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys)) + assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys)) + assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys)) - assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) - assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) - assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys)) + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys)) + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys)) // Fail when encounter a AutomaticPlaceholderConstraint assertFailsWith { HashAttachmentConstraint(SecureHash.randomSHA256()) - .canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) + .canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment, rotatedKeys) } - assertFailsWith { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) } + assertFailsWith { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment, rotatedKeys) } } private fun MockServices.recordTransaction(wireTransaction: WireTransaction) { diff --git a/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt new file mode 100644 index 0000000000..8e0da67b8d --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt @@ -0,0 +1,279 @@ +package net.corda.coretests.contracts + +import net.corda.core.contracts.RotatedKeys +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.sha256 +import net.corda.core.internal.hash +import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey +import net.corda.testing.core.internal.SelfCleaningDir +import org.junit.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RotatedKeysTest { + @Test(timeout = 300_000) + fun `when input and output keys are the same canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKey = file.path.generateKey() + val rotatedKeys = RotatedKeys() + assertTrue(rotatedKeys.canBeTransitioned(publicKey, publicKey)) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are the same and output is a list canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKey = file.path.generateKey() + val rotatedKeys = RotatedKeys() + assertTrue(rotatedKeys.canBeTransitioned(publicKey, listOf(publicKey))) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different and output is a list canBeTransitioned returns false`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey("AAAA") + val publicKeyB = file.path.generateKey("BBBB") + val rotatedKeys = RotatedKeys() + assertFalse(rotatedKeys.canBeTransitioned(publicKeyA, listOf(publicKeyB))) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different and rotated and output is a list canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey("AAAA") + val publicKeyB = file.path.generateKey("BBBB") + val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256())))) + assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, listOf(publicKeyB))) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are the same and both are lists canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKey = file.path.generateKey() + val rotatedKeys = RotatedKeys() + assertTrue(rotatedKeys.canBeTransitioned(listOf(publicKey), listOf(publicKey))) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different and rotated and both are lists canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256())))) + assertTrue(rotatedKeys.canBeTransitioned(listOf(publicKeyA), listOf(publicKeyB))) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different canBeTransitioned returns false`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val rotatedKeys = RotatedKeys() + assertFalse(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB)) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different but are rotated canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val rotatedKeysData = listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()))) + val rotatedKeys = RotatedKeys(rotatedKeysData) + assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB)) + } + } + + @Test(timeout = 300_000) + fun `when input and output keys are different with multiple rotations canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()), + listOf(publicKeyC.hash.sha256(), publicKeyD.hash.sha256())) + val rotatedKeys = RotatedKeys(rotatedKeysData) + assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB)) + } + } + + @Test(timeout = 300_000) + fun `when multiple input and output keys are different with multiple rotations canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256()), + listOf(publicKeyB.hash.sha256(), publicKeyD.hash.sha256())) + val rotatedKeys = RotatedKeys(rotatedKeysData) + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build() + assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when multiple input and output keys are diff and diff ordering with multiple rotations canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256()), + listOf(publicKeyB.hash.sha256(), publicKeyD.hash.sha256())) + val rotatedKeys = RotatedKeys(rotatedKeysData) + + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyD, publicKeyC).build() + assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are composite and the same canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val compositeKey = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val rotatedKeys = RotatedKeys() + assertTrue(rotatedKeys.canBeTransitioned(compositeKey, compositeKey)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are composite and different canBeTransitioned returns false`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build() + val rotatedKeys = RotatedKeys() + assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are composite and different but key is rotated canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build() + val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyB.hash.sha256(), publicKeyC.hash.sha256())))) + assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are composite and different and diff key is rotated canBeTransitioned returns false`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build() + val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256())))) + assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input is composite (1 key) and output is composite (2 keys) canBeTransitioned returns false`() { + // For composite keys number of input and output leaves must be the same, in canBeTransitioned check. + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val rotatedKeys = RotatedKeys() + assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are composite with 2 levels and the same canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build() + val compositeKeyC = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build() + val rotatedKeys = RotatedKeys() + assertTrue(rotatedKeys.canBeTransitioned(compositeKeyC, compositeKeyC)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are different & composite & rotated with 2 levels canBeTransitioned returns true`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + + // in output DDDD has rotated to EEEE + val publicKeyE = file.path.generateKey(alias = "EEEE") + val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build() + val compositeKeyC = CompositeKey.Builder().addKeys(publicKeyC, publicKeyE).build() + + val compositeKeyInput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyC).build() + + val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyD.hash.sha256(), publicKeyE.hash.sha256())))) + assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000) + fun `when input and output key are different & composite & not rotated with 2 levels canBeTransitioned returns false`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + val publicKeyD = file.path.generateKey(alias = "DDDD") + + // in output DDDD has rotated to EEEE + val publicKeyE = file.path.generateKey(alias = "EEEE") + val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build() + val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build() + val compositeKeyC = CompositeKey.Builder().addKeys(publicKeyC, publicKeyE).build() + + val compositeKeyInput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build() + val compositeKeyOutput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyC).build() + + val rotatedKeys = RotatedKeys() + assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput)) + } + } + + @Test(timeout = 300_000, expected = IllegalStateException::class) + fun `when key is repeated in rotated list, throws exception`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + RotatedKeys(listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256(), publicKeyA.hash.sha256()))) + } + } + + @Test(timeout = 300_000, expected = IllegalStateException::class) + fun `when key is repeated across rotated lists, throws exception`() { + SelfCleaningDir().use { file -> + val publicKeyA = file.path.generateKey(alias = "AAAA") + val publicKeyB = file.path.generateKey(alias = "BBBB") + val publicKeyC = file.path.generateKey(alias = "CCCC") + RotatedKeys(listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()), listOf(publicKeyC.hash.sha256(), publicKeyA.hash.sha256()))) + } + } +} \ No newline at end of file diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 4eb85ec26b..9c4ecf4d99 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -518,7 +518,8 @@ class FinalityFlowTests : WithFinality { @Suspendable override fun call() { val handleNotaryError = otherSide.receive().unwrap { it } - subFlow(ReceiveFinalityFlow(otherSide, handlePropagatedNotaryError = handleNotaryError)) + val stx = subFlow(ReceiveFinalityFlow(otherSide, handlePropagatedNotaryError = handleNotaryError)) + stx.verify(serviceHub) } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index 8c60b950be..3827697e93 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -73,7 +73,7 @@ class AttachmentsClassLoaderTests { } val ALICE = TestIdentity(ALICE_NAME, 70).party val BOB = TestIdentity(BOB_NAME, 80).party - val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) + private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val DUMMY_NOTARY get() = dummyNotary.party const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract" } @@ -344,141 +344,6 @@ class AttachmentsClassLoaderTests { createClassloader(untrustedAttachment).use {} } - @Test(timeout=300_000) - fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() { - val keyPairA = Crypto.generateKeyPair() - val keyPairB = Crypto.generateKeyPair() - val untrustedClassJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone untrusted" - ).inputStream() - val untrustedAttachment = storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - untrustedClassJar, - signers = listOf(keyPairA.public, keyPairB.public) - ) - - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment).use {} - } - } - - @Test(timeout=300_000) - fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() { - val keyPairA = Crypto.generateKeyPair() - val keyPairB = Crypto.generateKeyPair() - val classJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone untrusted with the same keys" - ).inputStream() - storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - classJar, - signers = listOf(keyPairA.public, keyPairB.public) - ) - - val untrustedClassJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone untrusted" - ).inputStream() - val untrustedAttachment = storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - untrustedClassJar, - signers = listOf(keyPairA.public, keyPairB.public) - ) - - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment).use {} - } - } - - @Test(timeout=300_000) - fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() { - val keyPairA = Crypto.generateKeyPair() - val keyPairB = Crypto.generateKeyPair() - val keyPairC = Crypto.generateKeyPair() - val classJar = fakeAttachment( - "/com/example/something/TrustedClass.class", - "Signed by someone untrusted with the same keys" - ).inputStream() - storage.importContractAttachment( - listOf("TrustedClass.class"), - "app", - classJar, - signers = listOf(keyPairA.public) - ) - - val inheritedTrustClassJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone who inherits trust" - ).inputStream() - val inheritedTrustAttachment = storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - inheritedTrustClassJar, - signers = listOf(keyPairB.public, keyPairA.public) - ) - - val untrustedClassJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone untrusted" - ).inputStream() - val untrustedAttachment = storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - untrustedClassJar, - signers = listOf(keyPairB.public, keyPairC.public) - ) - - // pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment - createClassloader(inheritedTrustAttachment).use { - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment).use {} - } - } - } - - @Test(timeout=300_000) - fun `Cannot load an untrusted contract jar if it is signed by a blacklisted key even if there is another attachment signed by the same keys that is trusted`() { - val keyPairA = Crypto.generateKeyPair() - val keyPairB = Crypto.generateKeyPair() - - attachmentTrustCalculator = NodeAttachmentTrustCalculator( - storage.toInternal(), - cacheFactory, - blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash) - ) - - val classJar = fakeAttachment( - "/com/example/something/TrustedClass.class", - "Signed by someone trusted" - ).inputStream() - storage.importContractAttachment( - listOf("TrustedClass.class"), - "rpc", - classJar, - signers = listOf(keyPairA.public, keyPairB.public) - ) - - val untrustedClassJar = fakeAttachment( - "/com/example/something/UntrustedClass.class", - "Signed by someone untrusted" - ).inputStream() - val untrustedAttachment = storage.importContractAttachment( - listOf("UntrustedClass.class"), - "untrusted", - untrustedClassJar, - signers = listOf(keyPairA.public, keyPairB.public) - ) - - assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { - createClassloader(untrustedAttachment).use {} - } - } - @Test(timeout=300_000) fun `Allow loading a trusted attachment that is signed by a blacklisted key`() { val keyPairA = Crypto.generateKeyPair() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt new file mode 100644 index 0000000000..35d0878bb7 --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt @@ -0,0 +1,237 @@ +package net.corda.coretests.transactions + +import com.codahale.metrics.MetricRegistry +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.hash +import net.corda.core.internal.verification.NodeVerificationSupport +import net.corda.core.node.NetworkParameters +import net.corda.core.node.services.AttachmentId +import net.corda.core.serialization.internal.AttachmentsClassLoader +import net.corda.coretesting.internal.rigorousMock +import net.corda.node.services.attachments.NodeAttachmentTrustCalculator +import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.node.services.persistence.toInternal +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.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.internal.ContractJarTestUtils +import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar +import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey +import net.corda.testing.core.internal.JarSignatureTestUtils.signJar +import net.corda.testing.core.internal.SelfCleaningDir +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import java.net.URL +import kotlin.test.assertFailsWith + +class AttachmentsClassLoaderWithStoragePersistenceTests { + companion object { + val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderWithStoragePersistenceTests::class.java.getResource("isolated-4.0.jar")!! + private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) + val DUMMY_NOTARY get() = dummyNotary.party + const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract" + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private lateinit var database: CordaPersistence + private lateinit var storage: NodeAttachmentService + private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator + private lateinit var attachmentTrustCalculator2: AttachmentTrustCalculator + private val networkParameters = testNetworkParameters() + private val cacheFactory = TestingNamedCacheFactory(1) + private val cacheFactory2 = TestingNamedCacheFactory() + private val nodeVerificationSupport = rigorousMock().also { + doReturn(testNetworkParameters()).whenever(it).networkParameters + } + + private fun createClassloader( + attachment: AttachmentId, + params: NetworkParameters = networkParameters + ): AttachmentsClassLoader { + return createClassloader(listOf(attachment), params) + } + + private fun createClassloader( + attachments: List, + params: NetworkParameters = networkParameters + ): AttachmentsClassLoader { + return AttachmentsClassLoader( + attachments.map { storage.openAttachment(it)!! }, + params, + SecureHash.zeroHash, + attachmentTrustCalculator2::calculate + ) + } + + @Before + fun setUp() { + val dataSourceProperties = MockServices.makeTestDataSourceProperties() + database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) + storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { + database.transaction { + it.start() + } + } + storage.nodeVerificationSupport = nodeVerificationSupport + attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory) + attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(storage, database, cacheFactory2) + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() { + val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH_V4, copyFirst = true) + val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "untrusted", "isolated-signed.jar" ) + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(isolatedSignedId).use {} + } + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() { + SelfCleaningDir().use { file -> + val path = file.path + val alias1 = "AAAA" + val alias2 = "BBBB" + val password = "testPassword" + + path.generateKey(alias1, password) + path.generateKey(alias2, password) + + val contractName = "net.corda.testing.contracts.MyDummyContract" + val content = createContractString(contractName) + val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, contractName, content = content, version = 2) + path.signJar(contractJarPath.toAbsolutePath().toString(), alias1, password) + path.signJar(contractJarPath.toAbsolutePath().toString(), alias2, password) + val untrustedAttachment = storage.importAttachment(contractJarPath.toUri().toURL().openStream(), "untrusted", "contract.jar") + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + + @Test(timeout=300_000) + fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() { + SelfCleaningDir().use { file -> + val path = file.path + val alias1 = "AAAA" + val alias2 = "BBBB" + val alias3 = "CCCC" + val password = "testPassword" + + path.generateKey(alias1, password) + path.generateKey(alias2, password) + path.generateKey(alias3, password) + + val contractName1 = "net.corda.testing.contracts.MyDummyContract1" + val contractName2 = "net.corda.testing.contracts.MyDummyContract2" + val contractName3 = "net.corda.testing.contracts.MyDummyContract3" + + val content = createContractString(contractName1) + val contractJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = content) + path.signJar(contractJar.toAbsolutePath().toString(), alias1, password) + storage.privilegedImportAttachment(contractJar.toUri().toURL().openStream(), "app", "contract.jar") + + val content2 = createContractString(contractName2) + val contractJarPath2 = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = content2, version = 2) + path.signJar(contractJarPath2.toAbsolutePath().toString(), alias1, password) + path.signJar(contractJarPath2.toAbsolutePath().toString(), alias2, password) + val inheritedTrustAttachment = storage.importAttachment(contractJarPath2.toUri().toURL().openStream(), "untrusted", "dummy-contract.jar") + + val content3 = createContractString(contractName3) + val contractJarPath3 = ContractJarTestUtils.makeTestContractJar(path, contractName3, content = content3, version = 3) + path.signJar(contractJarPath3.toAbsolutePath().toString(), alias2, password) + path.signJar(contractJarPath3.toAbsolutePath().toString(), alias3, password) + val untrustedAttachment = storage.importAttachment(contractJarPath3.toUri().toURL() + .openStream(), "untrusted", "contract.jar") + + // pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment + createClassloader(inheritedTrustAttachment).use { + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if it is signed by a blacklisted key even if there is another attachment signed by the same keys that is trusted`() { + SelfCleaningDir().use { file -> + + val path = file.path + val aliasA = "AAAA" + val aliasB = "BBBB" + val password = "testPassword" + + val publicKeyA = path.generateKey(aliasA, password) + path.generateKey(aliasB, password) + + attachmentTrustCalculator2 = NodeAttachmentTrustCalculator( + storage, + cacheFactory, + blacklistedAttachmentSigningKeys = listOf(publicKeyA.hash) + ) + + val contractName1 = "net.corda.testing.contracts.MyDummyContract1" + val contractName2 = "net.corda.testing.contracts.MyDummyContract2" + + val contentTrusted = createContractString(contractName1) + val classJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = contentTrusted) + path.signJar(classJar.toAbsolutePath().toString(), aliasA, password) + path.signJar(classJar.toAbsolutePath().toString(), aliasB, password) + storage.privilegedImportAttachment(classJar.toUri().toURL().openStream(), "app", "contract.jar") + + val contentUntrusted = createContractString(contractName2) + val untrustedClassJar = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = contentUntrusted) + path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasA, password) + path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasB, password) + val untrustedAttachment = storage.importAttachment(untrustedClassJar.toUri().toURL() + .openStream(), "untrusted", "untrusted-contract.jar") + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + + private fun createContractString(contractName: String, versionSeed: Int = 0): String { + val pkgs = contractName.split(".") + val className = pkgs.last() + val packages = pkgs.subList(0, pkgs.size - 1) + + val output = """package ${packages.joinToString(".")}; + import net.corda.core.contracts.*; + import net.corda.core.transactions.*; + import java.net.URL; + import java.io.InputStream; + + public class $className implements Contract { + private int seed = $versionSeed; + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + System.gc(); + InputStream str = this.getClass().getClassLoader().getResourceAsStream("importantDoc.pdf"); + if (str == null) throw new IllegalStateException("Could not find importantDoc.pdf"); + } + } + """.trimIndent() + + println(output) + return output + } +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt new file mode 100644 index 0000000000..71d3aca7f6 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -0,0 +1,117 @@ +package net.corda.core.contracts + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.internal.hash +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken +import java.security.PublicKey +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +object CordaRotatedKeys { + val keys = RotatedKeys() +} + +// The current development CorDapp code signing public key hash +const val DEV_CORDAPP_CODE_SIGNING_STR = "AA59D829F2CA8FDDF5ABEA40D815F937E3E54E572B65B93B5C216AE6594E7D6B" +// The non production CorDapp code signing public key hash +const val NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR = "B710A80780A12C52DF8A0B4C2247E08907CCA5D0F19AB1E266FE7BAEA9036790" +// The production CorDapp code signing public key hash +const val PROD_CORDAPP_CODE_SIGNING_STR = "EB4989E7F861FEBEC242E6C24CF0B51C41E108D2C4479D296C5570CB8DAD3EE0" +// The new production CorDapp code signing public key hash +const val NEW_PROD_CORDAPP_CODE_SIGNING_STR = "01EFA14B42700794292382C1EEAC9788A26DAFBCCC98992C01D5BC30EEAACD28" + +// Rotations used by Corda +private val CORDA_SIGNING_KEY_ROTATIONS = listOf( + listOf(SecureHash.create(DEV_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR).sha256()), + listOf(SecureHash.create(PROD_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_PROD_CORDAPP_CODE_SIGNING_STR).sha256()) +) + +/** + * This class represents the rotated CorDapp signing keys known by this node. + * + * A public key in this class is identified by its SHA-256 hash of the public key encoded bytes (@see PublicKey.getEncoded()). + * A sequence of rotated keys is represented by a list of hashes of those public keys. The list of those lists represents + * each unrelated set of rotated keys. A key should not appear more than once, either in the same list of in multiple lists. + * + * For the purposes of SignatureConstraints this means we treat all entries in a list of key hashes as equivalent. + * For two keys to be equivalent, they must be equal, or they must appear in the same list of hashes. + * + * @param rotatedSigningKeys A List of rotated keys. With a rotated key being represented by a list of hashes. This list comes from + * node.conf. + * + */ +@CordaSerializable +data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()): SingletonSerializeAsToken() { + private val canBeTransitionedMap: ConcurrentMap, Boolean> = ConcurrentHashMap() + private val rotateMap: Map = HashMap().apply { + (rotatedSigningKeys + CORDA_SIGNING_KEY_ROTATIONS).forEach { rotatedKeyList -> + rotatedKeyList.forEach { key -> + if (this.containsKey(key)) throw IllegalStateException("The key with sha256(hash) $key appears in the rotated keys configuration more than once.") + this[key] = rotatedKeyList.last() + } + } + } + + fun canBeTransitioned(inputKey: PublicKey, outputKeys: List): Boolean { + return canBeTransitioned(inputKey, CompositeKey.Builder().addKeys(outputKeys).build()) + } + + fun canBeTransitioned(inputKeys: List, outputKeys: List): Boolean { + return canBeTransitioned(CompositeKey.Builder().addKeys(inputKeys).build(), CompositeKey.Builder().addKeys(outputKeys).build()) + } + + fun canBeTransitioned(inputKey: PublicKey, outputKey: PublicKey): Boolean { + // Need to handle if inputKey and outputKey are composite keys. They could be if part of SignatureConstraints + return canBeTransitionedMap.getOrPut(Pair(inputKey, outputKey)) { + when { + (inputKey is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey) + (inputKey is CompositeKey && outputKey !is CompositeKey) -> compareKeys(inputKey, outputKey) + (inputKey !is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey) + else -> isRotatedEquals(inputKey, outputKey) + } + } + } + + private fun rotate(key: SecureHash): SecureHash { + return rotateMap[key] ?: key + } + + private fun isRotatedEquals(inputKey: PublicKey, outputKey: PublicKey): Boolean { + return when { + inputKey == outputKey -> true + rotate(inputKey.hash.sha256()) == rotate(outputKey.hash.sha256()) -> true + else -> false + } + } + + private fun compareKeys(inputKey: CompositeKey, outputKey: PublicKey): Boolean { + if (inputKey.leafKeys.size == 1) { + return canBeTransitioned(inputKey.leafKeys.first(), outputKey) + } + return false + } + + private fun compareKeys(inputKey: PublicKey, outputKey: CompositeKey): Boolean { + if (outputKey.leafKeys.size == 1) { + return canBeTransitioned(inputKey, outputKey.leafKeys.first()) + } + return false + } + + private fun compareKeys(inputKey: CompositeKey, outputKey: CompositeKey): Boolean { + if (inputKey.leafKeys.size != outputKey.leafKeys.size) { + return false + } + else { + inputKey.leafKeys.forEach { inputLeafKey -> + if (!outputKey.leafKeys.any { outputLeafKey -> canBeTransitioned(inputLeafKey, outputLeafKey) }) { + return false + } + } + return true + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 31bbc4cf3b..d109b53f48 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -75,8 +75,8 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow val stx = resolvePayload(payload) stx.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") - checkParameterHash(stx.networkParametersHash) subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) + checkParameterHash(stx.networkParametersHash) logger.info("Transaction dependencies resolution completed.") verifyTx(stx, checkSufficientSignatures) if (checkSufficientSignatures) { @@ -127,6 +127,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) logger.info("Peer finalised transaction with notary signature.") } + return stx + notarySignatures } catch (e: NotaryException) { logger.info("Peer received notary error.") val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index 7e59c207b5..8238eb7fcf 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -57,7 +57,8 @@ val ContractState.requiredContractClassName: String? get() { * JAR are required to sign in the future. * */ -fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean { +@Suppress("ComplexMethod") +fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment, rotatedKeys: RotatedKeys): Boolean { val output = this @Suppress("DEPRECATION") @@ -83,7 +84,7 @@ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, atta // The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key. // TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key. - input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key + input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> rotatedKeys.canBeTransitioned(input.key, output.key) // HashAttachmentConstraint can be transformed to a SignatureAttachmentConstraint when hash constraint verification checking disabled. HashAttachmentConstraint.disableHashConstraints && input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> true diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt index 401b8135f4..50e351d795 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt @@ -1,6 +1,7 @@ package net.corda.core.internal.verification import net.corda.core.contracts.Attachment +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash @@ -24,6 +25,8 @@ interface VerificationSupport { val attachmentsClassLoaderCache: AttachmentsClassLoaderCache? get() = null + val rotatedKeys: RotatedKeys + // TODO Use SequencedCollection if upgraded to Java 21 fun getParties(keys: Collection): List diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt index ab448bd0b0..f6c82b23c9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt @@ -1,10 +1,12 @@ package net.corda.core.internal.verification +import net.corda.core.contracts.AttachmentConstraint import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractState import net.corda.core.contracts.HashAttachmentConstraint +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.SignatureAttachmentConstraint import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef @@ -89,7 +91,7 @@ abstract class AbstractVerifier( * Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the * wrong object instance. This class helps avoid that. */ -private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) { +private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, private val rotatedKeys: RotatedKeys) { private val inputStates: List> = ltx.inputs.map(StateAndRef::state) private val allStates: List> = inputStates + ltx.references.map(StateAndRef::state) + ltx.outputs @@ -376,7 +378,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact outputConstraints.forEach { outputConstraint -> inputConstraints.forEach { inputConstraint -> - if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) { + if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment, rotatedKeys))) { throw ConstraintPropagationRejection( ltx.id, contractClassName, @@ -430,8 +432,20 @@ private class Validator(private val ltx: LedgerTransaction, private val transact if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint) logger.warnOnce("Skipping hash constraints verification.") - else if (!constraint.isSatisfiedBy(constraintAttachment)) - throw ContractConstraintRejection(ltx.id, contract) + else if (!constraint.isSatisfiedBy(constraintAttachment)) { + verifyConstraintUsingRotatedKeys(constraint, constraintAttachment, contract) + } + } + } + + private fun verifyConstraintUsingRotatedKeys(constraint: AttachmentConstraint, constraintAttachment: AttachmentWithContext, contract: ContractClassName ) { + // constraint could be an input constraint so we manually have to rotate to updated constraint + if (constraint is SignatureAttachmentConstraint && rotatedKeys.canBeTransitioned(constraint.key, constraintAttachment.signerKeys)) { + val constraintWithRotatedKeys = SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build()) + if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract) + } + else { + throw ContractConstraintRejection(ltx.id, contract) } } } @@ -465,7 +479,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun } private fun validateTransaction(ltx: LedgerTransaction) { - Validator(ltx, transactionClassLoader).validate() + Validator(ltx, transactionClassLoader, ltx.rotatedKeys).validate() } override fun apply(transactionFactory: Supplier) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index d10b6b962d..6ef0ed0da8 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -4,6 +4,8 @@ import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.CordaRotatedKeys +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException @@ -337,7 +339,8 @@ object AttachmentsClassLoaderBuilder { block: (SerializationContext) -> T): T { val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id) - val cache = attachmentsClassLoaderCache ?: fallBackCache + val cache = if (attachmentsClassLoaderCache is AttachmentsClassLoaderForRotatedKeysOnlyImpl) fallBackCache else + attachmentsClassLoaderCache ?: fallBackCache val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params)) { key -> // Create classloader and load serializers, whitelisted classes val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) @@ -453,14 +456,14 @@ private class AttachmentsHolderImpl : AttachmentsHolder { } interface AttachmentsClassLoaderCache { + val rotatedKeys: RotatedKeys fun computeIfAbsent( key: AttachmentsClassLoaderKey, mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext ): SerializationContext } -class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { - +class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { private class ToBeClosed( serializationContext: SerializationContext, val classLoaderToClose: AutoCloseable, @@ -513,7 +516,7 @@ class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : Singlet } } -class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLoaderCache { +class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache { private val cache: MutableMap = createSimpleCache(cacheSize).toSynchronised() @@ -525,6 +528,12 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLo } } +class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache { + override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext): SerializationContext { + throw NotImplementedError("AttachmentsClassLoaderForRotatedKeysOnlyImpl.computeIfAbsent should never be called. Should be replaced by the fallback cache") + } +} + // We use a set here because the ordering of attachments doesn't affect code execution, due to the no // overlap rule, and attachments don't have any particular ordering enforced by the builders. So we // can just do unordered comparisons here. But the same attachments run with different network parameters diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 3a866562e4..ec6545e61a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -7,7 +7,9 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.CommandWithParties import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.contracts.ContractState +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionState @@ -31,6 +33,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl import net.corda.core.utilities.contextLogger import java.util.Collections.unmodifiableList import java.util.function.Predicate @@ -96,6 +99,8 @@ private constructor( val digestService: DigestService ) : FullTransaction() { + val rotatedKeys = attachmentsClassLoaderCache?.rotatedKeys ?: CordaRotatedKeys.keys + /** * Old version of [LedgerTransaction] constructor for ABI compatibility. */ @@ -195,7 +200,8 @@ private constructor( privacySalt: PrivacySalt, networkParameters: NetworkParameters?, references: List>, - digestService: DigestService): LedgerTransaction { + digestService: DigestService, + rotatedKeys: RotatedKeys): LedgerTransaction { return LedgerTransaction( inputs = protect(inputs), outputs = protect(outputs), @@ -212,7 +218,7 @@ private constructor( serializedReferences = null, isAttachmentTrusted = { true }, verifierFactory = ::NoOpVerifier, - attachmentsClassLoaderCache = null, + attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl(rotatedKeys), digestService = digestService // This check accesses input states and must run on the LedgerTransaction // instance that is verified, not on the outer LedgerTransaction shell. @@ -872,7 +878,8 @@ private class DefaultVerifier( privacySalt = ltx.privacySalt, networkParameters = ltx.networkParameters, references = deserializedReferences, - digestService = ltx.digestService + digestService = ltx.digestService, + rotatedKeys = ltx.rotatedKeys ) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 237c8c39d2..d954abda92 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -532,10 +532,10 @@ open class TransactionBuilder( } // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. - val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment.currentAttachment, serviceHub) + val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName, + inputStates, selectedAttachment.currentAttachment, serviceHub) // Sanity check that the selected attachment actually passes. - val constraintAttachment = AttachmentWithContext(selectedAttachment.currentAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations) require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment" } @@ -547,7 +547,7 @@ open class TransactionBuilder( } else { // If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early. inputStates?.forEach { input -> - require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment)) { + require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment, serviceHub.toVerifyingServiceHub().rotatedKeys)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } } @@ -559,6 +559,27 @@ open class TransactionBuilder( return Pair(selectedAttachment, resolvedOutputStates) } + private fun selectDefaultOutputConstraintAndConstraintAttachment( contractClassName: ContractClassName, + inputStates: List>?, + attachmentToUse: ContractAttachment, + services: ServicesForResolution): Pair { + + val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) + + // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. + val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) + + // Sanity check that the selected attachment actually passes. + + if (!defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { + // The defaultOutputConstraint is the input constraint by the attachment in use currently may have a rotated key + if (defaultOutputConstraint is SignatureAttachmentConstraint && services.toVerifyingServiceHub().rotatedKeys.canBeTransitioned(defaultOutputConstraint.key, constraintAttachment.signerKeys)) { + return Pair(makeSignatureAttachmentConstraint(attachmentToUse.signerKeys), constraintAttachment) + } + } + return Pair(defaultOutputConstraint, constraintAttachment) + } + /** * Checks whether the current transaction can migrate from a [HashAttachmentConstraint] to a * [SignatureAttachmentConstraint]. This is only possible in very specific scenarios. Most diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 79765cb6c6..df6522f0b7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -11,6 +11,7 @@ import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP import net.corda.core.contracts.ContractState import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionResolutionException @@ -181,6 +182,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr override val appClassLoader: ClassLoader get() = throw AbstractMethodError() override fun getTrustedClassAttachments(className: String) = throw AbstractMethodError() override fun fixupAttachmentIds(attachmentIds: Collection) = throw AbstractMethodError() + override val rotatedKeys: RotatedKeys get() = throw AbstractMethodError() }) } diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile index ecd3bb48d5..d6bddfe1ed 100644 --- a/docker/src/docker/Dockerfile +++ b/docker/src/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:17.0.8.1 +FROM azul/zulu-openjdk:17.0.12 ## Remove Azul Zulu repo, as it is gone by now RUN rm -rf /etc/apt/sources.list.d/zulu.list diff --git a/docker/src/docker/Dockerfile-debug b/docker/src/docker/Dockerfile-debug index 8b36530f5e..783b9a5af0 100644 --- a/docker/src/docker/Dockerfile-debug +++ b/docker/src/docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:17.0.8.1 +FROM azul/zulu-openjdk:17.0.12 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN apt-get update && \ diff --git a/docs/build.gradle b/docs/build.gradle index 12a1c72558..ee846412e2 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,6 +1,8 @@ import org.apache.tools.ant.taskdefs.condition.Os apply plugin: 'org.jetbrains.dokka' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.artifactory' dependencies { implementation rootProject @@ -56,7 +58,10 @@ dokkaJavadoc { url.set(new URL("https://docs.oracle.com/javafx/2/api/")) } externalDocumentationLink { - url.set(new URL("https://www.bouncycastle.org/docs/docs1.5on/")) + url.set(new URL("https://downloads.bouncycastle.org/java/docs/bcpkix-jdk18on-javadoc/")) + } + externalDocumentationLink { + url.set(new URL("https://downloads.bouncycastle.org/java/docs/bcprov-jdk18on-javadoc/")) } } } @@ -97,8 +102,6 @@ task archiveApiDocs(type: Tar) { publishing { publications { if (System.getProperty('publishApiDocs') != null) { - apply plugin: 'corda.common-publishing' - archivedApiDocs(MavenPublication) { artifact archiveApiDocs { artifactId archivedApiDocsBaseFilename @@ -107,3 +110,20 @@ publishing { } } } + +artifactoryPublish { + publications('archivedApiDocs') + version = version.replaceAll('-SNAPSHOT', '') + publishPom = false +} + +artifactory { + publish { + contextUrl = artifactory_contextUrl + repository { + repoKey = 'corda-dependencies-dev' + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } +} diff --git a/finance/contracts/build.gradle b/finance/contracts/build.gradle index 345d641d84..46845fd014 100644 --- a/finance/contracts/build.gradle +++ b/finance/contracts/build.gradle @@ -64,6 +64,7 @@ publishing { maven(MavenPublication) { artifactId 'corda-finance-contracts' from components.cordapp + artifact javadocJar } } } diff --git a/finance/workflows/build.gradle b/finance/workflows/build.gradle index 602248fc2c..d52deea15a 100644 --- a/finance/workflows/build.gradle +++ b/finance/workflows/build.gradle @@ -93,6 +93,7 @@ publishing { maven(MavenPublication) { artifactId 'corda-finance-workflows' from components.cordapp + artifact javadocJar } } } 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 7e61e90630..25172487ac 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 @@ -42,18 +42,18 @@ class ArtemisMessagingComponent { // We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint // that will handle messages, like a URL) with the terminology used by underlying MQ libraries, to avoid // confusion. - val topicProperty = SimpleString("platform-topic") - val cordaVendorProperty = SimpleString("corda-vendor") - val releaseVersionProperty = SimpleString("release-version") - val platformVersionProperty = SimpleString("platform-version") - val senderUUID = SimpleString("sender-uuid") - val senderSeqNo = SimpleString("send-seq-no") + val topicProperty = SimpleString.of("platform-topic") + val cordaVendorProperty = SimpleString.of("corda-vendor") + val releaseVersionProperty = SimpleString.of("release-version") + val platformVersionProperty = SimpleString.of("platform-version") + val senderUUID = SimpleString.of("sender-uuid") + val senderSeqNo = SimpleString.of("send-seq-no") /** * In the operation mode where we have an out of process bridge we cannot correctly populate the Artemis validated user header * as the TLS does not terminate directly onto Artemis. We therefore use this internal only header to forward * the equivalent information from the Float. */ - val bridgedCertificateSubject = SimpleString("sender-subject-name") + val bridgedCertificateSubject = SimpleString.of("sender-subject-name") object Type { const val KEY = "corda_p2p_message_type" 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 84974450d4..9c60a9885f 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 @@ -88,7 +88,7 @@ class BridgeControlListener(private val keyStore: CertificateStore, registerBridgeControlListener(artemisSession) registerBridgeDuplicateChecker(artemisSession) // Attempt to read available inboxes directly from Artemis before requesting updates from connected nodes - validInboundQueues.addAll(artemisSession.addressQuery(SimpleString("$P2P_PREFIX#")).queueNames.map { it.toString() }) + validInboundQueues.addAll(artemisSession.addressQuery(SimpleString.of("$P2P_PREFIX#")).queueNames.map { it.toString() }) log.info("Found inboxes: $validInboundQueues") if (active) { _activeChange.onNext(true) @@ -107,7 +107,7 @@ class BridgeControlListener(private val keyStore: CertificateStore, private fun registerBridgeControlListener(artemisSession: ClientSession) { try { artemisSession.createQueue( - QueueConfiguration(bridgeControlQueue).setAddress(BRIDGE_CONTROL).setRoutingType(RoutingType.MULTICAST) + QueueConfiguration.of(bridgeControlQueue).setAddress(BRIDGE_CONTROL).setRoutingType(RoutingType.MULTICAST) .setTemporary(true).setDurable(false)) } catch (ex: ActiveMQQueueExistsException) { // Ignore if there is a queue still not cleaned up @@ -129,7 +129,7 @@ class BridgeControlListener(private val keyStore: CertificateStore, private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) { try { artemisSession.createQueue( - QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) + QueueConfiguration.of(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) .setTemporary(true).setDurable(false)) } catch (ex: ActiveMQQueueExistsException) { // Ignore if there is a queue still not cleaned up @@ -189,11 +189,11 @@ class BridgeControlListener(private val keyStore: CertificateStore, } private fun validateInboxQueueName(queueName: String): Boolean { - return queueName.startsWith(P2P_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString(queueName)).isExists + return queueName.startsWith(P2P_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString.of(queueName)).isExists } private fun validateBridgingQueueName(queueName: String): Boolean { - return queueName.startsWith(PEERS_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString(queueName)).isExists + return queueName.startsWith(PEERS_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString.of(queueName)).isExists } private fun processControlMessage(msg: ClientMessage) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt index 2dd9f8bff0..ce68cfbd96 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/LoopbackBridgeManager.kt @@ -136,7 +136,7 @@ class LoopbackBridgeManager(keyStore: CertificateStore, private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { logDebugWithMDC { "Loopback Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" } val peerInbox = translateLocalQueueToInboxAddress(queueName) - producer?.send(SimpleString(peerInbox), artemisMessage) { artemisMessage.individualAcknowledge() } + producer?.send(SimpleString.of(peerInbox), artemisMessage) { artemisMessage.individualAcknowledge() } bridgeMetricsService?.let { metricsService -> val properties = ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.mapNotNull { key -> if (artemisMessage.containsProperty(key)) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RoundTripObservableSerializerTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RoundTripObservableSerializerTests.kt index df0383d642..e7a89bf0a3 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RoundTripObservableSerializerTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RoundTripObservableSerializerTests.kt @@ -70,7 +70,7 @@ class RoundTripObservableSerializerTests { subscriptionMap(id), clientAddressToObservables = ConcurrentHashMap(), deduplicationIdentity = "thisIsATest", - clientAddress = SimpleString("clientAddress")) + clientAddress = SimpleString.of("clientAddress")) val serverSerializer = serializationScheme.rpcServerSerializerFactory(serverObservableContext) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RpcServerObservableSerializerTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RpcServerObservableSerializerTests.kt index b48a8b1d0e..2f0025f569 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RpcServerObservableSerializerTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/RpcServerObservableSerializerTests.kt @@ -49,7 +49,7 @@ class RpcServerObservableSerializerTests { subscriptionMap(), clientAddressToObservables = ConcurrentHashMap(), deduplicationIdentity = "thisIsATest", - clientAddress = SimpleString("clientAddress")) + clientAddress = SimpleString.of("clientAddress")) val newContext = RpcServerObservableSerializer.createContext(serializationContext, observable) @@ -65,7 +65,7 @@ class RpcServerObservableSerializerTests { subscriptionMap(), clientAddressToObservables = ConcurrentHashMap(), deduplicationIdentity = "thisIsATest", - clientAddress = SimpleString(testClientAddress)) + clientAddress = SimpleString.of(testClientAddress)) val sf = SerializerFactoryBuilder.build(AllWhitelist, javaClass.classLoader).apply { register(RpcServerObservableSerializer()) diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt new file mode 100644 index 0000000000..299e71c52f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt @@ -0,0 +1,135 @@ +package net.corda.node + +import net.corda.core.crypto.sha256 +import net.corda.core.internal.hash +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.GBP +import net.corda.finance.POUNDS +import net.corda.finance.USD +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.workflows.getCashBalance +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey +import net.corda.testing.core.internal.SelfCleaningDir +import net.corda.testing.node.MockNetworkNotarySpec +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.MockNodeArgs +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.cordappWithPackages +import net.corda.testing.node.internal.startFlow +import org.apache.commons.io.FileUtils.deleteDirectory +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import kotlin.io.path.div +import kotlin.test.assertEquals + +class ContractWithRotatedKeyTest { + private val ref = OpaqueBytes.of(0x01) + + private val TestStartedNode.party get() = info.legalIdentities.first() + + private lateinit var mockNet: InternalMockNetwork + + @Before + fun setup() { + mockNet = InternalMockNetwork(initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = 8), notarySpecs = listOf(MockNetworkNotarySpec( + DUMMY_NOTARY_NAME, + validating = false + ))) + } + + @After + fun shutdown() { + mockNet.stopNodes() + } + + private fun restartNodeAndDeleteOldCorDapps(network: InternalMockNetwork, + node: TestStartedNode, + parameters: InternalMockNodeParameters = InternalMockNodeParameters(), + nodeFactory: (MockNodeArgs) -> InternalMockNetwork.MockNode = network.defaultFactory + ): TestStartedNode { + node.internals.disableDBCloseOnStop() + node.dispose() + val cordappsDir = network.baseDirectory(node) / "cordapps" + deleteDirectory(cordappsDir.toFile()) + return network.createNode( + parameters.copy(legalName = node.internals.configuration.myLegalName, forcedID = node.internals.id), + nodeFactory + ) + } + + @Test(timeout = 300_000) + fun `cordapp with rotated key continues to transact`() { + val keyStoreDir1 = SelfCleaningDir() + val keyStoreDir2 = SelfCleaningDir() + + val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="1-testcordapp-rsa") + val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="1-testcordapp-rsa") + + val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services") + val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2) + + val signedFinanceCorDapp1 = unsignedFinanceCorDapp1.signed( keyStoreDir1.path ) + val signedFinanceCorDapp2 = unsignedFinanceCorDapp2.signed( keyStoreDir2.path ) + + val configOverrides = { conf: NodeConfiguration -> + val rotatedKeys = listOf(RotatedCorDappSignerKeyConfiguration(listOf(packageOwnerKey1.hash.sha256().toString(), packageOwnerKey2.hash.sha256().toString()))) + doReturn(rotatedKeys).whenever(conf).rotatedCordappSignerKeys + } + + val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = listOf(signedFinanceCorDapp1), configOverrides = configOverrides)) + val bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = listOf(signedFinanceCorDapp1), configOverrides = configOverrides)) + + val flow1 = alice.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + val flow2 = alice.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity)) + val flow3 = bob.services.startFlow(CashIssueAndPaymentFlow(300.POUNDS, ref, bob.party, false, mockNet.defaultNotaryIdentity)) + val flow4 = bob.services.startFlow(CashIssueAndPaymentFlow(1000.POUNDS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + flow1.resultFuture.getOrThrow() + flow2.resultFuture.getOrThrow() + flow3.resultFuture.getOrThrow() + flow4.resultFuture.getOrThrow() + + val alice2 = restartNodeAndDeleteOldCorDapps(mockNet, alice, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2), configOverrides = configOverrides)) + val bob2 = restartNodeAndDeleteOldCorDapps(mockNet, bob, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2), configOverrides = configOverrides)) + + assertEquals(alice.party, alice2.party) + assertEquals(bob.party, bob2.party) + assertEquals(alice2.party, alice2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + assertEquals(bob2.party, alice2.services.identityService.wellKnownPartyFromX500Name(BOB_NAME)) + assertEquals(alice2.party, bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) + assertEquals(bob2.party, bob2.services.identityService.wellKnownPartyFromX500Name(BOB_NAME)) + + val flow5 = alice2.services.startFlow(CashPaymentFlow(300.DOLLARS, bob2.party, false)) + val flow6 = bob2.services.startFlow(CashPaymentFlow(300.POUNDS, alice2.party, false)) + mockNet.runNetwork() + val flow7 = bob2.services.startFlow(CashPaymentFlow(1300.DOLLARS, alice2.party, false)) + val flow8 = alice2.services.startFlow(CashPaymentFlow(1300.POUNDS, bob2.party, false)) + mockNet.runNetwork() + + flow5.resultFuture.getOrThrow() + flow6.resultFuture.getOrThrow() + flow7.resultFuture.getOrThrow() + flow8.resultFuture.getOrThrow() + + assertEquals(1300.DOLLARS, alice2.services.getCashBalance(USD)) + assertEquals(0.POUNDS, alice2.services.getCashBalance(GBP)) + assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) + assertEquals(1300.POUNDS, bob2.services.getCashBalance(GBP)) + + keyStoreDir1.close() + keyStoreDir2.close() + } +} \ No newline at end of file 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 859e3cdf95..3e1fb603e0 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 @@ -62,7 +62,7 @@ class AMQPBridgeTest { putIntProperty(P2PMessagingHeaders.senderUUID, i) writeBodyBufferBytes("Test$i".toByteArray()) // Use the magic deduplication property built into Artemis as our message identity too - putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) + putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString.of(UUID.randomUUID().toString())) } artemis.producer.send(sourceQueueName, artemisMessage) } @@ -139,7 +139,7 @@ class AMQPBridgeTest { putIntProperty(P2PMessagingHeaders.senderUUID, 3) writeBodyBufferBytes("Test3".toByteArray()) // Use the magic deduplication property built into Artemis as our message identity too - putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString())) + putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString.of(UUID.randomUUID().toString())) } artemis.producer.send(sourceQueueName, artemisMessage) @@ -224,7 +224,7 @@ class AMQPBridgeTest { if (sourceQueueName != null) { // Local queue for outgoing messages artemis.session.createQueue( - QueueConfiguration(sourceQueueName).setRoutingType(RoutingType.ANYCAST).setAddress(sourceQueueName).setDurable(true)) + QueueConfiguration.of(sourceQueueName).setRoutingType(RoutingType.ANYCAST).setAddress(sourceQueueName).setDurable(true)) bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(bob.name)) } return Triple(artemisServer, artemisClient, bridgeManager) 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 3970c13add..6536742a25 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 @@ -499,7 +499,7 @@ class ArtemisServerRevocationTest : AbstractServerRevocationTest() { val queueName = "${P2P_PREFIX}Test" artemisNode.client.started!!.session.createQueue( - QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName).setDurable(true) + QueueConfiguration.of(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName).setDurable(true) ) val clientConnectionChangeStatus = client.waitForInitialConnectionAndCaptureChanges(expectedConnectedStatus) 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 6b573fe2d3..4ff3a1b26d 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 @@ -374,7 +374,7 @@ class ProtonWrapperTests { assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) val artemis = artemisClient.started!! val sendAddress = P2P_PREFIX + "Test" - artemis.session.createQueue(QueueConfiguration("queue") + artemis.session.createQueue(QueueConfiguration.of("queue") .setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true)) val consumer = artemis.session.createConsumer("queue") val testData = "Test".toByteArray() @@ -404,7 +404,7 @@ class ProtonWrapperTests { assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) val artemis = artemisClient.started!! val sendAddress = P2P_PREFIX + "Test" - artemis.session.createQueue(QueueConfiguration("queue") + artemis.session.createQueue(QueueConfiguration.of("queue") .setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true)) val consumer = artemis.session.createConsumer("queue") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt index 150af49ff1..0506cc0c7f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt @@ -1,13 +1,30 @@ package net.corda.node.services.identity -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.whenever +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.builder +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.workflows.getCashBalance import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DevIdentityGenerator @@ -24,13 +41,19 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.startFlow import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import java.util.Currency import kotlin.io.path.createDirectories import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull @RunWith(Parameterized::class) class NotaryCertificateRotationTest(private val validating: Boolean) { @@ -91,8 +114,9 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { val bob2 = mockNet.restartNode(bob) val charlie = mockNet.createPartyNode(CHARLIE_NAME) - // Save previous network parameters for subsequent backchain verification. - mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) } + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) // Verify that notary identity has been changed. assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) @@ -126,4 +150,116 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD)) } + + @Test(timeout = 300_000) + fun `rotate notary identity and new node receives netparams and understands old notary`() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating)), + initialNetworkParameters = testNetworkParameters() + ) + val alice = mockNet.createPartyNode(ALICE_NAME) + val bob = mockNet.createPartyNode(BOB_NAME) + + // Issue states and notarize them with initial notary identity. + alice.services.startFlow(CashIssueFlow(1000.DOLLARS, ref, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(2000.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(4000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + + val oldHash = alice.services.networkParametersService.currentHash + + // Rotate notary identity and update network parameters. + val newNotaryIdentity = DevIdentityGenerator.installKeyStoreWithNodeIdentity( + mockNet.baseDirectory(mockNet.nextNodeId), + DUMMY_NOTARY_NAME + ) + val newNetworkParameters = testNetworkParameters(epoch = 2) + .addNotary(mockNet.defaultNotaryIdentity, validating) + .addNotary(newNotaryIdentity, validating) + val ca = createDevNetworkMapCa() + NetworkParametersCopier(newNetworkParameters, ca, overwriteFile = true).apply { + install(mockNet.baseDirectory(alice)) + install(mockNet.baseDirectory(bob)) + install(mockNet.baseDirectory(mockNet.nextNodeId)) + install(mockNet.baseDirectory(mockNet.nextNodeId + 1).apply { createDirectories() }) + } + + // Start notary with new identity and restart nodes. + mockNet.createNode(InternalMockNodeParameters( + legalName = DUMMY_NOTARY_NAME, + configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary } + )) + val alice2 = mockNet.restartNode(alice) + val bob2 = mockNet.restartNode(bob) + // We hide the old notary as trying to simulate it's replacement + mockNet.hideNode(mockNet.defaultNotaryNode) + val charlie = mockNet.createPartyNode(CHARLIE_NAME) + + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + + assertNotNull(alice2.services.networkParametersService.lookup(oldHash)) + assertNotNull(bob2.services.networkParametersService.lookup(oldHash)) + assertNull(charlie.services.networkParametersService.lookup(oldHash)) + + // Verify that notary identity has been changed. + assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), bob2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), charlie.services.networkMapCache.notaryIdentities) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + + // Now send an existing transaction on Bob (from before rotation) to Charlie + val bobVault: Vault.Page = bob2.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, bobVault.states.size) + val handle = bob2.services.startFlow(RpcSendTransactionFlow(bobVault.states[0].ref.txhash, charlie.party)) + mockNet.runNetwork() + // Check flow completed successfully + assertEquals(handle.resultFuture.getOrThrow(), Unit) + + // Check Charlie recorded it in the vault (could resolve notary, for example) + val charlieVault: Vault.Page = charlie.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, charlieVault.states.size) + + // Check Charlie gained the network parameters from before the rotation + assertNotNull(charlie.services.networkParametersService.lookup(oldHash)) + + // We unhide the old notary so it can be shutdown + mockNet.unhideNode(mockNet.defaultNotaryNode) + } + + private fun generateCashCriteria(currency: Currency): QueryCriteria { + val stateCriteria = QueryCriteria.FungibleAssetQueryCriteria() + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } + // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, relevancyStatus = Vault.RelevancyStatus.ALL) + return stateCriteria.and(ccyCriteria) + } + + @StartableByRPC + @InitiatingFlow + class RpcSendTransactionFlow(private val tx: SecureHash, private val party: Party) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(party) + val stx: SignedTransaction = serviceHub.validatedTransactions.getTransaction(tx)!! + subFlow(SendTransactionFlow(session, stx)) + } + } + + @InitiatedBy(RpcSendTransactionFlow::class) + class RpcSendTransactionResponderFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveTransactionFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } + } } 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 3b012b7672..5290d4fd08 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 @@ -117,7 +117,7 @@ abstract class MQSecurityTest : NodeBasedTest() { fun loginToRPCAndGetClientQueue(): String { loginToRPC(alice.node.configuration.rpcOptions.address, rpcUser) - val clientQueueQuery = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.${rpcUser.username}.*") + val clientQueueQuery = SimpleString.of("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.${rpcUser.username}.*") val client = clientTo(alice.node.configuration.rpcOptions.address) client.start(rpcUser.username, rpcUser.password, false) return client.session.addressQuery(clientQueueQuery).queueNames.single().toString() @@ -131,7 +131,7 @@ abstract class MQSecurityTest : NodeBasedTest() { fun assertTempQueueCreationAttackFails(queue: String) { assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") { - attacker.session.createQueue(QueueConfiguration(queue) + attacker.session.createQueue(QueueConfiguration.of(queue) .setRoutingType(RoutingType.MULTICAST) .setAddress(queue) .setTemporary(true) @@ -153,7 +153,7 @@ abstract class MQSecurityTest : NodeBasedTest() { val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE" assertAttackFails(queue, permission) { attacker.session.createQueue( - QueueConfiguration(queue).setAddress(queue).setRoutingType(RoutingType.MULTICAST).setDurable(durable)) + QueueConfiguration.of(queue).setAddress(queue).setRoutingType(RoutingType.MULTICAST).setDurable(durable)) } // Double-check assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy { 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 629f21a8aa..19d237e0b0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -12,6 +12,7 @@ import net.corda.confidential.SwapIdentitiesFlow import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom @@ -246,7 +247,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private val notaryLoader = configuration.notary?.let { NotaryLoader(it, versionInfo) } - val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo).closeOnStop(false) + val rotatedKeys = makeRotatedKeysService(configuration).tokenize() + val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo, rotatedKeys).closeOnStop(false) val telemetryService: TelemetryServiceImpl = TelemetryServiceImpl().also { val openTelemetryComponent = OpenTelemetryComponent(configuration.myLegalName.toString(), configuration.telemetry.spanStartEndEventsEnabled, configuration.telemetry.copyBaggageToTags) if (configuration.telemetry.openTelemetryEnabled && openTelemetryComponent.isEnabled()) { @@ -290,7 +292,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, database, configuration.devMode ).tokenize() - val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database) + val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database, rotatedKeys) @Suppress("LeakingThis") val networkParametersStorage = makeNetworkParametersStorage() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @@ -303,7 +305,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions? val networkMapUpdater = makeNetworkMapUpdater() - private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory, rotatedKeys).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @Suppress("LeakingThis") @@ -842,7 +844,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, unfinishedSchedules = busyNodeLatch ).tokenize() - private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader { + private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo, rotatedKeys: RotatedKeys): CordappLoader { val generatedCordapps = mutableListOf(VirtualCordapp.generateCore(versionInfo)) notaryLoader?.builtInNotary?.let { notaryImpl -> generatedCordapps += notaryImpl @@ -858,7 +860,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, (configuration.baseDirectory / LEGACY_CONTRACTS_DIR_NAME).takeIf { it.exists() }, versionInfo, extraCordapps = generatedCordapps, - signerKeyFingerprintBlacklist = blacklistedKeys + signerKeyFingerprintBlacklist = blacklistedKeys, + rotatedKeys = rotatedKeys ) } @@ -873,9 +876,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + private fun makeRotatedKeysService( configuration: NodeConfiguration ): RotatedKeys { + return RotatedKeys(configuration.rotatedCordappSignerKeys.map { rotatedKeysConfiguration -> + parseSecureHashConfiguration(rotatedKeysConfiguration.rotatedKeys) { "Error while parsing rotated keys $it"} + }.toList()) + } + private fun makeAttachmentTrustCalculator( configuration: NodeConfiguration, - database: CordaPersistence + database: CordaPersistence, + rotatedKeys: RotatedKeys ): AttachmentTrustCalculator { val blacklistedAttachmentSigningKeys: List = parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" } @@ -883,7 +893,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, attachmentStorage = attachments, database = database, cacheFactory = cacheFactory, - blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys + blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys, + rotatedKeys = rotatedKeys ).tokenize() } @@ -1192,6 +1203,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor override val notaryService: NotaryService? get() = this@AbstractNode.notaryService override val telemetryService: TelemetryService get() = this@AbstractNode.telemetryService + override val rotatedKeys: RotatedKeys get() = this@AbstractNode.rotatedKeys private lateinit var _myInfo: NodeInfo override val myInfo: NodeInfo get() = _myInfo diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 6bcdf1d577..17c490fc50 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -7,6 +7,7 @@ import io.github.classgraph.ScanResult import net.corda.common.logging.errorReporting.CordappErrors import net.corda.common.logging.errorReporting.ErrorCode import net.corda.core.CordaRuntimeException +import net.corda.core.contracts.RotatedKeys import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -72,12 +73,13 @@ import kotlin.reflect.KProperty1 * @property cordappJars The classpath of cordapp JARs * @property legacyContractJars Legacy contract CorDapps (4.11 or earlier) needed for backwards compatibility with 4.11 nodes. */ -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") class JarScanningCordappLoader(private val cordappJars: Set, private val legacyContractJars: Set = emptySet(), private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, private val extraCordapps: List = emptyList(), - private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoader { + private val signerKeyFingerprintBlacklist: List = emptyList(), + private val rotatedKeys: RotatedKeys = RotatedKeys()) : CordappLoader { companion object { private val logger = contextLogger() @@ -93,14 +95,15 @@ class JarScanningCordappLoader(private val cordappJars: Set, legacyContractsDir: Path? = null, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), - signerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { + signerKeyFingerprintBlacklist: List = emptyList(), + rotatedKeys: RotatedKeys = RotatedKeys()): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${cordappDirs.toSet().joinToString(", ", "[", "]")}") val cordappJars = cordappDirs .asSequence() .flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() } .toSet() val legacyContractJars = legacyContractsDir?.useDirectoryEntries("*.jar") { it.toSet() } ?: emptySet() - return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) + return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist, rotatedKeys) } } @@ -217,7 +220,7 @@ class JarScanningCordappLoader(private val cordappJars: Set, private fun checkSignersMatch(legacyCordapp: CordappImpl, nonLegacyCordapp: CordappImpl) { val legacySigners = legacyCordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectSigners) val nonLegacySigners = nonLegacyCordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectSigners) - check(legacySigners == nonLegacySigners) { + check(rotatedKeys.canBeTransitioned(legacySigners, nonLegacySigners)) { "Newer contract CorDapp '${nonLegacyCordapp.jarFile}' signers do not match legacy contract CorDapp " + "'${legacyCordapp.jarFile}' signers." } diff --git a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt index 285f7fca5a..4099fb9b76 100644 --- a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt +++ b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt @@ -2,6 +2,8 @@ package net.corda.node.services.attachments import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.CordaRotatedKeys +import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.SecureHash import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.AttachmentTrustCalculator @@ -30,15 +32,17 @@ class NodeAttachmentTrustCalculator( private val attachmentStorage: AttachmentStorageInternal, private val database: CordaPersistence?, cacheFactory: NamedCacheFactory, - private val blacklistedAttachmentSigningKeys: List = emptyList() + private val blacklistedAttachmentSigningKeys: List = emptyList(), + private val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys ) : AttachmentTrustCalculator, SingletonSerializeAsToken() { @VisibleForTesting constructor( - attachmentStorage: AttachmentStorageInternal, - cacheFactory: NamedCacheFactory, - blacklistedAttachmentSigningKeys: List = emptyList() - ) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys) + attachmentStorage: AttachmentStorageInternal, + cacheFactory: NamedCacheFactory, + blacklistedAttachmentSigningKeys: List = emptyList(), + rotatedKeys: RotatedKeys = CordaRotatedKeys.keys + ) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys, rotatedKeys) // A cache for caching whether a signing key is trusted private val trustedKeysCache = cacheFactory.buildNamed("NodeAttachmentTrustCalculator_trustedKeysCache") @@ -55,11 +59,33 @@ class NodeAttachmentTrustCalculator( signersCondition = Builder.equal(listOf(signer)), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS) ) - attachmentStorage.queryAttachments(queryCriteria).isNotEmpty() + (attachmentStorage.queryAttachments(queryCriteria).isNotEmpty() || + calculateTrustUsingRotatedKeys(signer)) }!! } } + private fun calculateTrustUsingRotatedKeys(signer: PublicKey): Boolean { + val db = checkNotNull(database) { + // This should never be hit, except for tests that have not been setup correctly to test internal code + "CordaPersistence has not been set" + } + return db.transaction { + getTrustedAttachments().use { trustedAttachments -> + for ((_, trustedAttachmentFromDB) in trustedAttachments) { + if (canTrustedAttachmentAndAttachmentSignerBeTransitioned(trustedAttachmentFromDB, signer)) { + return@transaction true + } + } + } + return@transaction false + } + } + + private fun canTrustedAttachmentAndAttachmentSignerBeTransitioned(trustedAttachmentFromDB: Attachment, signer: PublicKey): Boolean { + return trustedAttachmentFromDB.signerKeys.any { signerKeyFromDB -> rotatedKeys.canBeTransitioned(signerKeyFromDB, signer) } + } + override fun calculateAllTrustInfo(): List { val publicKeyToTrustRootMap = mutableMapOf() 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 02d3695995..7fa506d885 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 @@ -86,6 +86,13 @@ interface NodeConfiguration : ConfigurationWithOptionsContainer { val cordappSignerKeyFingerprintBlacklist: List + /** + * Represents a list of rotated CorDapp attachment JAR signing key configurations. Each configuration describes a set of equivalent + * keys. Logically there should be no overlap between configurations, since that would mean they should be one combined list, + * and this is enforced. + */ + val rotatedCordappSignerKeys: List + val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? val networkParametersPath: Path @@ -222,6 +229,13 @@ data class TelemetryConfiguration( val copyBaggageToTags: Boolean ) +/** + * Represents a list of rotated CorDapp attachment signing keys. + * + * @param rotatedKeys This is a list of public key hashes (SHA-256) in uppercase hexidecimal, that are all equivalent. + */ +data class RotatedCorDappSignerKeyConfiguration(val rotatedKeys: List) + internal typealias Valid = Validated fun Config.parseAsNodeConfiguration(options: Configuration.Options = Configuration.Options(strict = true)): Valid = V1NodeConfigurationSpec.parse(this, options) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index 39abe4b4c7..f02abcdb70 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -80,6 +80,7 @@ data class NodeConfigurationImpl( override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType, override val flowOverrides: FlowOverrideConfig?, override val cordappSignerKeyFingerprintBlacklist: List = Defaults.cordappSignerKeyFingerprintBlacklist, + override val rotatedCordappSignerKeys: List = Defaults.rotatedCordappSignerKeys, override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? = Defaults.networkParameterAcceptanceSettings, override val blacklistedAttachmentSigningKeys: List = Defaults.blacklistedAttachmentSigningKeys, @@ -122,6 +123,7 @@ data class NodeConfigurationImpl( val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() } + val rotatedCordappSignerKeys: List = emptyList() val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings() val blacklistedAttachmentSigningKeys: List = emptyList() const val flowExternalOperationThreadPoolSize: Int = 1 diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt index 6aeb54b1b1..a474ebb3d5 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt @@ -27,6 +27,7 @@ import net.corda.node.services.config.NodeH2Settings import net.corda.node.services.config.NodeRpcSettings import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.PasswordEncryption +import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId import net.corda.node.services.config.TelemetryConfiguration @@ -225,6 +226,14 @@ internal object TelemetryConfigurationSpec : Configuration.Specification("RotatedCorDappSignerKeyConfiguration") { + private val rotatedKeys by string().listOrEmpty() + override fun parseValid(configuration: Config, options: Configuration.Options): Valid { + val config = configuration.withOptions(options) + return valid(RotatedCorDappSignerKeyConfiguration(config[rotatedKeys])) + } +} + internal object NotaryConfigSpec : Configuration.Specification("NotaryConfig") { private val validating by boolean() private val serviceLegalName by string().mapValid(::toCordaX500Name).optional() diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt index 2c06a0e844..87e4153af7 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt @@ -60,6 +60,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification) { val bridgeNotifyQueue = "$BRIDGE_NOTIFY.${myIdentity.toStringShort()}" - if (!session.queueQuery(SimpleString(bridgeNotifyQueue)).isExists) { - session.createQueue(QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) + if (!session.queueQuery(SimpleString.of(bridgeNotifyQueue)).isExists) { + session.createQueue(QueueConfiguration.of(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST) .setTemporary(true).setDurable(false)) } val bridgeConsumer = session.createConsumer(bridgeNotifyQueue) @@ -316,7 +316,7 @@ class P2PMessagingClient(val config: NodeConfiguration, node.legalIdentitiesAndCerts.map { partyAndCertificate -> val messagingAddress = NodeAddress(partyAndCertificate.party.owningKey) BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false) - }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() + }.filter { producerSession!!.queueQuery(SimpleString.of(it.queueName)).isExists }.asSequence() } } @@ -360,7 +360,7 @@ class P2PMessagingClient(val config: NodeConfiguration, } } - val queues = session.addressQuery(SimpleString("$PEERS_PREFIX#")).queueNames + val queues = session.addressQuery(SimpleString.of("$PEERS_PREFIX#")).queueNames knownQueues.clear() for (queue in queues) { val queueQuery = session.queueQuery(queue) @@ -604,10 +604,10 @@ class P2PMessagingClient(val config: NodeConfiguration, sendBridgeCreateMessage() delayStartQueues -= queueName } else { - val queueQuery = session.queueQuery(SimpleString(queueName)) + val queueQuery = session.queueQuery(SimpleString.of(queueName)) if (!queueQuery.isExists) { log.info("Create fresh queue $queueName bound on same address") - session.createQueue(QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName) + session.createQueue(QueueConfiguration.of(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName) .setDurable(true).setAutoCreated(false) .setMaxConsumers(ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers()) .setPurgeOnNoConsumers(ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers()) diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RPCServer.kt index 9d50bc72d3..20d16d996a 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RPCServer.kt @@ -321,14 +321,14 @@ class RPCServer( require(notificationType == CoreNotificationType.BINDING_REMOVED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_REMOVED.name}"} val clientAddress = artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME) log.info("Detected RPC client disconnect on address $clientAddress, scheduling for reaping") - invalidateClient(SimpleString(clientAddress)) + invalidateClient(SimpleString.of(clientAddress)) } private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) { lifeCycle.requireState(State.STARTED) val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE) require(notificationType == CoreNotificationType.BINDING_ADDED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_ADDED.name}"} - val clientAddress = SimpleString(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)) + val clientAddress = SimpleString.of(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)) log.debug("RPC client queue created on address $clientAddress") val buffer = stopBuffering(clientAddress) 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 14da13763b..66446e76d1 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 @@ -39,7 +39,7 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, queueConfigs = queueConfigurations() - managementNotificationAddress = SimpleString(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS) + managementNotificationAddress = SimpleString.of(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS) addressSettings = mapOf( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply { maxSizeBytes = 5L * maxMessageSize @@ -51,7 +51,7 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, globalMaxSize = Runtime.getRuntime().maxMemory() / 8 initialiseSettings(maxMessageSize, journalBufferTimeout) - val nodeInternalRole = Role(BrokerJaasLoginModule.NODE_RPC_ROLE, true, true, true, true, true, true, true, true, true, true) + val nodeInternalRole = Role(BrokerJaasLoginModule.NODE_RPC_ROLE, true, true, true, true, true, true, true, true, true, true, false, false) val addRPCRoleToUsers = if (shouldStartLocalShell) listOf(INTERNAL_SHELL_USER) else emptyList() val rolesAdderOnLogin = RolesAdderOnLogin(addRPCRoleToUsers) { username -> @@ -127,12 +127,12 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, } private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): QueueConfiguration { - return QueueConfiguration(name).setAddress(address).setFilterString(filter).setDurable(durable) + return QueueConfiguration.of(name).setAddress(address).setFilterString(filter).setDurable(durable) } private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false, deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false, deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role { - return Role(name, send, consume, createDurableQueue, deleteDurableQueue, createNonDurableQueue, deleteNonDurableQueue, manage, browse, createDurableQueue || createNonDurableQueue, deleteDurableQueue || deleteNonDurableQueue) + return Role(name, send, consume, createDurableQueue, deleteDurableQueue, createNonDurableQueue, deleteNonDurableQueue, manage, browse, createDurableQueue || createNonDurableQueue, deleteDurableQueue || deleteNonDurableQueue, false, false) } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt index fd9c1cd91a..211eda6d2d 100644 --- a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt +++ b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt @@ -237,7 +237,8 @@ class ExternalVerifierHandleImpl( customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name }, serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name }, System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization - serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize() + serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize(), + serializedRotatedKeys = verificationSupport.rotatedKeys.serialize() ) channel.writeCordaSerializable(initialisation) } diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index b4bc32760d..c6129d0ca0 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -1,3 +1,6 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 +urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability +requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability +setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index d5a09d413a..07507ae219 100644 --- a/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -1276,7 +1276,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi ) factory2.register(net.corda.serialization.internal.amqp.custom.SimpleStringSerializer) - val obj = SimpleString("Bob") + val obj = SimpleString.of("Bob") serdes(obj, factory, factory2) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt index 7d5344bbd1..414132951e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.verifier import net.corda.core.contracts.Attachment +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toStringShort @@ -23,6 +24,7 @@ import kotlin.math.min import kotlin.reflect.KClass typealias SerializedNetworkParameters = SerializedBytes +typealias SerializedRotatedKeys = SerializedBytes @CordaSerializable sealed class ExternalVerifierInbound { @@ -30,16 +32,19 @@ sealed class ExternalVerifierInbound { val customSerializerClassNames: Set, val serializationWhitelistClassNames: Set, val customSerializationSchemeClassName: String?, - val serializedCurrentNetworkParameters: SerializedNetworkParameters + val serializedCurrentNetworkParameters: SerializedNetworkParameters, + val serializedRotatedKeys: SerializedRotatedKeys ) : ExternalVerifierInbound() { val currentNetworkParameters: NetworkParameters by lazy { serializedCurrentNetworkParameters.deserialize() } + val rotatedKeys: RotatedKeys by lazy { serializedRotatedKeys.deserialize() } override fun toString(): String { return "Initialisation(" + "customSerializerClassNames=$customSerializerClassNames, " + "serializationWhitelistClassNames=$serializationWhitelistClassNames, " + "customSerializationSchemeClassName=$customSerializationSchemeClassName, " + - "currentNetworkParameters=$currentNetworkParameters)" + "currentNetworkParameters=$currentNetworkParameters, " + + "rotatedKeys=$rotatedKeys)" } } diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index 5da6ca0cf9..75ca3d95b1 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -5,6 +5,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' apply plugin: 'corda.api-scanner' +description 'Corda Node Driver module' + //noinspection GroovyAssignabilityCheck configurations { integrationTestImplementation.extendsFrom testImplementation diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index f9c0a14e1b..fa8f9f697b 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -140,16 +140,42 @@ class DriverTests { } } + /** + * The uniqueness of nodes by the DSL is checked using the node organisation name and, if specified, + * the organisation unit name. + * All other X500 components are ignored in this regard. + */ @Test(timeout=300_000) fun `driver rejects multiple nodes with the same organisation name`() { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { newNode(CordaX500Name(commonName = "Notary", organisation = "R3CEV", locality = "New York", country = "US"))().getOrThrow() assertThatIllegalArgumentException().isThrownBy { - newNode(CordaX500Name(commonName = "Regulator", organisation = "R3CEV", locality = "New York", country = "US"))().getOrThrow() + newNode(CordaX500Name(commonName = "Regulator", organisation = "R3CEV", locality = "Newcastle", country = "GB"))().getOrThrow() } } } + @Test(timeout=300_000) + fun `driver allows multiple nodes with the same organisation name but different organisation unit name`() { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + newNode(CordaX500Name(commonName = "Notary", organisation = "R3CEV", organisationUnit = "Eric", locality = "New York", country = "US", state = null))().getOrThrow() + assertThatCode { + newNode(CordaX500Name(commonName = "Regulator", organisation = "R3CEV", organisationUnit = "Ernie", locality = "Newcastle", country = "GB", state = null))().getOrThrow() + }.doesNotThrowAnyException() + } + } + + @Test(timeout=300_000) + fun `driver rejects multiple nodes with the same organisation name and organisation unit name`() { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + newNode(CordaX500Name(commonName = "Notary", organisation = "R3CEV", organisationUnit = "Eric", locality = "New York", country = "US", state = null))().getOrThrow() + assertThatIllegalArgumentException().isThrownBy { + newNode(CordaX500Name(commonName = "Regulator", organisation = "R3CEV", organisationUnit = "Eric", locality = "Newcastle", country = "GB", state = null))().getOrThrow() + } + } + } + /** **** **/ + @Test(timeout=300_000) fun `driver allows reusing names of nodes that have been stopped`() { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 50febd80e1..4967282c78 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -5,6 +5,7 @@ import net.corda.core.CordaInternal import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractState +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState @@ -128,8 +129,8 @@ open class MockServices private constructor( ) ) : ServiceHub { companion object { - private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { - return JarScanningCordappLoader(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo = versionInfo) + private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN, rotatedKeys: RotatedKeys = RotatedKeys()): CordappLoader { + return JarScanningCordappLoader(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo = versionInfo, rotatedKeys = rotatedKeys) } /** @@ -500,6 +501,7 @@ open class MockServices private constructor( protected val servicesForResolution: ServicesForResolution get() = verifyingView private val verifyingView: VerifyingServiceHub by lazy { VerifyingView(this) } + val rotatedKeys: RotatedKeys = RotatedKeys() internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService( @@ -564,10 +566,10 @@ open class MockServices private constructor( private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices { override val attachmentTrustCalculator = NodeAttachmentTrustCalculator( attachmentStorage = mockServices.attachments.toInternal(), - cacheFactory = TestingNamedCacheFactory() + cacheFactory = TestingNamedCacheFactory(), rotatedKeys = mockServices.rotatedKeys ) - override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory(), mockServices.rotatedKeys) override val cordappProvider: CordappProviderInternal get() = mockServices.mockCordappProvider @@ -579,6 +581,8 @@ open class MockServices private constructor( override val externalVerifierHandle: ExternalVerifierHandle get() = throw UnsupportedOperationException("`Verification of legacy transactions is not supported by MockServices. Use MockNode instead.") + + override val rotatedKeys: RotatedKeys = mockServices.rotatedKeys } 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 2d7b467d83..366cf98a98 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 @@ -664,7 +664,11 @@ class DriverDSLImpl( } override fun baseDirectory(nodeName: CordaX500Name): Path { - val nodeDirectoryName = nodeName.organisation.filter { !it.isWhitespace() } + val nodeDirectoryName = if (nodeName.organisationUnit != null) { + "${nodeName.organisation}-${nodeName.organisationUnit}" + } else { + nodeName.organisation + }.filter { !it.isWhitespace() } return driverDirectory / nodeDirectoryName } @@ -1142,11 +1146,16 @@ private class NetworkVisibilityController { fun register(name: CordaX500Name): VisibilityHandle { val handle = VisibilityHandle() + val handleName = if (name.organisationUnit != null) { + "${name.organisation}-${name.organisationUnit}" + } else { + name.organisation + } nodeVisibilityHandles.locked { - require(name.organisation !in keys) { - "Node with organisation name ${name.organisation} is already started or starting" + require(handleName !in keys) { + "Node with the organisation name (+ unit name) \"$handleName\" is already started or starting" } - put(name.organisation, handle) + put(handleName, handle) } return handle } 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 3ff7b5c363..1c508d2edd 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 @@ -42,6 +42,7 @@ import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration import net.corda.node.services.config.TelemetryConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.identity.PersistentIdentityService @@ -499,6 +500,18 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), return node } + fun hideNode( + node: TestStartedNode + ) { + _nodes.remove(node.internals) + } + + fun unhideNode( + node: TestStartedNode + ) { + _nodes.add(node.internals) + } + fun restartNode( node: TestStartedNode, parameters: InternalMockNodeParameters = InternalMockNodeParameters(), @@ -658,6 +671,7 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio doReturn(rigorousMock()).whenever(it).configurationWithOptions doReturn(2).whenever(it).flowExternalOperationThreadPoolSize doReturn(false).whenever(it).reloadCheckpointAfterSuspend + doReturn(emptyList()).whenever(it).rotatedCordappSignerKeys } } 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 0abc936e6d..d4aaf87130 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 @@ -192,16 +192,16 @@ data class RPCDriverDSL( private fun ConfigurationImpl.configureCommonSettings(maxFileSize: Int, maxBufferedBytesPerClient: Long) { name = "RPCDriver" - managementNotificationAddress = SimpleString(NOTIFICATION_ADDRESS) + managementNotificationAddress = SimpleString.of(NOTIFICATION_ADDRESS) isPopulateValidatedUser = true journalBufferSize_NIO = maxFileSize journalBufferSize_AIO = maxFileSize journalFileSize = maxFileSize queueConfigs = listOf( - QueueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME).setAddress(RPCApi.RPC_SERVER_QUEUE_NAME).setDurable(false), - QueueConfiguration(RPCApi.RPC_CLIENT_BINDING_REMOVALS).setAddress(NOTIFICATION_ADDRESS) + QueueConfiguration.of(RPCApi.RPC_SERVER_QUEUE_NAME).setAddress(RPCApi.RPC_SERVER_QUEUE_NAME).setDurable(false), + QueueConfiguration.of(RPCApi.RPC_CLIENT_BINDING_REMOVALS).setAddress(NOTIFICATION_ADDRESS) .setFilterString(RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION).setDurable(false), - QueueConfiguration(RPCApi.RPC_CLIENT_BINDING_ADDITIONS).setAddress(NOTIFICATION_ADDRESS) + QueueConfiguration.of(RPCApi.RPC_CLIENT_BINDING_ADDITIONS).setAddress(NOTIFICATION_ADDRESS) .setFilterString(RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION).setDurable(false) ) addressSettings = mapOf( diff --git a/testing/test-common/build.gradle b/testing/test-common/build.gradle index 243327dbe9..47f5b8254b 100644 --- a/testing/test-common/build.gradle +++ b/testing/test-common/build.gradle @@ -3,6 +3,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' apply plugin: 'corda.api-scanner' +description 'Corda Test Common module' + dependencies { implementation project(':core') implementation project(':node-api') diff --git a/testing/test-db/build.gradle b/testing/test-db/build.gradle index 39028cb818..09bb75120c 100644 --- a/testing/test-db/build.gradle +++ b/testing/test-db/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' apply plugin: 'corda.api-scanner' +description 'Corda test-db module' + dependencies { implementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 3280a456c8..f7002629d2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -14,6 +14,7 @@ import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.verification.ExternalVerifierHandle +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord @@ -108,11 +109,13 @@ data class TestTransactionDSLInterpreter private constructor( ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build() ) + override val rotatedKeys: RotatedKeys = ledgerInterpreter.services.toVerifyingServiceHub().rotatedKeys + override val attachmentTrustCalculator: AttachmentTrustCalculator = ledgerInterpreter.services.attachments.let { // Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api // while still allowing the tests to work - NodeAttachmentTrustCalculator(attachmentStorage = it.toInternal(), cacheFactory = TestingNamedCacheFactory()) + NodeAttachmentTrustCalculator(attachmentStorage = it.toInternal(), cacheFactory = TestingNamedCacheFactory(), rotatedKeys = rotatedKeys) } override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle index 07a9eaea4c..1e34fcd8e3 100644 --- a/tools/blobinspector/build.gradle +++ b/tools/blobinspector/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'corda.common-publishing' +description 'Corda blob inspector module' + dependencies { implementation project(':core') implementation project(':serialization') diff --git a/tools/network-builder/build.gradle b/tools/network-builder/build.gradle index d587af84ee..3a22036a10 100644 --- a/tools/network-builder/build.gradle +++ b/tools/network-builder/build.gradle @@ -4,6 +4,8 @@ plugins { id 'corda.common-publishing' } +description 'Corda Network Builder module' + apply plugin: 'org.openjfx.javafxplugin' javafx { diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt index c410564c0a..de2104f622 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt @@ -1,6 +1,7 @@ package net.corda.verifier import net.corda.core.contracts.Attachment +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party @@ -14,7 +15,8 @@ class ExternalVerificationContext( override val appClassLoader: ClassLoader, override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache, private val externalVerifier: ExternalVerifier, - private val transactionInputsAndReferences: Map + private val transactionInputsAndReferences: Map, + override val rotatedKeys: RotatedKeys ) : VerificationSupport { override val isInProcess: Boolean get() = false diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt index 398266f33f..d2537c87ad 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt @@ -2,6 +2,7 @@ package net.corda.verifier import com.github.benmanes.caffeine.cache.Cache import net.corda.core.contracts.Attachment +import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.internal.loadClassOfType @@ -70,6 +71,7 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc private lateinit var appClassLoader: ClassLoader private lateinit var currentNetworkParameters: NetworkParameters + private lateinit var rotatedKeys: RotatedKeys init { val cacheFactory = ExternalVerifierNamedCacheFactory() @@ -117,7 +119,7 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc currentNetworkParameters = initialisation.currentNetworkParameters networkParametersMap.put(initialisation.serializedCurrentNetworkParameters.hash, Optional.of(currentNetworkParameters)) - + rotatedKeys = initialisation.rotatedKeys log.info("External verifier initialised") } @@ -132,7 +134,8 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc @Suppress("INVISIBLE_MEMBER") private fun verifyTransaction(request: VerificationRequest) { - val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.ctxInputsAndReferences) + val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, + request.ctxInputsAndReferences, rotatedKeys) val result: Try = try { val ctx = request.ctx when (ctx) {