Change IdentitySyncFlow to only offer own identities (#1740)

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.
This commit is contained in:
Ross Nicoll 2017-10-05 10:33:35 +01:00 committed by GitHub
parent 46532ccbcb
commit 257756d862
2 changed files with 55 additions and 14 deletions

View File

@ -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<FlowSession>,
@ -36,17 +34,10 @@ object IdentitySyncFlow {
@Suspendable
override fun call() {
progressTracker.currentStep = SYNCING_IDENTITIES
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
val identities: Set<AbstractParty> = 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<AbstractParty, PartyAndCertificate?> = identities
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = extractOurConfidentialIdentities()
otherSideSessions.forEach { otherSideSession ->
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(confidentialIdentities).unwrap { req ->
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(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<AbstractParty, PartyAndCertificate?> {
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
val identities: Set<AbstractParty> = 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()
}
}
/**

View File

@ -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<Cash.State>().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<Cash.State>().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.
*/