mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
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:
parent
b01f278eb3
commit
3b8a74fe44
@ -1,118 +1,106 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.verify
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
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.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
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 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
|
||||
* transaction.
|
||||
* transaction. The flow running on the other side must also call this flow at the correct location.
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
// 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())
|
||||
|
||||
class SwapIdentitiesFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SwapIdentitiesFlow.AnonymousResult>() {
|
||||
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
|
||||
* 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
|
||||
* verified against the expected blob, rather than exchanging the blob.
|
||||
*/
|
||||
fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray {
|
||||
val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey)
|
||||
return certOwnerAssert.serialize().bytes
|
||||
@CordaInternal
|
||||
@VisibleForTesting
|
||||
internal fun buildDataToSign(identity: PartyAndCertificate): ByteArray {
|
||||
return CertificateOwnershipAssertion(identity.name, identity.owningKey).serialize().bytes
|
||||
}
|
||||
|
||||
@Throws(SwapIdentitiesException::class)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService,
|
||||
otherSide: Party,
|
||||
anonymousOtherSideBytes: PartyAndCertificate,
|
||||
sigBytes: DigitalSignature): PartyAndCertificate {
|
||||
val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes
|
||||
if (anonymousOtherSide.name != otherSide.name) {
|
||||
@CordaInternal
|
||||
@VisibleForTesting
|
||||
internal fun validateAndRegisterIdentity(serviceHub: ServiceHub,
|
||||
otherSide: Party,
|
||||
theirAnonymousIdentity: PartyAndCertificate,
|
||||
signature: DigitalSignature): PartyAndCertificate {
|
||||
if (theirAnonymousIdentity.name != otherSide.name) {
|
||||
throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.")
|
||||
}
|
||||
val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes)
|
||||
try {
|
||||
signature.verify(buildDataToSign(anonymousOtherSideBytes))
|
||||
} catch(ex: SignatureException) {
|
||||
theirAnonymousIdentity.owningKey.verify(buildDataToSign(theirAnonymousIdentity), signature)
|
||||
} catch (ex: SignatureException) {
|
||||
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
|
||||
// counterparty.
|
||||
identityService.verifyAndRegisterIdentity(anonymousOtherSide)
|
||||
return anonymousOtherSide
|
||||
// Validate then store their identity so that we can prove the key in the transaction is owned by the counterparty.
|
||||
serviceHub.identityService.verifyAndRegisterIdentity(theirAnonymousIdentity)
|
||||
return theirAnonymousIdentity
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
progressTracker.currentStep = AWAITING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(legalIdentityAnonymous.serialize().bytes)
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated.
|
||||
// TODO: for increased privacy, we should create one anonymous key per output state.
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||
identities[otherParty] = legalIdentityAnonymous.party.anonymise()
|
||||
} else {
|
||||
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()
|
||||
override fun call(): AnonymousResult {
|
||||
progressTracker.currentStep = GENERATING_IDENTITY
|
||||
val ourAnonymousIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
val data = buildDataToSign(ourAnonymousIdentity)
|
||||
progressTracker.currentStep = SIGNING_IDENTITY
|
||||
val signature = serviceHub.keyManagementService.sign(data, ourAnonymousIdentity.owningKey).withoutKey()
|
||||
val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity, signature)
|
||||
progressTracker.currentStep = AWAITING_IDENTITY
|
||||
val theirAnonymousIdentity = otherSideSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
|
||||
progressTracker.currentStep = VERIFYING_IDENTITY
|
||||
validateAndRegisterIdentity(serviceHub, otherSideSession.counterparty, theirIdentWithSig.identity, theirIdentWithSig.signature)
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null) : FlowException(message, cause)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,40 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.MatchResult
|
||||
import com.natpryce.hamkrest.Matcher
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
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.internal.matchers.allOf
|
||||
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.node.internal.InternalMockNetwork
|
||||
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.Test
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class SwapIdentitiesFlowTests {
|
||||
companion object {
|
||||
private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||
private val mockNet = InternalMockNetwork(
|
||||
networkSendManuallyPumped = false,
|
||||
threadPerNode = true,
|
||||
cordappsForAllNodes = cordappsForPackages(this::class.packageName)
|
||||
)
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
@ -36,7 +50,7 @@ class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `issue key`() {
|
||||
assert.that(
|
||||
aliceNode.services.startFlow(SwapIdentitiesFlow(bob)),
|
||||
aliceNode.services.startFlow(SwapIdentitiesInitiator(bob)),
|
||||
willReturn(
|
||||
hasOnlyEntries(
|
||||
alice to allOf(
|
||||
@ -102,21 +116,20 @@ class SwapIdentitiesFlowTests {
|
||||
services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false)
|
||||
}
|
||||
|
||||
private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey) =
|
||||
services.keyManagementService.sign(
|
||||
SwapIdentitiesFlow.buildDataToSign(party),
|
||||
owningKey)
|
||||
private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey): DigitalSignature.WithKey {
|
||||
return services.keyManagementService.sign(SwapIdentitiesFlow.buildDataToSign(party), owningKey)
|
||||
}
|
||||
|
||||
private fun TestStartedNode.validateSwapIdentitiesFlow(
|
||||
party: Party,
|
||||
counterparty: PartyAndCertificate,
|
||||
signature: DigitalSignature.WithKey) =
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(
|
||||
services.identityService,
|
||||
party,
|
||||
counterparty,
|
||||
signature.withoutKey()
|
||||
)
|
||||
private fun TestStartedNode.validateSwapIdentitiesFlow(party: Party,
|
||||
counterparty: PartyAndCertificate,
|
||||
signature: DigitalSignature.WithKey): PartyAndCertificate {
|
||||
return SwapIdentitiesFlow.validateAndRegisterIdentity(
|
||||
services,
|
||||
party,
|
||||
counterparty,
|
||||
signature.withoutKey()
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Matchers
|
||||
@ -141,21 +154,36 @@ class SwapIdentitiesFlowTests {
|
||||
override val description =
|
||||
"has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}"
|
||||
|
||||
override fun invoke(actual: AnonymousParty) =
|
||||
if (negated != actual.owningKey in node.services.keyManagementService.keys) {
|
||||
MatchResult.Match
|
||||
} else {
|
||||
MatchResult.Mismatch("""
|
||||
had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
override fun not(): Matcher<AnonymousParty> {
|
||||
return copy(negated=!negated)
|
||||
override fun invoke(actual: AnonymousParty): MatchResult {
|
||||
return if (negated != actual.owningKey in node.services.keyManagementService.keys) {
|
||||
MatchResult.Match
|
||||
} else {
|
||||
MatchResult.Mismatch("""
|
||||
had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
override fun not(): Matcher<AnonymousParty> = copy(negated=!negated)
|
||||
}
|
||||
|
||||
private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this)
|
||||
//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))
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,6 @@ import java.security.InvalidKeyException
|
||||
import java.security.PublicKey
|
||||
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. */
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
|
@ -25,7 +25,7 @@ open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSign
|
||||
*/
|
||||
@Throws(SignatureException::class)
|
||||
fun verified(): T {
|
||||
sig.by.verify(raw.bytes, sig)
|
||||
sig.verify(raw)
|
||||
val data: T = uncheckedCast(raw.deserialize<Any>())
|
||||
verifyData(data)
|
||||
return data
|
||||
|
@ -7,6 +7,17 @@ release, see :doc:`upgrade-notes`.
|
||||
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
|
||||
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
|
||||
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
|
||||
security problem with the handler flow as it accepts any transaction it receives without any checks. Existing CorDapp
|
||||
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that
|
||||
CorDapps switch to this new API. See :doc:`upgrade-notes` for further details.
|
||||
* ``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 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 CorDapps switch to
|
||||
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
|
||||
|
||||
|
@ -5,11 +5,11 @@ import net.corda.confidential.SwapIdentitiesFlow
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX
|
||||
import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_ID
|
||||
@ -49,23 +49,26 @@ open class CashPaymentFlow(
|
||||
@Suspendable
|
||||
override fun call(): AbstractCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_ID
|
||||
val txIdentities = if (anonymous) {
|
||||
subFlow(SwapIdentitiesFlow(recipient))
|
||||
val recipientSession = initiateFlow(recipient)
|
||||
recipientSession.send(anonymous)
|
||||
val anonymousRecipient = if (anonymous) {
|
||||
subFlow(SwapIdentitiesFlow(recipientSession)).theirIdentity
|
||||
} else {
|
||||
emptyMap<Party, AnonymousParty>()
|
||||
recipient
|
||||
}
|
||||
val anonymousRecipient = txIdentities[recipient] ?: recipient
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(notary = notary ?: serviceHub.networkMapCache.notaryIdentities.first())
|
||||
logger.info("Generating spend for: ${builder.lockId}")
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
Cash.generateSpend(serviceHub,
|
||||
Cash.generateSpend(
|
||||
serviceHub,
|
||||
builder,
|
||||
amount,
|
||||
ourIdentityAndCert,
|
||||
anonymousRecipient,
|
||||
issuerConstraint)
|
||||
issuerConstraint
|
||||
)
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Insufficient cash for spend: ${e.message}", e)
|
||||
}
|
||||
@ -76,8 +79,8 @@ open class CashPaymentFlow(
|
||||
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
logger.info("Finalising transaction for: ${tx.id}")
|
||||
val sessions = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(initiateFlow(recipient))
|
||||
val notarised = finaliseTx(tx, sessions, "Unable to notarise spend")
|
||||
val sessionsForFinality = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(recipientSession)
|
||||
val notarised = finaliseTx(tx, sessionsForFinality, "Unable to notarise spend")
|
||||
logger.info("Finalised transaction for: ${notarised.id}")
|
||||
return Result(notarised, anonymousRecipient)
|
||||
}
|
||||
@ -91,9 +94,16 @@ open class CashPaymentFlow(
|
||||
}
|
||||
|
||||
@InitiatedBy(CashPaymentFlow::class)
|
||||
class CashPaymentResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
class CashPaymentReceiverFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,9 +55,7 @@ object TwoPartyDealFlow {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = GENERATING_ID
|
||||
val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty))
|
||||
val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise()
|
||||
val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise()
|
||||
val (anonymousMe, anonymousCounterparty) = subFlow(SwapIdentitiesFlow(otherSideSession))
|
||||
// DOCEND 2
|
||||
progressTracker.currentStep = SENDING_PROPOSAL
|
||||
// Make the first message we'll send to kick off the flow.
|
||||
@ -131,6 +129,7 @@ object TwoPartyDealFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateHandshake(): Handshake<U> {
|
||||
subFlow(SwapIdentitiesFlow(otherSideSession))
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val handshake = otherSideSession.receive<Handshake<U>>()
|
||||
|
@ -68,7 +68,6 @@ processTestResources {
|
||||
|
||||
dependencies {
|
||||
compile project(':node-api')
|
||||
compile project(":confidential-identities")
|
||||
compile project(':client:rpc')
|
||||
compile project(':tools:shell')
|
||||
compile project(':tools:cliutils')
|
||||
|
@ -42,13 +42,12 @@ class DistributedServiceTests {
|
||||
invokeRpc(CordaRPCOps::stateMachinesFeed))
|
||||
)
|
||||
driver(DriverParameters(
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas", "net.corda.notary.raft"),
|
||||
notarySpecs = listOf(
|
||||
NotarySpec(
|
||||
DUMMY_NOTARY_NAME,
|
||||
rpcUsers = listOf(testUser),
|
||||
cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity))
|
||||
)
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance", "net.corda.notary.raft"),
|
||||
notarySpecs = listOf(NotarySpec(
|
||||
DUMMY_NOTARY_NAME,
|
||||
rpcUsers = listOf(testUser),
|
||||
cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity)
|
||||
))
|
||||
)) {
|
||||
alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow()
|
||||
raftNotaryIdentity = defaultNotaryIdentity
|
||||
|
@ -4,8 +4,6 @@ import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.collect.MutableClassToInstanceMap
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
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.concurrent.CordaFuture
|
||||
import net.corda.core.context.InvocationContext
|
||||
@ -660,8 +658,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
installFinalityHandler()
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
||||
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.
|
||||
|
@ -14,7 +14,6 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.extractFile
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.*
|
||||
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.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.internal.security.AuthorizingSubject
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
@ -56,20 +56,22 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
// Mock an AuthorizingSubject instance sticking to a fixed set of permissions
|
||||
private fun buildSubject(principal: String, permissionStrings: Set<String>) =
|
||||
RPCSecurityManagerImpl.fromUserList(
|
||||
id = AuthServiceId("TEST"),
|
||||
users = listOf(User(username = principal,
|
||||
password = "",
|
||||
permissions = permissionStrings)))
|
||||
.buildSubject(principal)
|
||||
private fun buildSubject(principal: String, permissionStrings: Set<String>): AuthorizingSubject {
|
||||
return RPCSecurityManagerImpl.fromUserList(
|
||||
id = AuthServiceId("TEST"),
|
||||
users = listOf(User(
|
||||
username = principal,
|
||||
password = "",
|
||||
permissions = permissionStrings
|
||||
))
|
||||
).buildSubject(principal)
|
||||
}
|
||||
|
||||
class CordaRPCOpsImplTest {
|
||||
private companion object {
|
||||
@ -87,7 +89,7 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
@Before
|
||||
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))
|
||||
rpc = aliceNode.rpcOps
|
||||
CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet())))
|
||||
@ -163,11 +165,13 @@ class CordaRPCOpsImplTest {
|
||||
@Test
|
||||
fun `issue and move`() {
|
||||
@Suppress("DEPRECATION")
|
||||
withPermissions(invokeRpc(CordaRPCOps::stateMachinesFeed),
|
||||
withPermissions(
|
||||
invokeRpc(CordaRPCOps::stateMachinesFeed),
|
||||
invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed),
|
||||
invokeRpc("vaultTrackBy"),
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>()) {
|
||||
startFlow<CashPaymentFlow>()
|
||||
) {
|
||||
aliceNode.database.transaction {
|
||||
stateMachineUpdates = rpc.stateMachinesFeed().updates
|
||||
transactions = rpc.internalVerifiedTransactionsFeed().updates
|
||||
@ -183,7 +187,8 @@ class CordaRPCOpsImplTest {
|
||||
mockNet.runNetwork()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
var paymentSmId: StateMachineRunId? = null
|
||||
var paymentRecSmId: StateMachineRunId? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
@ -191,14 +196,20 @@ class CordaRPCOpsImplTest {
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
assertThat(remove.id).isEqualTo(issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
// PAYMENT
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
paymentSmId = add.id
|
||||
},
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
paymentRecSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
assertThat(remove.id).isEqualTo(paymentRecSmId)
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
assertThat(remove.id).isEqualTo(paymentSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user