CORDA-1724 Add tx access right validity check during resolution (#3732)

* CORDA-1724 Add tx access right validity check during resolution

* CORDA-1724 Fix missing payload type

* CORDA-1724 Fix test

* CORDA-1724 Add test

* CORDA-1865 Improve comments

* CORDA-1724 Address code review comments
This commit is contained in:
Tudor Malene 2018-08-02 08:26:58 +01:00 committed by GitHub
parent 56c0067540
commit 055ba90e0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 14 deletions

View File

@ -2,8 +2,10 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.readFully import net.corda.core.internal.readFully
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
@ -42,6 +44,25 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
override fun call(): Void? { override fun call(): Void? {
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data. // The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
var payload = payload var payload = payload
// Depending on who called this flow, the type of the initial payload is different.
// The authorisation logic is to maintain a dynamic list of transactions that the caller is authorised to make based on the transactions that were made already.
// Each time an authorised transaction is requested, the input transactions are added to the list.
// Once a transaction has been requested, it will be removed from the authorised list. This means that it is a protocol violation to request a transaction twice.
val authorisedTransactions = when (payload) {
is NotarisationPayload -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload.signedTransaction))
is SignedTransaction -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload))
is RetrieveAnyTransactionPayload -> TransactionAuthorisationFilter(acceptAll = true)
is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { stateAndRef ->
if (stateAndRef is StateAndRef<*>) {
getInputTransactions(serviceHub.validatedTransactions.getTransaction(stateAndRef.ref.txhash)!!) + stateAndRef.ref.txhash
} else {
throw Exception("Unknown payload type: ${stateAndRef!!::class.java} ?")
}
}.toSet())
else -> throw Exception("Unknown payload type: ${payload::class.java} ?")
}
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need // This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of // to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of
// data request. // data request.
@ -56,14 +77,47 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
FetchDataFlow.Request.End -> return null FetchDataFlow.Request.End -> return null
} }
} }
payload = when (dataRequest.dataType) { payload = when (dataRequest.dataType) {
FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId ->
serviceHub.validatedTransactions.getTransaction(it) ?: throw FetchDataFlow.HashNotFound(it) if (!authorisedTransactions.isAuthorised(txId)) {
throw FetchDataFlow.IllegalTransactionRequest(txId)
}
val tx = serviceHub.validatedTransactions.getTransaction(txId)
?: throw FetchDataFlow.HashNotFound(txId)
authorisedTransactions.removeAuthorised(tx.id)
authorisedTransactions.addAuthorised(getInputTransactions(tx))
tx
} }
FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map { FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map {
serviceHub.attachments.openAttachment(it)?.open()?.readFully() ?: throw FetchDataFlow.HashNotFound(it) serviceHub.attachments.openAttachment(it)?.open()?.readFully()
?: throw FetchDataFlow.HashNotFound(it)
} }
} }
} }
} }
@Suspendable
private fun getInputTransactions(tx: SignedTransaction): Set<SecureHash> = tx.inputs.map { it.txhash }.toSet()
private class TransactionAuthorisationFilter(private val authorisedTransactions: MutableSet<SecureHash> = mutableSetOf(), val acceptAll: Boolean = false) {
fun isAuthorised(txId: SecureHash) = acceptAll || authorisedTransactions.contains(txId)
fun addAuthorised(txs: Set<SecureHash>): TransactionAuthorisationFilter {
authorisedTransactions.addAll(txs)
return this
}
fun removeAuthorised(txId: SecureHash) {
authorisedTransactions.remove(txId)
}
}
} }
/**
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
*
* Todo Fails with a serialization exception if it is not a list. Why?
*/
@CordaSerializable
object RetrieveAnyTransactionPayload : ArrayList<Any>()

View File

@ -50,6 +50,8 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
class HashNotFound(val requested: SecureHash) : FlowException() class HashNotFound(val requested: SecureHash) : FlowException()
class IllegalTransactionRequest(val requested: SecureHash) : FlowException("Illegal attempt to request a transaction (${requested}) that is not in the transitive dependency graph of the sent transaction.")
@CordaSerializable @CordaSerializable
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>) data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
@ -179,9 +181,11 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
* Given a set of tx hashes (IDs), either loads them from local disk or asks the remote peer to provide them. * Given a set of tx hashes (IDs), either loads them from local disk or asks the remote peer to provide them.
* *
* A malicious response in which the data provided by the remote peer does not hash to the requested hash results in * A malicious response in which the data provided by the remote peer does not hash to the requested hash results in
* [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown. If the remote peer doesn't have an entry, it * [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown.
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into * If the remote peer doesn't have an entry, it results in a [FetchDataFlow.HashNotFound] exception.
* the database, because it's up to the caller to actually verify the transactions are valid. * If the remote peer is not authorized to request this transaction, it results in a [FetchDataFlow.IllegalTransactionRequest] exception.
* Authorisation is accorded only on valid ancestors of the root transation.
* Note that returned transactions are not inserted into the database, because it's up to the caller to actually verify the transactions are valid.
*/ */
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) : class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) { FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {

View File

@ -72,7 +72,7 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
} }
@Suspendable @Suspendable
@Throws(FetchDataFlow.HashNotFound::class) @Throws(FetchDataFlow.HashNotFound::class, FetchDataFlow.IllegalTransactionRequest::class)
override fun call() { override fun call() {
val newTxns = ArrayList<SignedTransaction>(txHashes.size) val newTxns = ArrayList<SignedTransaction>(txHashes.size)
// Start fetching data. // Start fetching data.

View File

@ -116,7 +116,7 @@ class AttachmentTests : WithMockNet {
@InitiatedBy(InitiatingFetchAttachmentsFlow::class) @InitiatedBy(InitiatingFetchAttachmentsFlow::class)
private class FetchAttachmentsResponse(val otherSideSession: FlowSession) : FlowLogic<Void?>() { private class FetchAttachmentsResponse(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
@Suspendable @Suspendable
override fun call() = subFlow(TestDataVendingFlow(otherSideSession)) override fun call() = subFlow(TestNoSecurityDataVendingFlow(otherSideSession))
} }
//region Generators //region Generators

View File

@ -5,7 +5,7 @@ import net.corda.core.internal.FetchDataFlow
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
// Flow to start data vending without sending transaction. For testing only. // Flow to start data vending without sending transaction. For testing only.
class TestDataVendingFlow(otherSideSession: FlowSession) : SendStateAndRefFlow(otherSideSession, emptyList()) { class TestNoSecurityDataVendingFlow(otherSideSession: FlowSession) : DataVendingFlow(otherSideSession, RetrieveAnyTransactionPayload) {
@Suspendable @Suspendable
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> { override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
return if (payload is List<*> && payload.isEmpty()) { return if (payload is List<*> && payload.isEmpty()) {

View File

@ -6,8 +6,10 @@ import net.corda.core.flows.*
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.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.sequence import net.corda.core.utilities.sequence
import net.corda.core.utilities.unwrap
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
@ -34,6 +36,8 @@ class ResolveTransactionsFlowTest {
private lateinit var miniCorp: Party private lateinit var miniCorp: Party
private lateinit var notary: Party private lateinit var notary: Party
private lateinit var rootTx: SignedTransaction
@Before @Before
fun setup() { fun setup() {
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.internal")) mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.internal"))
@ -160,6 +164,34 @@ class ResolveTransactionsFlowTest {
} }
} }
@Test
fun `Requesting a transaction while having the right to see it succeeds`() {
val (_, stx2) = makeTransactions()
val p = TestNoRightsVendingFlow(miniCorp, toVend = stx2, toRequest = stx2)
val future = megaCorpNode.startFlow(p)
mockNet.runNetwork()
future.getOrThrow()
}
@Test
fun `Requesting a transaction without having the right to see it results in exception`() {
val (_, stx2) = makeTransactions()
val (_, stx3) = makeTransactions()
val p = TestNoRightsVendingFlow(miniCorp, toVend = stx2, toRequest = stx3)
val future = megaCorpNode.startFlow(p)
mockNet.runNetwork()
assertFailsWith<FetchDataFlow.IllegalTransactionRequest> { future.getOrThrow() }
}
@Test
fun `Requesting a transaction twice results in exception`() {
val (_, stx2) = makeTransactions()
val p = TestResolveTwiceVendingFlow(miniCorp, stx2)
val future = megaCorpNode.startFlow(p)
mockNet.runNetwork()
assertFailsWith<FetchDataFlow.IllegalTransactionRequest> { future.getOrThrow() }
}
// DOCSTART 2 // DOCSTART 2
private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> { private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> {
// Make a chain of custody of dummy states and insert into node A. // Make a chain of custody of dummy states and insert into node A.
@ -187,8 +219,9 @@ class ResolveTransactionsFlowTest {
} }
// DOCEND 2 // DOCEND 2
@InitiatingFlow @InitiatingFlow
private class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() { private open class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() {
constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(otherSide, { ResolveTransactionsFlow(txHashes, it) }, txCountLimit = txCountLimit) constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(otherSide, { ResolveTransactionsFlow(txHashes, it) }, txCountLimit = txCountLimit)
constructor(stx: SignedTransaction, otherSide: Party) : this(otherSide, { ResolveTransactionsFlow(stx, it) }) constructor(stx: SignedTransaction, otherSide: Party) : this(otherSide, { ResolveTransactionsFlow(stx, it) })
@ -200,11 +233,54 @@ class ResolveTransactionsFlowTest {
subFlow(resolveTransactionsFlow) subFlow(resolveTransactionsFlow)
} }
} }
@Suppress("unused") @Suppress("unused")
@InitiatedBy(TestFlow::class) @InitiatedBy(TestFlow::class)
private class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() { private class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
@Suspendable @Suspendable
override fun call() = subFlow(TestDataVendingFlow(otherSideSession)) override fun call() = subFlow(TestNoSecurityDataVendingFlow(otherSideSession))
}
// Used by the no-rights test
@InitiatingFlow
private class TestNoRightsVendingFlow(val otherSide: Party, val toVend: SignedTransaction, val toRequest: SignedTransaction) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(otherSide)
session.send(toRequest)
subFlow(DataVendingFlow(session, toVend))
}
}
@Suppress("unused")
@InitiatedBy(TestNoRightsVendingFlow::class)
private open class TestResponseResolveNoRightsFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val noRightsTx = otherSideSession.receive<SignedTransaction>().unwrap { it }
otherSideSession.receive<Any>().unwrap { it }
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(noRightsTx.inputs.first().txhash), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
otherSideSession.send(FetchDataFlow.Request.End)
}
}
//Used by the resolve twice test
@InitiatingFlow
private class TestResolveTwiceVendingFlow(val otherSide: Party, val tx: SignedTransaction) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(otherSide)
subFlow(DataVendingFlow(session, tx))
}
}
@Suppress("unused")
@InitiatedBy(TestResolveTwiceVendingFlow::class)
private open class TestResponseResolveTwiceFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val tx = otherSideSession.receive<SignedTransaction>().unwrap { it }
val parent1 = tx.inputs.first().txhash
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(parent1), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(parent1), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
otherSideSession.send(FetchDataFlow.Request.End)
}
} }
} }

View File

@ -6,7 +6,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.TestDataVendingFlow import net.corda.core.flows.TestNoSecurityDataVendingFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
@ -89,7 +89,7 @@ class AttachmentSerializationTest {
@Suspendable @Suspendable
override fun call() { override fun call() {
if (sendData) { if (sendData) {
subFlow(TestDataVendingFlow(clientSession)) subFlow(TestNoSecurityDataVendingFlow(clientSession))
} }
clientSession.receive<String>().unwrap { assertEquals("ping one", it) } clientSession.receive<String>().unwrap { assertEquals("ping one", it) }
clientSession.sendAndReceive<String>("pong").unwrap { assertEquals("ping two", it) } clientSession.sendAndReceive<String>("pong").unwrap { assertEquals("ping two", it) }