From ea36cf12e6b9938e6b0ed2c927be6cc8aeadd0bc Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Mon, 9 Oct 2017 17:31:57 +0100 Subject: [PATCH] Change IdentitySyncFlow to only offer own identities (#1812) Change IdentitySyncFlow to only offer confidential identities matching the identity the flow is run as. This avoids risks of nodes being convinced to include a state with a confidential identity in it, by a remote node, then feeding the well known identity to the node during identity sync. --- .../corda/confidential/IdentitySyncFlow.kt | 31 ++++++++------- .../confidential/IdentitySyncFlowTests.kt | 38 ++++++++++++++++++- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index 01f520afda..b454556bf3 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -12,14 +12,12 @@ import net.corda.core.utilities.unwrap object IdentitySyncFlow { /** - * Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential - * identities used in the transaction. This is intended for use as a subflow of another flow, typically between + * Flow for ensuring that our counterparties in a transaction have the full certificate paths for *our* confidential + * identities used in states present in the transaction. This is intended for use as a subflow of another flow, typically between * transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants * to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that * party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that * identity. - * - * @return a mapping of well known identities to the confidential identities used in the transaction. */ // TODO: Can this be triggered automatically from [SendTransactionFlow] class Send(val otherSideSessions: Set, @@ -36,17 +34,10 @@ object IdentitySyncFlow { @Suspendable override fun call() { progressTracker.currentStep = SYNCING_IDENTITIES - val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) - val identities: Set = states.flatMap { it.participants }.toSet() - // Filter participants down to the set of those not in the network map (are not well known) - val confidentialIdentities = identities - .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } - .toList() - val identityCertificates: Map = identities - .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap() + val identityCertificates: Map = extractOurConfidentialIdentities() otherSideSessions.forEach { otherSideSession -> - val requestedIdentities: List = otherSideSession.sendAndReceive>(confidentialIdentities).unwrap { req -> + val requestedIdentities: List = otherSideSession.sendAndReceive>(identityCertificates.keys.toList()).unwrap { req -> require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" } req } @@ -61,6 +52,20 @@ object IdentitySyncFlow { } } + private fun extractOurConfidentialIdentities(): Map { + val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val identities: Set = states.flatMap(ContractState::participants).toSet() + // Filter participants down to the set of those not in the network map (are not well known) + val confidentialIdentities = identities + .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } + .toList() + return confidentialIdentities + .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) } + // Filter down to confidential identities of our well known identity + // TODO: Consider if this too restrictive - we perhaps should be checking the name on the signing certificate in the certificate path instead + .filter { it.second?.name == ourIdentity.name } + .toMap() + } } /** diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index f2907c7c7c..2d5d064fbb 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -13,12 +13,14 @@ import net.corda.core.utilities.unwrap import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class IdentitySyncFlowTests { @@ -45,12 +47,12 @@ class IdentitySyncFlowTests { val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() val bob: Party = bobNode.services.myInfo.chooseIdentity() + val notary = notaryNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about val anonymous = true val ref = OpaqueBytes.of(0x01) - val notary = aliceNode.services.getDefaultNotary() val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary)) val issueTx = issueFlow.resultFuture.getOrThrow().stx val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner @@ -67,6 +69,40 @@ class IdentitySyncFlowTests { assertEquals(expected, actual) } + @Test + fun `don't offer other's identities confidential identities`() { + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + val charlieNode = mockNet.createPartyNode(notaryNode.network.myAddress, CHARLIE.name) + val alice: Party = aliceNode.services.myInfo.chooseIdentity() + val bob: Party = bobNode.services.myInfo.chooseIdentity() + val charlie: Party = charlieNode.services.myInfo.chooseIdentity() + val notary = notaryNode.services.getDefaultNotary() + bobNode.internals.registerInitiatedFlow(Receive::class.java) + + // Charlie issues then pays some cash to a new confidential identity + val anonymous = true + val ref = OpaqueBytes.of(0x01) + val issueFlow = charlieNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, charlie, anonymous, notary)) + val issueTx = issueFlow.resultFuture.getOrThrow().stx + val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner + val confidentialIdentCert = charlieNode.services.identityService.certificateFromKey(confidentialIdentity.owningKey)!! + + // Manually inject this identity into Alice's database so the node could leak it, but we prove won't + aliceNode.database.transaction { aliceNode.services.identityService.verifyAndRegisterIdentity(confidentialIdentCert) } + assertNotNull(aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + + // Generate a payment from Charlie to Alice, including the confidential state + val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).resultFuture.getOrThrow().stx + + // Run the flow to sync up the identities, and confirm Charlie's confidential identity doesn't leak + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + aliceNode.services.startFlow(Initiator(bob, payTx.tx)).resultFuture.getOrThrow() + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + } + /** * Very lightweight wrapping flow to trigger the counterparty flow that receives the identities. */