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..8078308f12 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -0,0 +1,116 @@ +package net.corda.core.contracts + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.internal.hash +import net.corda.core.serialization.CordaSerializable +import java.security.PublicKey +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +object CordaRotatedKeys { + val keys = RotatedKeys() +} + +// The current development CorDapp code signing public key hash +const val DEV_CORDAPP_CODE_SIGNING_STR = "AA59D829F2CA8FDDF5ABEA40D815F937E3E54E572B65B93B5C216AE6594E7D6B" +// The non production CorDapp code signing public key hash +const val NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR = "B710A80780A12C52DF8A0B4C2247E08907CCA5D0F19AB1E266FE7BAEA9036790" +// The production CorDapp code signing public key hash +const val PROD_CORDAPP_CODE_SIGNING_STR = "EB4989E7F861FEBEC242E6C24CF0B51C41E108D2C4479D296C5570CB8DAD3EE0" +// The new production CorDapp code signing public key hash +const val NEW_PROD_CORDAPP_CODE_SIGNING_STR = "01EFA14B42700794292382C1EEAC9788A26DAFBCCC98992C01D5BC30EEAACD28" + +// Rotations used by Corda +private val CORDA_SIGNING_KEY_ROTATIONS = listOf( + listOf(SecureHash.create(DEV_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR).sha256()), + listOf(SecureHash.create(PROD_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_PROD_CORDAPP_CODE_SIGNING_STR).sha256()) +) + +/** + * This class represents the rotated CorDapp signing keys known by this node. + * + * A public key in this class is identified by its SHA-256 hash of the public key encoded bytes (@see PublicKey.getEncoded()). + * A sequence of rotated keys is represented by a list of hashes of those public keys. The list of those lists represents + * each unrelated set of rotated keys. A key should not appear more than once, either in the same list of in multiple lists. + * + * For the purposes of SignatureConstraints this means we treat all entries in a list of key hashes as equivalent. + * For two keys to be equivalent, they must be equal, or they must appear in the same list of hashes. + * + * @param rotatedSigningKeys A List of rotated keys. With a rotated key being represented by a list of hashes. This list comes from + * node.conf. + * + */ +@CordaSerializable +data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()) { + 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 ea5f7d4ecf..a27022fc12 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 67d0a8e7cb..ee73c85bda 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 @@ -3,7 +3,19 @@ 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 @@ -326,52 +338,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..3c6a0d9e98 --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/RotatedKeysTest.kt @@ -0,0 +1,294 @@ +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 -> + 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..f91ba5d62b --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderWithStoragePersistenceTests.kt @@ -0,0 +1,234 @@ +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.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.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 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 attachmentTrustCalculator2: AttachmentTrustCalculator + private val networkParameters = testNetworkParameters() + private val cacheFactory = TestingNamedCacheFactory(1) + private val cacheFactory2 = TestingNamedCacheFactory() + private val services = 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.servicesForResolution = services + attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(storage, database, cacheFactory2) + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() { + val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH_V4, copyFirst = true) + val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "untrusted", "isolated-signed.jar" ) + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(isolatedSignedId).use {} + } + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() { + SelfCleaningDir().use { file -> + val path = file.path + val alias1 = "AAAA" + val alias2 = "BBBB" + val password = "testPassword" + + path.generateKey(alias1, password) + path.generateKey(alias2, password) + + val contractName = "net.corda.testing.contracts.MyDummyContract" + val content = createContractString(contractName) + val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, contractName, content = content, version = 2) + path.signJar(contractJarPath.toAbsolutePath().toString(), alias1, password) + path.signJar(contractJarPath.toAbsolutePath().toString(), alias2, password) + val untrustedAttachment = storage.importAttachment(contractJarPath.toUri().toURL().openStream(), "untrusted", "contract.jar") + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + + @Test(timeout=300_000) + fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() { + SelfCleaningDir().use { file -> + val path = file.path + val alias1 = "AAAA" + val alias2 = "BBBB" + val alias3 = "CCCC" + val password = "testPassword" + + path.generateKey(alias1, password) + path.generateKey(alias2, password) + path.generateKey(alias3, password) + + val contractName1 = "net.corda.testing.contracts.MyDummyContract1" + val contractName2 = "net.corda.testing.contracts.MyDummyContract2" + val contractName3 = "net.corda.testing.contracts.MyDummyContract3" + + val content = createContractString(contractName1) + val contractJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = content) + path.signJar(contractJar.toAbsolutePath().toString(), alias1, password) + storage.privilegedImportAttachment(contractJar.toUri().toURL().openStream(), "app", "contract.jar") + + val content2 = createContractString(contractName2) + val contractJarPath2 = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = content2, version = 2) + path.signJar(contractJarPath2.toAbsolutePath().toString(), alias1, password) + path.signJar(contractJarPath2.toAbsolutePath().toString(), alias2, password) + val inheritedTrustAttachment = storage.importAttachment(contractJarPath2.toUri().toURL().openStream(), "untrusted", "dummy-contract.jar") + + val content3 = createContractString(contractName3) + val contractJarPath3 = ContractJarTestUtils.makeTestContractJar(path, contractName3, content = content3, version = 3) + path.signJar(contractJarPath3.toAbsolutePath().toString(), alias2, password) + path.signJar(contractJarPath3.toAbsolutePath().toString(), alias3, password) + val untrustedAttachment = storage.importAttachment(contractJarPath3.toUri().toURL() + .openStream(), "untrusted", "contract.jar") + + // pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment + createClassloader(inheritedTrustAttachment).use { + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + } + + @Test(timeout=300_000) + fun `Cannot load an untrusted contract jar if it is signed by a blacklisted key even if there is another attachment signed by the same keys that is trusted`() { + SelfCleaningDir().use { file -> + + val path = file.path + val aliasA = "AAAA" + val aliasB = "BBBB" + val password = "testPassword" + + val publicKeyA = path.generateKey(aliasA, password) + path.generateKey(aliasB, password) + + attachmentTrustCalculator2 = NodeAttachmentTrustCalculator( + storage, + cacheFactory, + blacklistedAttachmentSigningKeys = listOf(publicKeyA.hash) + ) + + val contractName1 = "net.corda.testing.contracts.MyDummyContract1" + val contractName2 = "net.corda.testing.contracts.MyDummyContract2" + + val contentTrusted = createContractString(contractName1) + val classJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = contentTrusted) + path.signJar(classJar.toAbsolutePath().toString(), aliasA, password) + path.signJar(classJar.toAbsolutePath().toString(), aliasB, password) + storage.privilegedImportAttachment(classJar.toUri().toURL().openStream(), "app", "contract.jar") + + val contentUntrusted = createContractString(contractName2) + val untrustedClassJar = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = contentUntrusted) + path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasA, password) + path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasB, password) + val untrustedAttachment = storage.importAttachment(untrustedClassJar.toUri().toURL() + .openStream(), "untrusted", "untrusted-contract.jar") + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + createClassloader(untrustedAttachment).use {} + } + } + } + + private fun createContractString(contractName: String, versionSeed: Int = 0): String { + val pkgs = contractName.split(".") + val className = pkgs.last() + val packages = pkgs.subList(0, pkgs.size - 1) + + val output = """package ${packages.joinToString(".")}; + import net.corda.core.contracts.*; + import net.corda.core.transactions.*; + import java.net.URL; + import java.io.InputStream; + + public class $className implements Contract { + private int seed = $versionSeed; + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + System.gc(); + InputStream str = this.getClass().getClassLoader().getResourceAsStream("importantDoc.pdf"); + if (str == null) throw new IllegalStateException("Could not find importantDoc.pdf"); + } + } + """.trimIndent() + + println(output) + return output + } +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt new file mode 100644 index 0000000000..71d3aca7f6 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -0,0 +1,117 @@ +package net.corda.core.contracts + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.internal.hash +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken +import java.security.PublicKey +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +object CordaRotatedKeys { + val keys = RotatedKeys() +} + +// The current development CorDapp code signing public key hash +const val DEV_CORDAPP_CODE_SIGNING_STR = "AA59D829F2CA8FDDF5ABEA40D815F937E3E54E572B65B93B5C216AE6594E7D6B" +// The non production CorDapp code signing public key hash +const val NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR = "B710A80780A12C52DF8A0B4C2247E08907CCA5D0F19AB1E266FE7BAEA9036790" +// The production CorDapp code signing public key hash +const val PROD_CORDAPP_CODE_SIGNING_STR = "EB4989E7F861FEBEC242E6C24CF0B51C41E108D2C4479D296C5570CB8DAD3EE0" +// The new production CorDapp code signing public key hash +const val NEW_PROD_CORDAPP_CODE_SIGNING_STR = "01EFA14B42700794292382C1EEAC9788A26DAFBCCC98992C01D5BC30EEAACD28" + +// Rotations used by Corda +private val CORDA_SIGNING_KEY_ROTATIONS = listOf( + listOf(SecureHash.create(DEV_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR).sha256()), + listOf(SecureHash.create(PROD_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_PROD_CORDAPP_CODE_SIGNING_STR).sha256()) +) + +/** + * This class represents the rotated CorDapp signing keys known by this node. + * + * A public key in this class is identified by its SHA-256 hash of the public key encoded bytes (@see PublicKey.getEncoded()). + * A sequence of rotated keys is represented by a list of hashes of those public keys. The list of those lists represents + * each unrelated set of rotated keys. A key should not appear more than once, either in the same list of in multiple lists. + * + * For the purposes of SignatureConstraints this means we treat all entries in a list of key hashes as equivalent. + * For two keys to be equivalent, they must be equal, or they must appear in the same list of hashes. + * + * @param rotatedSigningKeys A List of rotated keys. With a rotated key being represented by a list of hashes. This list comes from + * node.conf. + * + */ +@CordaSerializable +data class RotatedKeys(val rotatedSigningKeys: List> = emptyList()): SingletonSerializeAsToken() { + private val canBeTransitionedMap: ConcurrentMap, Boolean> = ConcurrentHashMap() + private val rotateMap: Map = HashMap().apply { + (rotatedSigningKeys + CORDA_SIGNING_KEY_ROTATIONS).forEach { rotatedKeyList -> + rotatedKeyList.forEach { key -> + if (this.containsKey(key)) throw IllegalStateException("The key with sha256(hash) $key appears in the rotated keys configuration more than once.") + this[key] = rotatedKeyList.last() + } + } + } + + fun canBeTransitioned(inputKey: PublicKey, outputKeys: List): Boolean { + return canBeTransitioned(inputKey, CompositeKey.Builder().addKeys(outputKeys).build()) + } + + fun canBeTransitioned(inputKeys: List, outputKeys: List): Boolean { + return canBeTransitioned(CompositeKey.Builder().addKeys(inputKeys).build(), CompositeKey.Builder().addKeys(outputKeys).build()) + } + + fun canBeTransitioned(inputKey: PublicKey, outputKey: PublicKey): Boolean { + // Need to handle if inputKey and outputKey are composite keys. They could be if part of SignatureConstraints + return canBeTransitionedMap.getOrPut(Pair(inputKey, outputKey)) { + when { + (inputKey is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey) + (inputKey is CompositeKey && outputKey !is CompositeKey) -> compareKeys(inputKey, outputKey) + (inputKey !is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey) + else -> isRotatedEquals(inputKey, outputKey) + } + } + } + + private fun rotate(key: SecureHash): SecureHash { + return rotateMap[key] ?: key + } + + private fun isRotatedEquals(inputKey: PublicKey, outputKey: PublicKey): Boolean { + return when { + inputKey == outputKey -> true + rotate(inputKey.hash.sha256()) == rotate(outputKey.hash.sha256()) -> true + else -> false + } + } + + private fun compareKeys(inputKey: CompositeKey, outputKey: PublicKey): Boolean { + if (inputKey.leafKeys.size == 1) { + return canBeTransitioned(inputKey.leafKeys.first(), outputKey) + } + return false + } + + private fun compareKeys(inputKey: PublicKey, outputKey: CompositeKey): Boolean { + if (outputKey.leafKeys.size == 1) { + return canBeTransitioned(inputKey, outputKey.leafKeys.first()) + } + return false + } + + private fun compareKeys(inputKey: CompositeKey, outputKey: CompositeKey): Boolean { + if (inputKey.leafKeys.size != outputKey.leafKeys.size) { + return false + } + else { + inputKey.leafKeys.forEach { inputLeafKey -> + if (!outputKey.leafKeys.any { outputLeafKey -> canBeTransitioned(inputLeafKey, outputLeafKey) }) { + return false + } + } + return true + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index 7e59c207b5..d8c2f599bb 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 /** @@ -57,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") @@ -83,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/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index bd7c1142ac..c97c8bd479 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -1,6 +1,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata @@ -28,6 +29,8 @@ interface ServiceHubCoreInternal : ServiceHub { val attachmentsClassLoaderCache: AttachmentsClassLoaderCache + val rotatedKeys: RotatedKeys + /** * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage, * inclusive of flow recovery metadata. 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 41e94a236a..2e83aa7bad 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -7,14 +7,32 @@ 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.* import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.contextLogger 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 ?: return CordaRotatedKeys.keys.also { + this.contextLogger().warn("${javaClass.name} is not a MockServices instance - returning default rotated keys") + } + } +} + /** Constructs a [NotaryChangeWireTransaction]. */ class NotaryChangeTransactionBuilder(val inputs: List, val notary: Party, 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 840163238f..5c8bc2c11c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -2,11 +2,13 @@ package net.corda.core.internal 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 @@ -84,9 +86,9 @@ 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. + * wrong object instance. This class helps */ -private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) { +private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, private val rotatedKeys: RotatedKeys) { private val inputStates: List> = ltx.inputs.map(StateAndRef::state) private val allStates: List> = inputStates + ltx.references.map(StateAndRef::state) + ltx.outputs @@ -371,7 +373,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, @@ -425,8 +427,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(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build()) + if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract) + } + else { + throw ContractConstraintRejection(ltx.id, contract) } } } @@ -461,7 +475,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 0d84556633..b8083c6727 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -4,6 +4,8 @@ import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.CordaRotatedKeys +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException @@ -357,7 +359,8 @@ object AttachmentsClassLoaderBuilder { block: (SerializationContext) -> T): T { val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id) - val cache = attachmentsClassLoaderCache ?: fallBackCache + val cache = if (attachmentsClassLoaderCache is AttachmentsClassLoaderForRotatedKeysOnlyImpl) fallBackCache else + attachmentsClassLoaderCache ?: fallBackCache val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key -> // Create classloader and load serializers, whitelisted classes val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) @@ -473,11 +476,11 @@ private class AttachmentsHolderImpl : AttachmentsHolder { } interface AttachmentsClassLoaderCache { + val rotatedKeys: RotatedKeys fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext } -class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { - +class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { private class ToBeClosed( serializationContext: SerializationContext, val classLoaderToClose: AutoCloseable, @@ -528,8 +531,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() @@ -538,6 +540,12 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLo } } +class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache { + override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): 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 846799d0b3..51cfcb4867 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -7,7 +7,9 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.CommandWithParties import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.contracts.ContractState +import net.corda.core.contracts.CordaRotatedKeys import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.RotatedKeys import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionState @@ -31,6 +33,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.internal.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 @@ -96,6 +99,8 @@ private constructor( val digestService: DigestService ) : FullTransaction() { + val rotatedKeys = attachmentsClassLoaderCache?.rotatedKeys ?: CordaRotatedKeys.keys + /** * Old version of [LedgerTransaction] constructor for ABI compatibility. */ @@ -194,7 +199,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), @@ -211,7 +217,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. @@ -861,7 +867,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 4cede898a0..75a09efbe0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -14,7 +14,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 @@ -521,20 +523,25 @@ 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, attachmentToUse, services) // 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 $attachmentToUse" + } val resolvedOutputStates = outputStates.map { val outputConstraint = it.constraint + if (outputConstraint in automaticConstraints) { it.copy(constraint = defaultOutputConstraint) } 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, attachmentToUse, getRotatedKeys(serviceHub))) { + "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" + } } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } it @@ -544,6 +551,35 @@ open class TransactionBuilder( return Pair(selectedAttachmentId, resolvedOutputStates) } + private fun getRotatedKeys(services: ServiceHub?): RotatedKeys { + return services?.let { services.retrieveRotatedKeys() } ?: CordaRotatedKeys.keys.also { + log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + + "state pointers outside of flows. If you are writing a unit test then pass in a " + + "MockServices instance.") + } + } + + 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 && (getRotatedKeys(serviceHub).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 a0fa249240..c6eb410ad5 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -4,7 +4,17 @@ import net.corda.core.CordaInternal 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.ContractState +import net.corda.core.contracts.PrivacySalt +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.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 @@ -16,6 +26,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 @@ -149,7 +160,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr { stateRef -> resolveStateRef(stateRef)?.serialize() }, { null }, Attachment::isUploaderTrusted, - null + attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl() ) } 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..e58f63ea9f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt @@ -0,0 +1,134 @@ +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 com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +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).resolve("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="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) + + 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 72c31fc33c..bb141005e7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -12,6 +12,7 @@ import net.corda.confidential.SwapIdentitiesFlow import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.contracts.RotatedKeys import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom @@ -247,7 +248,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private val notaryLoader = configuration.notary?.let { NotaryLoader(it, versionInfo) } - val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo).closeOnStop(false) + val rotatedKeys = makeRotatedKeysService(configuration).tokenize() + val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo, rotatedKeys).closeOnStop(false) val telemetryService: TelemetryServiceImpl = TelemetryServiceImpl().also { val openTelemetryComponent = OpenTelemetryComponent(configuration.myLegalName.toString(), configuration.telemetry.spanStartEndEventsEnabled, configuration.telemetry.copyBaggageToTags) if (configuration.telemetry.openTelemetryEnabled && openTelemetryComponent.isEnabled()) { @@ -291,7 +293,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, database, configuration.devMode ).tokenize() - val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database) + val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database, rotatedKeys) @Suppress("LeakingThis") val networkParametersStorage = makeNetworkParametersStorage() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @@ -315,7 +317,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, cordappProvider = cordappProvider, attachments = attachments ).tokenize() - 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") @@ -853,7 +855,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 @@ -868,7 +870,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, configuration.cordappDirectories, versionInfo, extraCordapps = generatedCordapps, - signerKeyFingerprintBlacklist = blacklistedKeys + signerKeyFingerprintBlacklist = blacklistedKeys, + rotatedKeys = rotatedKeys ) } @@ -883,9 +886,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" } @@ -893,7 +903,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, attachmentStorage = attachments, database = database, cacheFactory = cacheFactory, - blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys + blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys, + rotatedKeys = rotatedKeys ).tokenize() } @@ -1205,6 +1216,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 override val telemetryService: TelemetryService get() = this@AbstractNode.telemetryService private lateinit var _myInfo: NodeInfo 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 ce64bd28f5..1b6041e4af 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,8 @@ 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 import net.corda.core.crypto.sha256 @@ -50,7 +52,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") @@ -76,10 +79,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) } /** @@ -88,9 +92,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/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 a5cf742a9e..ec2a43694b 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -86,6 +86,13 @@ interface NodeConfiguration : ConfigurationWithOptionsContainer { val cordappSignerKeyFingerprintBlacklist: List + /** + * Represents a list of rotated CorDapp attachment JAR signing key configurations. Each configuration describes a set of equivalent + * keys. Logically there should be no overlap between configurations, since that would mean they should be one combined list, + * and this is enforced. + */ + val rotatedCordappSignerKeys: List + val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? val networkParametersPath: Path @@ -215,6 +222,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) + data class TelemetryConfiguration( val openTelemetryEnabled: Boolean, val simpleLogTelemetryEnabled: Boolean, 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 facf8ad3f6..e8235bdce7 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 @@ -79,6 +79,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, @@ -121,6 +122,7 @@ data class NodeConfigurationImpl( val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() } + val rotatedCordappSignerKeys: List = emptyList() val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings() val blacklistedAttachmentSigningKeys: List = emptyList() const val flowExternalOperationThreadPoolSize: Int = 1 diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt index 6aeb54b1b1..0a8165be4f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt @@ -27,6 +27,7 @@ import net.corda.node.services.config.NodeH2Settings import net.corda.node.services.config.NodeRpcSettings import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.PasswordEncryption +import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId import net.corda.node.services.config.TelemetryConfiguration @@ -213,6 +214,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 TelemetryConfigurationSpec : Configuration.Specification("TelemetryConfiguration") { private val openTelemetryEnabled by boolean() private val simpleLogTelemetryEnabled by boolean() 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 023a1e66df..de824057ca 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 @@ -61,6 +61,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) } /** @@ -494,6 +496,7 @@ 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) 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 a844684510..b3cfdfaf8c 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 @@ -47,6 +47,7 @@ import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration import net.corda.node.services.config.TelemetryConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.identity.PersistentIdentityService @@ -677,6 +678,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 b460d02f30..1f941ecf96 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 @@ -17,6 +17,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord 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 @@ -109,17 +110,20 @@ data class TestTransactionDSLInterpreter private constructor( ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build() ) + override val rotatedKeys: RotatedKeys = (ledgerInterpreter.services as? ServiceHubCoreInternal)?.rotatedKeys ?: CordaRotatedKeys.keys + 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 ) }