CORDA-2114: SwapIdentitiesFlow is now inlined (#4260)

This is to fix the security issue whereby any counterparty is able to generate anonymous identities with a node at will without checks.
This commit is contained in:
Shams Asari 2018-11-26 09:41:14 +00:00 committed by GitHub
parent b01f278eb3
commit 3b8a74fe44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 196 additions and 194 deletions

View File

@ -1,118 +1,106 @@
package net.corda.confidential package net.corda.confidential
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.CordaInternal
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.verify
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.FlowSession
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.*
/** /**
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction * Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
* key and certificate paths between the parties. This is intended for use as a sub-flow of another flow which builds a * key and certificate paths between the parties. This is intended for use as a sub-flow of another flow which builds a
* transaction. * transaction. The flow running on the other side must also call this flow at the correct location.
*/ */
@StartableByRPC class SwapIdentitiesFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
@InitiatingFlow override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SwapIdentitiesFlow.AnonymousResult>() {
// TODO Make this non-initiating as otherwise any CorDapp using confidential identities will cause its node to have an
// open door where any counterparty will be able to swap identities at will. Instead SwapIdentitiesFlow and its counterpart,
// SwapIdentitiesHandler, should be in-lined and called by CorDapp specfic-flows.
class SwapIdentitiesFlow(private val otherParty: Party,
private val revocationEnabled: Boolean,
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
constructor(otherParty: Party) : this(otherParty, false, tracker())
companion object { companion object {
object AWAITING_KEY : ProgressTracker.Step("Awaiting key") object GENERATING_IDENTITY : ProgressTracker.Step("Generating our anonymous identity")
object SIGNING_IDENTITY : ProgressTracker.Step("Signing our anonymous identity")
object AWAITING_IDENTITY : ProgressTracker.Step("Awaiting counterparty's anonymous identity")
object VERIFYING_IDENTITY : ProgressTracker.Step("Verifying counterparty's anonymous identity")
@JvmStatic
fun tracker(): ProgressTracker = ProgressTracker(GENERATING_IDENTITY, SIGNING_IDENTITY, AWAITING_IDENTITY, VERIFYING_IDENTITY)
fun tracker() = ProgressTracker(AWAITING_KEY)
/** /**
* Generate the deterministic data blob the confidential identity's key holder signs to indicate they want to * Generate the deterministic data blob the confidential identity's key holder signs to indicate they want to
* represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes, * represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes,
* but only the signature is sent. The blob is built independently on each node and the received signature * but only the signature is sent. The blob is built independently on each node and the received signature
* verified against the expected blob, rather than exchanging the blob. * verified against the expected blob, rather than exchanging the blob.
*/ */
fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray { @CordaInternal
val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey) @VisibleForTesting
return certOwnerAssert.serialize().bytes internal fun buildDataToSign(identity: PartyAndCertificate): ByteArray {
return CertificateOwnershipAssertion(identity.name, identity.owningKey).serialize().bytes
} }
@Throws(SwapIdentitiesException::class) @CordaInternal
fun validateAndRegisterIdentity(identityService: IdentityService, @VisibleForTesting
otherSide: Party, internal fun validateAndRegisterIdentity(serviceHub: ServiceHub,
anonymousOtherSideBytes: PartyAndCertificate, otherSide: Party,
sigBytes: DigitalSignature): PartyAndCertificate { theirAnonymousIdentity: PartyAndCertificate,
val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes signature: DigitalSignature): PartyAndCertificate {
if (anonymousOtherSide.name != otherSide.name) { if (theirAnonymousIdentity.name != otherSide.name) {
throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.") throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.")
} }
val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes)
try { try {
signature.verify(buildDataToSign(anonymousOtherSideBytes)) theirAnonymousIdentity.owningKey.verify(buildDataToSign(theirAnonymousIdentity), signature)
} catch(ex: SignatureException) { } catch (ex: SignatureException) {
throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex) throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex)
} }
// Validate then store their identity so that we can prove the key in the transaction is owned by the // Validate then store their identity so that we can prove the key in the transaction is owned by the counterparty.
// counterparty. serviceHub.identityService.verifyAndRegisterIdentity(theirAnonymousIdentity)
identityService.verifyAndRegisterIdentity(anonymousOtherSide) return theirAnonymousIdentity
return anonymousOtherSide
} }
} }
@Suspendable @Suspendable
override fun call(): LinkedHashMap<Party, AnonymousParty> { override fun call(): AnonymousResult {
progressTracker.currentStep = AWAITING_KEY progressTracker.currentStep = GENERATING_IDENTITY
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) val ourAnonymousIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
val serializedIdentity = SerializedBytes<PartyAndCertificate>(legalIdentityAnonymous.serialize().bytes) val data = buildDataToSign(ourAnonymousIdentity)
progressTracker.currentStep = SIGNING_IDENTITY
// Special case that if we're both parties, a single identity is generated. val signature = serviceHub.keyManagementService.sign(data, ourAnonymousIdentity.owningKey).withoutKey()
// TODO: for increased privacy, we should create one anonymous key per output state. val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity, signature)
val identities = LinkedHashMap<Party, AnonymousParty>() progressTracker.currentStep = AWAITING_IDENTITY
if (serviceHub.myInfo.isLegalIdentity(otherParty)) { val theirAnonymousIdentity = otherSideSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
identities[otherParty] = legalIdentityAnonymous.party.anonymise() progressTracker.currentStep = VERIFYING_IDENTITY
} else { validateAndRegisterIdentity(serviceHub, otherSideSession.counterparty, theirIdentWithSig.identity, theirIdentWithSig.signature)
val otherSession = initiateFlow(otherParty)
val data = buildDataToSign(legalIdentityAnonymous)
val ourSig: DigitalSignature.WithKey = serviceHub.keyManagementService.sign(data, legalIdentityAnonymous.owningKey)
val ourIdentWithSig = IdentityWithSignature(serializedIdentity, ourSig.withoutKey())
val anonymousOtherSide = otherSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig)
.unwrap { (confidentialIdentityBytes, theirSigBytes) ->
val confidentialIdentity: PartyAndCertificate = confidentialIdentityBytes.bytes.deserialize()
validateAndRegisterIdentity(serviceHub.identityService, otherParty, confidentialIdentity, theirSigBytes)
}
identities[ourIdentity] = legalIdentityAnonymous.party.anonymise()
identities[otherParty] = anonymousOtherSide.party.anonymise()
} }
return identities return AnonymousResult(ourAnonymousIdentity.party.anonymise(), theirAnonymousIdentity.party.anonymise())
} }
/**
* Result class containing the caller's anonymous identity ([ourIdentity]) and the counterparty's ([theirIdentity]).
*/
@CordaSerializable @CordaSerializable
data class IdentityWithSignature(val identity: SerializedBytes<PartyAndCertificate>, val signature: DigitalSignature) data class AnonymousResult(val ourIdentity: AnonymousParty, val theirIdentity: AnonymousParty)
@CordaSerializable
private data class IdentityWithSignature(val identity: PartyAndCertificate, val signature: DigitalSignature)
/**
* Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it
* to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says
* the key represents the named entity) and protects against a malicious party incorrectly claiming others'
* keys.
*/
@CordaSerializable
private data class CertificateOwnershipAssertion(val name: CordaX500Name, val owningKey: PublicKey)
} }
/** open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null) : FlowException(message, cause)
* Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it
* to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says
* the key represents the named entity) and protects against a malicious party incorrectly claiming others'
* keys.
*/
@CordaSerializable
data class CertificateOwnershipAssertion(val x500Name: CordaX500Name,
val publicKey: PublicKey)
open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null)
: FlowException(message, cause)

View File

@ -1,36 +0,0 @@
package net.corda.confidential
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
constructor(otherSideSession: FlowSession) : this(otherSideSession, false)
companion object {
object SENDING_KEY : ProgressTracker.Step("Sending key")
}
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
@Suspendable
override fun call() {
val revocationEnabled = false
progressTracker.currentStep = SENDING_KEY
val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
val serializedIdentity = SerializedBytes<PartyAndCertificate>(ourConfidentialIdentity.serialize().bytes)
val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity)
val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey)
otherSideSession.sendAndReceive<SwapIdentitiesFlow.IdentityWithSignature>(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey()))
.unwrap { (theirConfidentialIdentityBytes, theirSigBytes) ->
val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize()
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes)
}
}
}

View File

@ -1,26 +1,40 @@
package net.corda.confidential package net.corda.confidential
import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.assertion.assert
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
import net.corda.core.identity.* import net.corda.core.crypto.DigitalSignature
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.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.packageName
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.matchers.allOf import net.corda.testing.internal.matchers.allOf
import net.corda.testing.internal.matchers.flow.willReturn import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.junit.Test
import kotlin.test.*
import com.natpryce.hamkrest.assertion.assert
import net.corda.core.crypto.DigitalSignature
import net.corda.testing.internal.matchers.hasOnlyEntries import net.corda.testing.internal.matchers.hasOnlyEntries
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.startFlow
import org.junit.AfterClass import org.junit.AfterClass
import org.junit.Test
import java.security.PublicKey import java.security.PublicKey
import kotlin.test.assertFailsWith
class SwapIdentitiesFlowTests { class SwapIdentitiesFlowTests {
companion object { companion object {
private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true) private val mockNet = InternalMockNetwork(
networkSendManuallyPumped = false,
threadPerNode = true,
cordappsForAllNodes = cordappsForPackages(this::class.packageName)
)
@AfterClass @AfterClass
@JvmStatic @JvmStatic
@ -36,7 +50,7 @@ class SwapIdentitiesFlowTests {
@Test @Test
fun `issue key`() { fun `issue key`() {
assert.that( assert.that(
aliceNode.services.startFlow(SwapIdentitiesFlow(bob)), aliceNode.services.startFlow(SwapIdentitiesInitiator(bob)),
willReturn( willReturn(
hasOnlyEntries( hasOnlyEntries(
alice to allOf( alice to allOf(
@ -102,21 +116,20 @@ class SwapIdentitiesFlowTests {
services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false) services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false)
} }
private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey) = private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey): DigitalSignature.WithKey {
services.keyManagementService.sign( return services.keyManagementService.sign(SwapIdentitiesFlow.buildDataToSign(party), owningKey)
SwapIdentitiesFlow.buildDataToSign(party), }
owningKey)
private fun TestStartedNode.validateSwapIdentitiesFlow( private fun TestStartedNode.validateSwapIdentitiesFlow(party: Party,
party: Party, counterparty: PartyAndCertificate,
counterparty: PartyAndCertificate, signature: DigitalSignature.WithKey): PartyAndCertificate {
signature: DigitalSignature.WithKey) = return SwapIdentitiesFlow.validateAndRegisterIdentity(
SwapIdentitiesFlow.validateAndRegisterIdentity( services,
services.identityService, party,
party, counterparty,
counterparty, signature.withoutKey()
signature.withoutKey() )
) }
//endregion //endregion
//region Matchers //region Matchers
@ -141,21 +154,36 @@ class SwapIdentitiesFlowTests {
override val description = override val description =
"has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}" "has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}"
override fun invoke(actual: AnonymousParty) = override fun invoke(actual: AnonymousParty): MatchResult {
if (negated != actual.owningKey in node.services.keyManagementService.keys) { return if (negated != actual.owningKey in node.services.keyManagementService.keys) {
MatchResult.Match MatchResult.Match
} else { } else {
MatchResult.Mismatch(""" MatchResult.Mismatch("""
had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name} had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name}
""".trimIndent()) """.trimIndent())
} }
override fun not(): Matcher<AnonymousParty> {
return copy(negated=!negated)
} }
override fun not(): Matcher<AnonymousParty> = copy(negated=!negated)
} }
private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this) private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this)
//endregion //endregion
}
@InitiatingFlow
private class SwapIdentitiesInitiator(private val otherSide: Party) : FlowLogic<Map<Party, AnonymousParty>>() {
@Suspendable
override fun call(): Map<Party, AnonymousParty> {
val (anonymousUs, anonymousThem) = subFlow(SwapIdentitiesFlow(initiateFlow(otherSide)))
return mapOf(ourIdentity to anonymousUs, otherSide to anonymousThem)
}
}
@InitiatedBy(SwapIdentitiesInitiator::class)
private class SwapIdentitiesResponder(private val otherSide: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(SwapIdentitiesFlow(otherSide))
}
} }

View File

@ -7,9 +7,6 @@ import java.security.InvalidKeyException
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
// TODO: Is there a use-case for bare DigitalSignature, or is everything a DigitalSignature.WithKey? If there's no
// actual use-case, we should merge the with key version into the parent class. In that case CompositeSignatureWithKeys
// should be renamed to match.
/** A wrapper around a digital signature. */ /** A wrapper around a digital signature. */
@CordaSerializable @CordaSerializable
@KeepForDJVM @KeepForDJVM

View File

@ -25,7 +25,7 @@ open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSign
*/ */
@Throws(SignatureException::class) @Throws(SignatureException::class)
fun verified(): T { fun verified(): T {
sig.by.verify(raw.bytes, sig) sig.verify(raw)
val data: T = uncheckedCast(raw.deserialize<Any>()) val data: T = uncheckedCast(raw.deserialize<Any>())
verifyData(data) verifyData(data)
return data return data

View File

@ -7,6 +7,17 @@ release, see :doc:`upgrade-notes`.
Unreleased Unreleased
---------- ----------
* ``SwapIdentitiesFlow``, from the experimental confidential-identities module, is now an inlined flow. Instead of passing in a ``Party`` with
whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must
also call ``SwapIdentitiesFlow``. This change was required as the previous API allowed any counterparty to generate anonoymous identities
with a node at will with no checks.
The result type has changed to a simple wrapper class, instead of a Map, to make extracting the identities easier. Also, the wire protocol
of the flow has slightly changed.
.. note:: V3 and V4 of confidential-identities are not compatible and confidential-identities V3 will not work with a V4 Corda node. CorDapps
in such scenarios using confidential-identities must be updated.
* Marked the ``Attachment`` interface as ``@DoNotImplement`` because it is not meant to be extended by CorDapp developers. If you have already * Marked the ``Attachment`` interface as ``@DoNotImplement`` because it is not meant to be extended by CorDapp developers. If you have already
done so, please get in contact on the usual communication channels. done so, please get in contact on the usual communication channels.
@ -33,10 +44,10 @@ Unreleased
un-acknowledged in the message broker. This enables the recovery scenerio whereby any missing CorDapp can be installed and retried on node un-acknowledged in the message broker. This enables the recovery scenerio whereby any missing CorDapp can be installed and retried on node
restart. As a consequence the initiating flow will be blocked until the receiving node has resolved the issue. restart. As a consequence the initiating flow will be blocked until the receiving node has resolved the issue.
* ``FinalityFlow`` is now an inlined flow and no longer requires a handler flow in the counterparty. This is to fix the * ``FinalityFlow`` is now an inlined flow and requires ``FlowSession`` s to each party intended to receive the transaction. This is to fix the
security problem with the handler flow as it accepts any transaction it receives without any checks. Existing CorDapp security problem with the old API that required every node to accept any transaction it received without any checks. Existing CorDapp
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that CorDapps switch to
CorDapps switch to this new API. See :doc:`upgrade-notes` for further details. this new API. See :doc:`upgrade-notes` for further details.
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter * Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter

View File

@ -5,11 +5,11 @@ import net.corda.confidential.SwapIdentitiesFlow
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX
import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_ID import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_ID
@ -49,23 +49,26 @@ open class CashPaymentFlow(
@Suspendable @Suspendable
override fun call(): AbstractCashFlow.Result { override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_ID progressTracker.currentStep = GENERATING_ID
val txIdentities = if (anonymous) { val recipientSession = initiateFlow(recipient)
subFlow(SwapIdentitiesFlow(recipient)) recipientSession.send(anonymous)
val anonymousRecipient = if (anonymous) {
subFlow(SwapIdentitiesFlow(recipientSession)).theirIdentity
} else { } else {
emptyMap<Party, AnonymousParty>() recipient
} }
val anonymousRecipient = txIdentities[recipient] ?: recipient
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary = notary ?: serviceHub.networkMapCache.notaryIdentities.first()) val builder = TransactionBuilder(notary = notary ?: serviceHub.networkMapCache.notaryIdentities.first())
logger.info("Generating spend for: ${builder.lockId}") logger.info("Generating spend for: ${builder.lockId}")
// TODO: Have some way of restricting this to states the caller controls // TODO: Have some way of restricting this to states the caller controls
val (spendTX, keysForSigning) = try { val (spendTX, keysForSigning) = try {
Cash.generateSpend(serviceHub, Cash.generateSpend(
serviceHub,
builder, builder,
amount, amount,
ourIdentityAndCert, ourIdentityAndCert,
anonymousRecipient, anonymousRecipient,
issuerConstraint) issuerConstraint
)
} catch (e: InsufficientBalanceException) { } catch (e: InsufficientBalanceException) {
throw CashException("Insufficient cash for spend: ${e.message}", e) throw CashException("Insufficient cash for spend: ${e.message}", e)
} }
@ -76,8 +79,8 @@ open class CashPaymentFlow(
progressTracker.currentStep = FINALISING_TX progressTracker.currentStep = FINALISING_TX
logger.info("Finalising transaction for: ${tx.id}") logger.info("Finalising transaction for: ${tx.id}")
val sessions = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(initiateFlow(recipient)) val sessionsForFinality = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(recipientSession)
val notarised = finaliseTx(tx, sessions, "Unable to notarise spend") val notarised = finaliseTx(tx, sessionsForFinality, "Unable to notarise spend")
logger.info("Finalised transaction for: ${notarised.id}") logger.info("Finalised transaction for: ${notarised.id}")
return Result(notarised, anonymousRecipient) return Result(notarised, anonymousRecipient)
} }
@ -91,9 +94,16 @@ open class CashPaymentFlow(
} }
@InitiatedBy(CashPaymentFlow::class) @InitiatedBy(CashPaymentFlow::class)
class CashPaymentResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() { class CashPaymentReceiverFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
subFlow(ReceiveFinalityFlow(otherSide)) val anonymous = otherSide.receive<Boolean>().unwrap { it }
if (anonymous) {
subFlow(SwapIdentitiesFlow(otherSide))
}
// Not ideal that we have to do this check, but we must as FinalityFlow does not send locally
if (!serviceHub.myInfo.isLegalIdentity(otherSide.counterparty)) {
subFlow(ReceiveFinalityFlow(otherSide))
}
} }
} }

View File

@ -55,9 +55,7 @@ object TwoPartyDealFlow {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
progressTracker.currentStep = GENERATING_ID progressTracker.currentStep = GENERATING_ID
val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty)) val (anonymousMe, anonymousCounterparty) = subFlow(SwapIdentitiesFlow(otherSideSession))
val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise()
val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise()
// DOCEND 2 // DOCEND 2
progressTracker.currentStep = SENDING_PROPOSAL progressTracker.currentStep = SENDING_PROPOSAL
// Make the first message we'll send to kick off the flow. // Make the first message we'll send to kick off the flow.
@ -131,6 +129,7 @@ object TwoPartyDealFlow {
@Suspendable @Suspendable
private fun receiveAndValidateHandshake(): Handshake<U> { private fun receiveAndValidateHandshake(): Handshake<U> {
subFlow(SwapIdentitiesFlow(otherSideSession))
progressTracker.currentStep = RECEIVING progressTracker.currentStep = RECEIVING
// Wait for a trade request to come in on our pre-provided session ID. // Wait for a trade request to come in on our pre-provided session ID.
val handshake = otherSideSession.receive<Handshake<U>>() val handshake = otherSideSession.receive<Handshake<U>>()

View File

@ -68,7 +68,6 @@ processTestResources {
dependencies { dependencies {
compile project(':node-api') compile project(':node-api')
compile project(":confidential-identities")
compile project(':client:rpc') compile project(':client:rpc')
compile project(':tools:shell') compile project(':tools:shell')
compile project(':tools:cliutils') compile project(':tools:cliutils')

View File

@ -42,13 +42,12 @@ class DistributedServiceTests {
invokeRpc(CordaRPCOps::stateMachinesFeed)) invokeRpc(CordaRPCOps::stateMachinesFeed))
) )
driver(DriverParameters( driver(DriverParameters(
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas", "net.corda.notary.raft"), extraCordappPackagesToScan = listOf("net.corda.finance", "net.corda.notary.raft"),
notarySpecs = listOf( notarySpecs = listOf(NotarySpec(
NotarySpec( DUMMY_NOTARY_NAME,
DUMMY_NOTARY_NAME, rpcUsers = listOf(testUser),
rpcUsers = listOf(testUser), cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity)
cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity)) ))
)
)) { )) {
alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow() alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow()
raftNotaryIdentity = defaultNotaryIdentity raftNotaryIdentity = defaultNotaryIdentity

View File

@ -4,8 +4,6 @@ import com.codahale.metrics.MetricRegistry
import com.google.common.collect.MutableClassToInstanceMap import com.google.common.collect.MutableClassToInstanceMap
import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.MoreExecutors
import com.zaxxer.hikari.pool.HikariPool import com.zaxxer.hikari.pool.HikariPool
import net.corda.confidential.SwapIdentitiesFlow
import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationContext
@ -660,8 +658,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
installFinalityHandler() installFinalityHandler()
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler) flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler) flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
// TODO Make this an inlined flow (and remove this flow mapping!), which should be possible now that FinalityFlow is also inlined
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
} }
// The FinalityHandler is insecure as it blindly accepts any and all transactions into the node's local vault without doing any checks. // The FinalityHandler is insecure as it blindly accepts any and all transactions into the node's local vault without doing any checks.

View File

@ -14,7 +14,6 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.extractFile
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
@ -31,6 +30,7 @@ import net.corda.finance.GBP
import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.internal.security.AuthorizingSubject
import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.Permissions.Companion.startFlow
@ -56,20 +56,22 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.jar.JarInputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
// Mock an AuthorizingSubject instance sticking to a fixed set of permissions // Mock an AuthorizingSubject instance sticking to a fixed set of permissions
private fun buildSubject(principal: String, permissionStrings: Set<String>) = private fun buildSubject(principal: String, permissionStrings: Set<String>): AuthorizingSubject {
RPCSecurityManagerImpl.fromUserList( return RPCSecurityManagerImpl.fromUserList(
id = AuthServiceId("TEST"), id = AuthServiceId("TEST"),
users = listOf(User(username = principal, users = listOf(User(
password = "", username = principal,
permissions = permissionStrings))) password = "",
.buildSubject(principal) permissions = permissionStrings
))
).buildSubject(principal)
}
class CordaRPCOpsImplTest { class CordaRPCOpsImplTest {
private companion object { private companion object {
@ -87,7 +89,7 @@ class CordaRPCOpsImplTest {
@Before @Before
fun setup() { fun setup() {
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts.asset", "net.corda.finance.schemas")) mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.finance"))
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
rpc = aliceNode.rpcOps rpc = aliceNode.rpcOps
CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet()))) CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet())))
@ -163,11 +165,13 @@ class CordaRPCOpsImplTest {
@Test @Test
fun `issue and move`() { fun `issue and move`() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
withPermissions(invokeRpc(CordaRPCOps::stateMachinesFeed), withPermissions(
invokeRpc(CordaRPCOps::stateMachinesFeed),
invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed), invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed),
invokeRpc("vaultTrackBy"), invokeRpc("vaultTrackBy"),
startFlow<CashIssueFlow>(), startFlow<CashIssueFlow>(),
startFlow<CashPaymentFlow>()) { startFlow<CashPaymentFlow>()
) {
aliceNode.database.transaction { aliceNode.database.transaction {
stateMachineUpdates = rpc.stateMachinesFeed().updates stateMachineUpdates = rpc.stateMachinesFeed().updates
transactions = rpc.internalVerifiedTransactionsFeed().updates transactions = rpc.internalVerifiedTransactionsFeed().updates
@ -183,7 +187,8 @@ class CordaRPCOpsImplTest {
mockNet.runNetwork() mockNet.runNetwork()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null var paymentSmId: StateMachineRunId? = null
var paymentRecSmId: StateMachineRunId? = null
stateMachineUpdates.expectEvents { stateMachineUpdates.expectEvents {
sequence( sequence(
// ISSUE // ISSUE
@ -191,14 +196,20 @@ class CordaRPCOpsImplTest {
issueSmId = add.id issueSmId = add.id
}, },
expect { remove: StateMachineUpdate.Removed -> expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId) assertThat(remove.id).isEqualTo(issueSmId)
}, },
// MOVE // PAYMENT
expect { add: StateMachineUpdate.Added -> expect { add: StateMachineUpdate.Added ->
moveSmId = add.id paymentSmId = add.id
},
expect { add: StateMachineUpdate.Added ->
paymentRecSmId = add.id
}, },
expect { remove: StateMachineUpdate.Removed -> expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId) assertThat(remove.id).isEqualTo(paymentRecSmId)
},
expect { remove: StateMachineUpdate.Removed ->
assertThat(remove.id).isEqualTo(paymentSmId)
} }
) )
} }