From 6f4ec5d9e5ee3e6a615d7ab7677a5ce5a470565b Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:53:11 +0100 Subject: [PATCH] ENT-11975: Contract key rotation (#7806) ENT-11975: Contract key rotation implementation. --- core-tests/build.gradle | 1 + .../contracts/ConstraintsPropagationTests.kt | 44 +-- .../coretests/contracts/RotatedKeysTest.kt | 279 ++++++++++++++++++ .../AttachmentsClassLoaderTests.kt | 137 +-------- ...sClassLoaderWithStoragePersistenceTests.kt | 237 +++++++++++++++ .../net/corda/core/contracts/RotatedKeys.kt | 117 ++++++++ .../corda/core/internal/ConstraintsUtils.kt | 5 +- .../verification/VerificationSupport.kt | 3 + .../core/internal/verification/Verifier.kt | 24 +- .../internal/AttachmentsClassLoader.kt | 17 +- .../core/transactions/LedgerTransaction.kt | 13 +- .../core/transactions/TransactionBuilder.kt | 27 +- .../core/transactions/WireTransaction.kt | 2 + .../corda/node/ContractWithRotatedKeyTest.kt | 135 +++++++++ .../net/corda/node/internal/AbstractNode.kt | 26 +- .../cordapp/JarScanningCordappLoader.kt | 13 +- .../NodeAttachmentTrustCalculator.kt | 38 ++- .../node/services/config/NodeConfiguration.kt | 14 + .../services/config/NodeConfigurationImpl.kt | 2 + .../config/schema/v1/ConfigSections.kt | 9 + .../schema/v1/V1NodeConfigurationSpec.kt | 4 +- .../ExternalVerifierHandleImpl.kt | 3 +- .../verifier/ExternalVerifierTypes.kt | 9 +- .../net/corda/testing/node/MockServices.kt | 12 +- .../node/internal/InternalMockNetwork.kt | 2 + .../kotlin/net/corda/testing/dsl/TestDSL.kt | 5 +- .../verifier/ExternalVerificationContext.kt | 4 +- .../net/corda/verifier/ExternalVerifier.kt | 7 +- 28 files changed, 985 insertions(+), 204 deletions(-) create mode 100644 core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt create mode 100644 core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt create mode 100644 core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt 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/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/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/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/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 +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/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/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index aaadddf5b3..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 @@ -670,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/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/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) {