From 282ee9518862472fb9552932fbcf42e60274e997 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 1/4] ENT-11975: Contract key rotation (#7806) ENT-11975: Contract key rotation implementation. --- .../net/corda/core/contracts/RotatedKeys.kt | 121 ++++++++ core-tests/build.gradle | 1 + .../contracts/ConstraintsPropagationTests.kt | 58 ++-- .../coretests/contracts/RotatedKeysTest.kt | 279 ++++++++++++++++++ .../AttachmentsClassLoaderTests.kt | 137 +-------- ...sClassLoaderWithStoragePersistenceTests.kt | 237 +++++++++++++++ .../net/corda/core/contracts/RotatedKeys.kt | 119 ++++++++ .../corda/core/internal/ConstraintsUtils.kt | 5 +- .../TransactionVerifierServiceInternal.kt | 28 +- .../internal/AttachmentsClassLoader.kt | 21 +- .../core/transactions/LedgerTransaction.kt | 13 +- .../core/transactions/TransactionBuilder.kt | 33 ++- .../core/transactions/WireTransaction.kt | 16 +- .../corda/node/ContractWithRotatedKeyTest.kt | 135 +++++++++ .../net/corda/node/internal/AbstractNode.kt | 26 +- .../cordapp/JarScanningCordappLoader.kt | 9 +- .../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 +- .../net/corda/testing/node/MockServices.kt | 6 +- .../node/internal/InternalMockNetwork.kt | 2 + .../kotlin/net/corda/testing/dsl/TestDSL.kt | 17 +- 24 files changed, 1122 insertions(+), 208 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt 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-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt new file mode 100644 index 0000000000..c3521d16ee --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -0,0 +1,121 @@ +package net.corda.core.contracts + +import net.corda.core.DeleteForDJVM +import net.corda.core.KeepForDJVM +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 + +@DeleteForDJVM +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 +@DeleteForDJVM +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-tests/build.gradle b/core-tests/build.gradle index c5e184e448..bfdda8dfc9 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -50,6 +50,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "junit:junit:$junit_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}" testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_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 0e77bcbed1..42311139ca 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 @@ -1,9 +1,22 @@ package net.corda.coretests.contracts +< import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.contracts.* +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.AutomaticPlaceholderConstraint +import net.corda.core.contracts.BelongsToContract +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 +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash.Companion.allOnesHash @@ -324,52 +337,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 e0f3778418..7b40185f59 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" } @@ -348,141 +348,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( - InternalMockAttachmentStorage(storage), - 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..2ac318d884 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -0,0 +1,119 @@ +package net.corda.core.node.services + +import net.corda.core.KeepForDJVM +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 +@KeepForDJVM +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 2cdb80fad1..78457a900d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -58,7 +58,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") @@ -84,7 +85,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/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index 0171d71e91..e830b49c84 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -4,11 +4,13 @@ import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Attachment +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 @@ -87,10 +89,8 @@ 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. - */ -@KeepForDJVM -private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) { + * wrong object instance. This class helps +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 @@ -375,7 +375,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, @@ -429,8 +429,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) } } } @@ -478,7 +490,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 83de5fe059..a003b544fb 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 @@ -5,6 +5,8 @@ import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.DeleteForDJVM 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 @@ -355,8 +357,9 @@ object AttachmentsClassLoaderBuilder { block: (SerializationContext) -> T): T { val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id) - val cache = attachmentsClassLoaderCache ?: fallBackCache - val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key -> + val cache = if (attachmentsClassLoaderCache is AttachmentsClassLoaderForRotatedKeysOnlyImpl) fallBackCache else + attachmentsClassLoaderCache ?: fallBackCache + val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params)), Function { key -> // Create classloader and load serializers, whitelisted classes val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) val serializers = try { @@ -471,12 +474,11 @@ private class AttachmentsHolderImpl : AttachmentsHolder { } interface AttachmentsClassLoaderCache { + val rotatedKeys: RotatedKeys fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext } -@DeleteForDJVM -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, @@ -527,8 +529,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() @@ -537,6 +538,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 25dfa7f293..097aa4cf96 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -9,7 +9,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 @@ -33,6 +35,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl import net.corda.core.utilities.contextLogger import java.util.Collections.unmodifiableList import java.util.function.Predicate @@ -99,6 +102,8 @@ private constructor( val digestService: DigestService ) : FullTransaction() { + val rotatedKeys = attachmentsClassLoaderCache?.rotatedKeys ?: CordaRotatedKeys.keys + /** * Old version of [LedgerTransaction] constructor for ABI compatibility. */ @@ -198,7 +203,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), @@ -215,7 +221,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. @@ -871,7 +877,8 @@ private class BasicVerifier( 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 c05f378223..220095cd9e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -523,11 +523,13 @@ open class TransactionBuilder( require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." } // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. - val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) + val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName, + inputStates, selectedAttachment.currentAttachment, serviceHub) // Sanity check that the selected attachment actually passes. - val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) - require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } + require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { + "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment" + } val resolvedOutputStates = outputStates.map { val outputConstraint = it.constraint @@ -536,7 +538,9 @@ 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, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } + require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment, serviceHub.toVerifyingServiceHub().rotatedKeys)) { + "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" + } } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } it @@ -546,6 +550,27 @@ open class TransactionBuilder( return Pair(selectedAttachmentId, 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 5ff7cae23e..ecd71dcc3e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -6,7 +6,20 @@ import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP -import net.corda.core.crypto.* +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 +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.node.NetworkParameters @@ -157,6 +170,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr { null }, Attachment::isUploaderTrusted, null + // TODO : elbad01 : does rotated keys need to be passed here, or we use attachment class loader cache ) } 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 3ff93abef5..b897acb2de 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -11,6 +11,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 schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize() val identityService = PersistentIdentityService(cacheFactory).tokenize() val database: CordaPersistence = createCordaPersistence( @@ -276,7 +278,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 cryptoService = makeCryptoService() @Suppress("LeakingThis") @@ -314,7 +316,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } else { BasicVerifierFactoryService() } - 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") @@ -828,7 +830,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 @@ -843,7 +845,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, configuration.cordappDirectories, versionInfo, extraCordapps = generatedCordapps, - signerKeyFingerprintBlacklist = blacklistedKeys + signerKeyFingerprintBlacklist = blacklistedKeys, + rotatedKeys = rotatedKeys ) } @@ -858,9 +861,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" } @@ -868,7 +878,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, attachmentStorage = attachments, database = database, cacheFactory = cacheFactory, - blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys + blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys, + rotatedKeys = rotatedKeys ).tokenize() } @@ -1122,6 +1133,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val diagnosticsService: DiagnosticsService get() = this@AbstractNode.diagnosticsService override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor override val notaryService: NotaryService? get() = this@AbstractNode.notaryService + 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 ef008b252c..9f5cefa3d5 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 @@ -6,6 +6,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 @@ -49,7 +50,8 @@ import kotlin.streams.toList class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List, - private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoaderTemplate() { + private val signerKeyFingerprintBlacklist: List = emptyList(), + private val rotatedKeys: RotatedKeys = RotatedKeys()) : CordappLoaderTemplate() { init { if (cordappJarPaths.isEmpty()) { logger.info("No CorDapp paths provided") @@ -75,10 +77,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: fun fromDirectories(cordappDirs: Collection, 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.distinct().joinToString(", ", "[", "]")}") val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist, rotatedKeys) } /** 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 2305203338..8d77dd1130 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 @@ -3,6 +3,8 @@ package net.corda.node.services.attachments 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.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.node.services.AttachmentId @@ -24,15 +26,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( @@ -52,11 +56,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 21ed0db28b..d366571b74 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 @@ -85,6 +85,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 @@ -220,6 +227,13 @@ data class FlowTimeoutConfiguration( val backoffBase: Double ) +/** + * 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 c6efe102ea..c1a966cd66 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 @@ -78,6 +78,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, @@ -120,6 +121,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 79a9c81dea..c75e2e2ffd 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 @@ -28,6 +28,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.Valid @@ -224,6 +225,14 @@ internal object FlowTimeoutConfigurationSpec : 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 3808597620..cd47349ad3 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, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { - return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo) + private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN, rotatedKeys: RotatedKeys = RotatedKeys()): CordappLoader { + return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo, rotatedKeys = rotatedKeys) } /** @@ -459,6 +460,7 @@ open class MockServices private constructor( protected val servicesForResolution: ServicesForResolution get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) + override val rotatedKeys: RotatedKeys = RotatedKeys() internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService( 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 0de7e5018a..4df37b8258 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 @@ -46,6 +46,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.VerifierType import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService @@ -671,6 +672,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 9a25595d63..2f12079345 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 @@ -10,9 +10,11 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.notary.NotaryService + import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId +import net.corda.core.contracts.RotatedKeys import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl @@ -104,17 +106,20 @@ data class TestTransactionDSLInterpreter private constructor( ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build() ) + override val rotatedKeys: RotatedKeys = ledgerInterpreter.services.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 = if (it is MockAttachmentStorage) { - InternalMockAttachmentStorage(it) - } else { - it as AttachmentStorageInternal - }, - cacheFactory = TestingNamedCacheFactory() + attachmentStorage = if (it is MockAttachmentStorage) { + InternalMockAttachmentStorage(it) + } else { + it as AttachmentStorageInternal + }, + cacheFactory = TestingNamedCacheFactory(), + rotatedKeys = rotatedKeys ) } From 828066a6465a7cc5556ce4f2cd3809f9aa8c65f7 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 9 Oct 2024 14:46:57 +0100 Subject: [PATCH 2/4] Backport contract key rotation to 4.9. --- core-deterministic/build.gradle | 3 +++ .../net/corda/core/contracts/RotatedKeys.kt | 7 +------ .../contracts/ConstraintsPropagationTests.kt | 1 - ...tsClassLoaderWithStoragePersistenceTests.kt | 13 +++++-------- .../net/corda/core/contracts/RotatedKeys.kt | 4 +--- .../corda/core/internal/ConstraintsUtils.kt | 1 + .../TransactionVerifierServiceInternal.kt | 3 ++- .../kotlin/net/corda/core/node/ServiceHub.kt | 6 ++++++ .../internal/AttachmentsClassLoader.kt | 5 ++--- .../core/transactions/TransactionBuilder.kt | 18 ++++++++++++++---- .../corda/core/transactions/WireTransaction.kt | 3 --- .../net/corda/node/djvm/LtxSupplierFactory.kt | 4 +++- .../corda/node/ContractWithRotatedKeyTest.kt | 11 +++++------ .../cordapp/JarScanningCordappLoader.kt | 5 +++-- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 1 - 15 files changed, 46 insertions(+), 39 deletions(-) diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index d2b38682be..f654de6eaf 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -77,6 +77,9 @@ def patchCore = tasks.register('patchCore', Zip) { exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class' exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' exclude 'net/corda/core/internal/rules/*.class' + exclude 'net/corda/core/contracts/CordaRotatedKeys.class' + exclude 'net/corda/core/contracts/RotatedKeysKt.class' + exclude 'net/corda/core/contracts/RotatedKeys.class' } reproducibleFileOrder = true diff --git a/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt index c3521d16ee..8078308f12 100644 --- a/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt +++ b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -1,18 +1,14 @@ package net.corda.core.contracts -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM 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 -@DeleteForDJVM object CordaRotatedKeys { val keys = RotatedKeys() } @@ -47,8 +43,7 @@ private val CORDA_SIGNING_KEY_ROTATIONS = listOf( * */ @CordaSerializable -@DeleteForDJVM -data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()): SingletonSerializeAsToken() { +data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()) { private val canBeTransitionedMap: ConcurrentMap, Boolean> = ConcurrentHashMap() private val rotateMap: Map = HashMap().apply { (rotatedSigningKeys + CORDA_SIGNING_KEY_ROTATIONS).forEach { rotatedKeyList -> 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 42311139ca..c668c8a294 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 @@ -1,6 +1,5 @@ package net.corda.coretests.contracts -< import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever 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 index 35d0878bb7..f91ba5d62b 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt @@ -1,18 +1,19 @@ package net.corda.coretests.transactions import com.codahale.metrics.MetricRegistry +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever 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.ServicesForResolution 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 @@ -30,8 +31,6 @@ 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 @@ -49,12 +48,11 @@ class AttachmentsClassLoaderWithStoragePersistenceTests { 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 { + private val services = rigorousMock().also { doReturn(testNetworkParameters()).whenever(it).networkParameters } @@ -86,8 +84,7 @@ class AttachmentsClassLoaderWithStoragePersistenceTests { it.start() } } - storage.nodeVerificationSupport = nodeVerificationSupport - attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory) + storage.servicesForResolution = services attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(storage, database, cacheFactory2) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt index 2ac318d884..71d3aca7f6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -1,6 +1,5 @@ -package net.corda.core.node.services +package net.corda.core.contracts -import net.corda.core.KeepForDJVM import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -45,7 +44,6 @@ private val CORDA_SIGNING_KEY_ROTATIONS = listOf( * */ @CordaSerializable -@KeepForDJVM data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()): SingletonSerializeAsToken() { private val canBeTransitionedMap: ConcurrentMap, Boolean> = ConcurrentHashMap() private val rotateMap: Map = HashMap().apply { 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 78457a900d..149fa2ea84 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -3,6 +3,7 @@ package net.corda.core.internal import net.corda.core.contracts.* import net.corda.core.crypto.keys import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.contracts.RotatedKeys import net.corda.core.utilities.loggerFor /** diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index e830b49c84..74cb1577f5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -90,6 +90,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 + */ 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 @@ -438,7 +439,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact 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()) + val constraintWithRotatedKeys = SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build()) if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract) } else { diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 612e341a6f..fa9a7baf34 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -171,6 +171,12 @@ interface ServiceHub : ServicesForResolution { */ val transactionVerifierService: TransactionVerifierService + /** + * INTERNAL. DO NOT USE. + * @suppress + */ + val rotatedKeys: RotatedKeys + /** * A [Clock] representing the node's current time. This should be used in preference to directly accessing the * clock so the current time can be controlled during unit testing. 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 a003b544fb..48db623eac 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 @@ -2,7 +2,6 @@ package net.corda.core.serialization.internal import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.CordaRotatedKeys @@ -359,7 +358,7 @@ object AttachmentsClassLoaderBuilder { val cache = if (attachmentsClassLoaderCache is AttachmentsClassLoaderForRotatedKeysOnlyImpl) fallBackCache else attachmentsClassLoaderCache ?: fallBackCache - val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params)), Function { key -> + val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key -> // Create classloader and load serializers, whitelisted classes val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) val serializers = try { @@ -539,7 +538,7 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int, override val rotated } class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache { - override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext): SerializationContext { + override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext { throw NotImplementedError("AttachmentsClassLoaderForRotatedKeysOnlyImpl.computeIfAbsent should never be called. Should be replaced by the fallback cache") } } 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 220095cd9e..cbe925b08b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -15,7 +15,9 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.AttachmentId +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.node.services.KeyManagementService +import net.corda.core.contracts.RotatedKeys import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults @@ -524,11 +526,11 @@ open class TransactionBuilder( // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName, - inputStates, selectedAttachment.currentAttachment, serviceHub) + inputStates, attachmentToUse, services) // Sanity check that the selected attachment actually passes. require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { - "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment" + "Selected output constraint: $defaultOutputConstraint not satisfying $attachmentToUse" } val resolvedOutputStates = outputStates.map { @@ -538,7 +540,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, serviceHub.toVerifyingServiceHub().rotatedKeys)) { + require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse, getRotatedKeys(serviceHub))) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } } @@ -550,6 +552,14 @@ open class TransactionBuilder( return Pair(selectedAttachmentId, resolvedOutputStates) } + private fun getRotatedKeys(services: ServiceHub?): RotatedKeys { + return services?.rotatedKeys ?: CordaRotatedKeys.keys.also { + log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + + "rotated keys defined in configuration. If you are writing a unit test then pass in a " + + "MockServices instance.") + } + } + private fun selectDefaultOutputConstraintAndConstraintAttachment( contractClassName: ContractClassName, inputStates: List>?, attachmentToUse: ContractAttachment, @@ -564,7 +574,7 @@ open class TransactionBuilder( 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)) { + if (defaultOutputConstraint is SignatureAttachmentConstraint && (getRotatedKeys(serviceHub).canBeTransitioned(defaultOutputConstraint.key, constraintAttachment.signerKeys))) { return Pair(makeSignatureAttachmentConstraint(attachmentToUse.signerKeys), constraintAttachment) } } 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 ecd71dcc3e..37b7c5a397 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -6,15 +6,12 @@ import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP 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 import net.corda.core.contracts.TransactionState -import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.SecureHash diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt index fd982d610a..69899508c5 100644 --- a/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt +++ b/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.Attachment import net.corda.core.contracts.CommandData import net.corda.core.contracts.CommandWithParties import net.corda.core.contracts.ContractState +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef @@ -54,7 +55,8 @@ class LtxSupplierFactory : Function, Supplier privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt, networkParameters = networkParameters, references = referencesProvider.get(), - digestService = txArgs[TX_DIGEST_SERVICE] as DigestService + digestService = txArgs[TX_DIGEST_SERVICE] as DigestService, + rotatedKeys = CordaRotatedKeys.keys ) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt index 299e71c52f..e58f63ea9f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt @@ -30,9 +30,8 @@ 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 com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import kotlin.test.assertEquals class ContractWithRotatedKeyTest { @@ -62,7 +61,7 @@ class ContractWithRotatedKeyTest { ): TestStartedNode { node.internals.disableDBCloseOnStop() node.dispose() - val cordappsDir = network.baseDirectory(node) / "cordapps" + val cordappsDir = network.baseDirectory(node).resolve("cordapps") deleteDirectory(cordappsDir.toFile()) return network.createNode( parameters.copy(legalName = node.internals.configuration.myLegalName, forcedID = node.internals.id), @@ -75,8 +74,8 @@ class ContractWithRotatedKeyTest { 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 packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="alias1") + val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1") val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services") val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2) 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 9f5cefa3d5..bfa11ea49c 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 @@ -6,6 +6,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.CordaRotatedKeys import net.corda.core.contracts.RotatedKeys import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash @@ -90,9 +91,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), - cordappsSignerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { + cordappsSignerKeyFingerprintBlacklist: List = emptyList(), rotatedKeys: RotatedKeys = CordaRotatedKeys.keys): JarScanningCordappLoader { val paths = scanJars.map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist, rotatedKeys) } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) 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 2f12079345..72ff0e541e 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 @@ -10,7 +10,6 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.notary.NotaryService - import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId From a2537d59f54d9a4d58bdca964dee5b7972cef0a7 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 9 Oct 2024 17:16:04 +0100 Subject: [PATCH 3/4] ENT-12291: Removed from rotated keys from public api. --- .../corda/core/internal/ServiceHubCoreInternal.kt | 3 +++ .../net/corda/core/internal/TransactionUtils.kt | 15 +++++++++++++++ .../main/kotlin/net/corda/core/node/ServiceHub.kt | 6 ------ .../corda/core/transactions/TransactionBuilder.kt | 5 +++-- .../corda/core/transactions/WireTransaction.kt | 4 ++-- .../kotlin/net/corda/testing/node/MockServices.kt | 3 ++- .../main/kotlin/net/corda/testing/dsl/TestDSL.kt | 2 +- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 4b7d856699..5f90afa5b7 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.DeleteForDJVM +import net.corda.core.contracts.RotatedKeys import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord @@ -24,6 +25,8 @@ interface ServiceHubCoreInternal : ServiceHub { fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver val attachmentsClassLoaderCache: AttachmentsClassLoaderCache + + val rotatedKeys: RotatedKeys } interface TransactionsResolver { diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 7bdfec76be..299a898267 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.algorithm import net.corda.core.crypto.internal.DigestAlgorithmFactory import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* import net.corda.core.transactions.* @@ -16,6 +17,20 @@ import java.io.ByteArrayOutputStream import java.security.PublicKey import kotlin.reflect.KClass + +fun ServiceHub.retrieveRotatedKeys(): RotatedKeys { + if (this is ServiceHubCoreInternal) { + return this.rotatedKeys + } + var clazz: Class<*> = javaClass + while (true) { + if (clazz.name == "net.corda.testing.node.MockServices") { + return clazz.getDeclaredMethod("getRotatedKeys").apply { isAccessible = true }.invoke(this) as RotatedKeys + } + clazz = clazz.superclass ?: throw ClassCastException("${javaClass.name} is not a ServiceHub") + } +} + /** Constructs a [NotaryChangeWireTransaction]. */ class NotaryChangeTransactionBuilder(val inputs: List, val notary: Party, diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index fa9a7baf34..612e341a6f 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -171,12 +171,6 @@ interface ServiceHub : ServicesForResolution { */ val transactionVerifierService: TransactionVerifierService - /** - * INTERNAL. DO NOT USE. - * @suppress - */ - val rotatedKeys: RotatedKeys - /** * A [Clock] representing the node's current time. This should be used in preference to directly accessing the * clock so the current time can be controlled during unit testing. 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 cbe925b08b..afc2b8d3e4 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -535,6 +535,7 @@ open class TransactionBuilder( val resolvedOutputStates = outputStates.map { val outputConstraint = it.constraint + if (outputConstraint in automaticConstraints) { it.copy(constraint = defaultOutputConstraint) } else { @@ -553,9 +554,9 @@ open class TransactionBuilder( } private fun getRotatedKeys(services: ServiceHub?): RotatedKeys { - return services?.rotatedKeys ?: CordaRotatedKeys.keys.also { + return services?.let { services.retrieveRotatedKeys() } ?: CordaRotatedKeys.keys.also { log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + - "rotated keys defined in configuration. If you are writing a unit test then pass in a " + + "state pointers outside of flows. If you are writing a unit test then pass in a " + "MockServices instance.") } } 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 37b7c5a397..6c33af9cc1 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -28,6 +28,7 @@ import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey @@ -166,8 +167,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr { stateRef -> resolveStateRef(stateRef)?.serialize() }, { null }, Attachment::isUploaderTrusted, - null - // TODO : elbad01 : does rotated keys need to be passed here, or we use attachment class loader cache + attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl() ) } 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 95adc20f18..606293ef03 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 @@ -3,6 +3,7 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateRef import net.corda.core.cordapp.CordappProvider @@ -457,10 +458,10 @@ open class MockServices private constructor( override val cordappProvider: CordappProvider get() = mockCordappProvider override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() + var rotatedKeys: RotatedKeys = CordaRotatedKeys.keys protected val servicesForResolution: ServicesForResolution get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) - override val rotatedKeys: RotatedKeys = RotatedKeys() internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService( 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 72ff0e541e..07b3ef1a3b 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 @@ -105,7 +105,7 @@ data class TestTransactionDSLInterpreter private constructor( ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build() ) - override val rotatedKeys: RotatedKeys = ledgerInterpreter.services.rotatedKeys + override val rotatedKeys: RotatedKeys = (ledgerInterpreter.services as? ServiceHubCoreInternal)?.rotatedKeys ?: CordaRotatedKeys.keys override val attachmentTrustCalculator: AttachmentTrustCalculator = ledgerInterpreter.services.attachments.let { From 4c0aa16f1ea2b0df0dcfbaeccc785fe3bbcebfae Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Thu, 10 Oct 2024 12:49:46 +0100 Subject: [PATCH 4/4] ENT-12291: RotatedKeys can be retrieved from MockServices. --- .../corda/coretests/contracts/RotatedKeysTest.kt | 15 +++++++++++++++ .../net/corda/core/internal/TransactionUtils.kt | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) 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 index 8e0da67b8d..3c6a0d9e98 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt @@ -1,16 +1,31 @@ package net.corda.coretests.contracts +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.sha256 +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.hash +import net.corda.core.internal.retrieveRotatedKeys +import net.corda.core.node.ServiceHub +import net.corda.testing.core.TestIdentity import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey import net.corda.testing.core.internal.SelfCleaningDir +import net.corda.testing.node.MockServices import org.junit.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class RotatedKeysTest { + + @Test(timeout = 300_000) + fun `validateDefaultRotatedKeysAreRetrievableFromMockServices`() { + val services: ServiceHub = MockServices(TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))) + val rotatedKeys = services.retrieveRotatedKeys() + assertEquals( CordaRotatedKeys.keys.rotatedSigningKeys, rotatedKeys.rotatedSigningKeys) + } + @Test(timeout = 300_000) fun `when input and output keys are the same canBeTransitioned returns true`() { SelfCleaningDir().use { file -> diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 299a898267..941f2db3e3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -13,6 +13,7 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.contextLogger import java.io.ByteArrayOutputStream import java.security.PublicKey import kotlin.reflect.KClass @@ -27,7 +28,9 @@ fun ServiceHub.retrieveRotatedKeys(): RotatedKeys { if (clazz.name == "net.corda.testing.node.MockServices") { return clazz.getDeclaredMethod("getRotatedKeys").apply { isAccessible = true }.invoke(this) as RotatedKeys } - clazz = clazz.superclass ?: throw ClassCastException("${javaClass.name} is not a ServiceHub") + clazz = clazz.superclass ?: return CordaRotatedKeys.keys.also { + this.contextLogger().warn("${javaClass.name} is not a MockServices instance - returning default rotated keys") + } } }