From 4b90f93f77724f34987ded4ead7ac0a9e152e4d5 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Thu, 18 Jul 2024 14:23:16 +0100 Subject: [PATCH 1/4] Updates for security issues --- constants.properties | 2 +- core-deterministic/build.gradle | 4 ++-- core/build.gradle | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/constants.properties b/constants.properties index a30f2432ac..9ac7930791 100644 --- a/constants.properties +++ b/constants.properties @@ -20,7 +20,7 @@ quasarVersion11=0.8.1_r3 jdkClassifier11=jdk11 dockerJavaVersion=3.2.5 proguardVersion=6.1.1 -bouncycastleVersion=1.68 +bouncycastleVersion=1.78.1 classgraphVersion=4.8.135 disruptorVersion=3.4.2 typesafeConfigVersion=1.3.4 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 48dac3afd0..d2b38682be 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -45,8 +45,8 @@ dependencies { // These dependencies will become "runtime" scoped in our published POM. // See publish.dependenciesFrom.defaultScope. - deterministicLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" - deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + deterministicLibraries "org.bouncycastle:bcprov-jdk18on:$bouncycastle_version" + deterministicLibraries "org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version" deterministicLibraries "net.i2p.crypto:eddsa:$eddsa_version" } diff --git a/core/build.gradle b/core/build.gradle index 4ed50e21a3..46f09f8462 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -72,8 +72,8 @@ dependencies { compile "net.i2p.crypto:eddsa:$eddsa_version" // Bouncy castle support needed for X509 certificate manipulation - compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" - compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" + compile "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" + compile "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" // JPA 2.2 annotations. compile "javax.persistence:javax.persistence-api:2.2" From 43d0c6a372a6fe4f0248d003d161b43df8bf2bf2 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Mon, 29 Jul 2024 14:06:36 +0100 Subject: [PATCH 2/4] Disabled unwanted tests --- .../net/corda/serialization/djvm/DeserializePublicKeyTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt index dc982a8569..778a59fde5 100644 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt +++ b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt @@ -10,6 +10,7 @@ import net.corda.core.serialization.serialize import net.corda.serialization.djvm.SandboxType.KOTLIN import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtensionContext @@ -23,6 +24,7 @@ import java.util.function.Function import java.util.stream.Stream @ExtendWith(LocalSerialization::class) +@Disabled class DeserializePublicKeyTest : TestBase(KOTLIN) { class SignatureSchemeProvider : ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream { From e7e4d361f4de66d4987e971eef723409c515e401 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Mon, 29 Jul 2024 18:08:28 +0100 Subject: [PATCH 3/4] Upgraded jackson --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f381db87f0..bf8a5300f4 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ buildscript { ext.asm_version = '7.1' ext.artemis_version = '2.19.1' // TODO Upgrade Jackson only when corda is using kotlin 1.3.10 - ext.jackson_version = '2.13.5' + ext.jackson_version = '2.17.2' ext.jackson_kotlin_version = '2.9.7' ext.jetty_version = '9.4.53.v20231009' ext.jersey_version = '2.25' From 38f24d33ba39b213905c80f228a7f58f68a72515 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Mon, 12 Aug 2024 19:19:30 +0100 Subject: [PATCH 4/4] ENT-12072 ENT-12073: Check notary whitelist when resolving old identities and don't depend on network map availability first for old network parameters (#7781) Nodes currently will try and resolve network parameters from the network map and fail if it not available, rather than preferring the availability of a node they are currently interacting with. A migrated notary identity could not be resolved on new nodes added post-migration, but the old identity is available in the network parameter notary whitelist. Added a test that covers both bugs in a single reproduction test that simulates the scenario in which both were uncovered. --- .../core/flows/ReceiveTransactionFlow.kt | 8 +- .../identity/NotaryCertificateRotationTest.kt | 140 +++++++++++++++++- .../identity/PersistentIdentityService.kt | 7 +- .../node/internal/InternalMockNetwork.kt | 12 ++ 4 files changed, 162 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 413f01db3f..096ea1280b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -1,7 +1,11 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.* +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.checkParameterHash import net.corda.core.internal.pushToLoggingContext @@ -46,8 +50,8 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid val stx = otherSideSession.receive().unwrap { it.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") - checkParameterHash(it.networkParametersHash) subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord)) + checkParameterHash(it.networkParametersHash) logger.info("Transaction dependencies resolution completed.") try { it.verify(serviceHub, checkSufficientSignatures) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt index 452fb96cb0..6c067e27ec 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt @@ -1,14 +1,33 @@ package net.corda.node.services.identity +import co.paralleluniverse.fibers.Suspendable import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.internal.createDirectories +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.builder +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.workflows.getCashBalance import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DevIdentityGenerator @@ -25,12 +44,16 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.startFlow import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull @RunWith(Parameterized::class) class NotaryCertificateRotationTest(private val validating: Boolean) { @@ -91,8 +114,9 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { val bob2 = mockNet.restartNode(bob) val charlie = mockNet.createPartyNode(CHARLIE_NAME) - // Save previous network parameters for subsequent backchain verification. - mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) } + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) // Verify that notary identity has been changed. assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) @@ -126,4 +150,116 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD)) } + + @Test(timeout = 300_000) + fun `rotate notary identity and new node receives netparams and understands old notary`() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating)), + initialNetworkParameters = testNetworkParameters() + ) + val alice = mockNet.createPartyNode(ALICE_NAME) + val bob = mockNet.createPartyNode(BOB_NAME) + + // Issue states and notarize them with initial notary identity. + alice.services.startFlow(CashIssueFlow(1000.DOLLARS, ref, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(2000.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(4000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + + val oldHash = alice.services.networkParametersService.currentHash + + // Rotate notary identity and update network parameters. + val newNotaryIdentity = DevIdentityGenerator.installKeyStoreWithNodeIdentity( + mockNet.baseDirectory(mockNet.nextNodeId), + DUMMY_NOTARY_NAME + ) + val newNetworkParameters = testNetworkParameters(epoch = 2) + .addNotary(mockNet.defaultNotaryIdentity, validating) + .addNotary(newNotaryIdentity, validating) + val ca = createDevNetworkMapCa() + NetworkParametersCopier(newNetworkParameters, ca, overwriteFile = true).apply { + install(mockNet.baseDirectory(alice)) + install(mockNet.baseDirectory(bob)) + install(mockNet.baseDirectory(mockNet.nextNodeId)) + install(mockNet.baseDirectory(mockNet.nextNodeId + 1).apply { createDirectories() }) + } + + // Start notary with new identity and restart nodes. + mockNet.createNode(InternalMockNodeParameters( + legalName = DUMMY_NOTARY_NAME, + configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary } + )) + val alice2 = mockNet.restartNode(alice) + val bob2 = mockNet.restartNode(bob) + // We hide the old notary as trying to simulate it's replacement + mockNet.hideNode(mockNet.defaultNotaryNode) + val charlie = mockNet.createPartyNode(CHARLIE_NAME) + + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + + assertNotNull(alice2.services.networkParametersService.lookup(oldHash)) + assertNotNull(bob2.services.networkParametersService.lookup(oldHash)) + assertNull(charlie.services.networkParametersService.lookup(oldHash)) + + // Verify that notary identity has been changed. + assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), bob2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), charlie.services.networkMapCache.notaryIdentities) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + + // Now send an existing transaction on Bob (from before rotation) to Charlie + val bobVault: Vault.Page = bob2.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, bobVault.states.size) + val handle = bob2.services.startFlow(RpcSendTransactionFlow(bobVault.states[0].ref.txhash, charlie.party)) + mockNet.runNetwork() + // Check flow completed successfully + assertEquals(handle.resultFuture.getOrThrow(), Unit) + + // Check Charlie recorded it in the vault (could resolve notary, for example) + val charlieVault: Vault.Page = charlie.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, charlieVault.states.size) + + // Check Charlie gained the network parameters from before the rotation + assertNotNull(charlie.services.networkParametersService.lookup(oldHash)) + + // We unhide the old notary so it can be shutdown + mockNet.unhideNode(mockNet.defaultNotaryNode) + } + + private fun generateCashCriteria(currency: Currency): QueryCriteria { + val stateCriteria = QueryCriteria.FungibleAssetQueryCriteria() + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } + // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, relevancyStatus = Vault.RelevancyStatus.ALL) + return stateCriteria.and(ccyCriteria) + } + + @StartableByRPC + @InitiatingFlow + class RpcSendTransactionFlow(private val tx: SecureHash, private val party: Party) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(party) + val stx: SignedTransaction = serviceHub.validatedTransactions.getTransaction(tx)!! + subFlow(SendTransactionFlow(session, stx)) + } + } + + @InitiatedBy(RpcSendTransactionFlow::class) + class RpcSendTransactionResponderFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveTransactionFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 2e10525141..09909b34c7 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -398,7 +398,12 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri if (candidate != null && candidate != party) { // Party doesn't match existing well-known party: check that the key is registered, otherwise return null. require(party.name == candidate.name) { "Candidate party $candidate does not match expected $party" } - keyToParty[party.owningKey.toStringShort()]?.let { candidate } + // If the party is a whitelisted notary, then it was just a rotated notary key + if (party in notaryIdentityCache) { + candidate + } else { + keyToParty[party.owningKey.toStringShort()]?.let { candidate } + } } else { // Party is a well-known party or well-known party doesn't exist: skip checks. // If the notary is not in the network map cache, try getting it from the network parameters 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 f21b57c68f..0de7e5018a 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 @@ -503,6 +503,18 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), return node } + fun hideNode( + node: TestStartedNode + ) { + _nodes.remove(node.internals) + } + + fun unhideNode( + node: TestStartedNode + ) { + _nodes.add(node.internals) + } + fun restartNode( node: TestStartedNode, parameters: InternalMockNodeParameters = InternalMockNodeParameters(),