CORDA-2334 - Net Params non-downgrade verification in transaction resolution / building. (#4351)

* Add FetchParametersFlow

* No downgrade parameters in ResolveTransactionsFlow

Make sure that parameters in the transaction
graph are ordered (this is to prevent the downgrade attack, when the
malicious notary and participants sign transaction that shouldn't be
notarised otherwise). We ensure that by checking that epochs of network
parameters in the transaction chain are ordered.

* Addressed some minor items from RP review feedback.

* Refactoring following rebase from master.

* Address RP PR review comments (round 2)

* Addressed a couple of minor PR review points.

* Renaming of unit tests and cleanup.

* Changes discusses with RP to ensure Network Param checking is applied at txn verify time + resolve order checking gated on existence of tagged NPs in txn and associated minimum platform version.

* Do not fail on missing ServiceHub impl + return nothing if txn not NP tagged.

* Unify HistoricNetworkParametersStorage and
NetworkParametersStorageInternal

* SignedDataWithCert implements NamedByHash

* Cleanup

* Move parameters ordering check to signed transaction resolution

* Fixes after merge, address comments

* Address Andrius comments
This commit is contained in:
Katarzyna Streich 2019-02-08 15:29:32 +00:00 committed by Shams Asari
parent db35f73bcc
commit f729453fee
7 changed files with 170 additions and 30 deletions

View File

@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NonEmptySet
@ -189,6 +190,27 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
""".trimIndent(), null)
/**
* If the network parameters associated with an input or reference state in a transaction are more recent than the network parameters of the new transaction itself.
*/
@KeepForDJVM
class TransactionNetworkParameterOrderingException(txId: SecureHash, inputStateRef: StateRef, txnNetworkParameters: NetworkParameters, inputNetworkParameters: NetworkParameters)
: TransactionVerificationException(txId, "The network parameters epoch (${txnNetworkParameters.epoch}) of this transaction " +
"is older than the epoch (${inputNetworkParameters.epoch}) of input state: $inputStateRef", null)
/**
* Thrown when the network parameters with hash: missingNetworkParametersHash is not available at this node. Usually all the parameters
* that are in the resolution chain for transaction with txId should be fetched from peer via [FetchParametersFlow] or from network map.
*
* @param txId Id of the transaction that has missing parameters hash in the resolution chain
* @param missingNetworkParametersHash Missing hash of the network parameters associated to this transaction
*/
@KeepForDJVM
class MissingNetworkParametersException(txId: SecureHash, missingNetworkParametersHash: SecureHash)
: TransactionVerificationException(txId, "Couldn't find network parameters with hash: $missingNetworkParametersHash related to this transaction: $txId", null)
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
@CordaSerializable
@KeepForDJVM

View File

@ -2,10 +2,13 @@ package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.node.ServiceHub
import net.corda.core.node.StatesToRecord
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.ContractUpgradeWireTransaction
@ -28,6 +31,8 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
private val txHashes = txHashesArg.toList()
/** Transaction to fetch attachments for. */
private var signedTransaction: SignedTransaction? = null
/**
* Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does
@ -63,9 +68,6 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
@CordaSerializable
class ExcessivelyLargeTransactionGraph : FlowException()
/** Transaction to fetch attachments for. */
private var signedTransaction: SignedTransaction? = null
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess.
/** The maximum number of transactions this flow will try to download before bailing out. */
var transactionCountLimit = 5000
@ -77,15 +79,16 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
@Suspendable
@Throws(FetchDataFlow.HashNotFound::class, FetchDataFlow.IllegalTransactionRequest::class)
override fun call() {
val counterpartyPlatformVersion = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSide.counterparty)?.platformVersion ?:
throw FlowException("Couldn't retrieve party's ${otherSide.counterparty} platform version from NetworkMapCache")
val counterpartyPlatformVersion = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSide.counterparty)?.platformVersion
?: throw FlowException("Couldn't retrieve party's ${otherSide.counterparty} platform version from NetworkMapCache")
val newTxns = ArrayList<SignedTransaction>(txHashes.size)
// Start fetching data.
for (pageNumber in 0..(txHashes.size - 1) / RESOLUTION_PAGE_SIZE) {
val page = page(pageNumber, RESOLUTION_PAGE_SIZE)
newTxns += downloadDependencies(page)
val txsWithMissingAttachments = if (pageNumber == 0) signedTransaction?.let { newTxns + it } ?: newTxns else newTxns
val txsWithMissingAttachments = if (pageNumber == 0) signedTransaction?.let { newTxns + it }
?: newTxns else newTxns
fetchMissingAttachments(txsWithMissingAttachments)
// Fetch missing parameters flow was added in version 4. This check is needed so we don't end up with node V4 sending parameters
// request to node V3 that doesn't know about this protocol.
@ -160,6 +163,7 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
// Add all input states and reference input states to the work queue.
val inputHashes = downloads.flatMap { it.inputs + it.references }.map { it.txhash }
nextRequests.addAll(inputHashes)
limitCounter = limitCounter exactAdd nextRequests.size

View File

@ -157,6 +157,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
} else {
checkSignaturesAreValid()
}
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
resolveAndCheckNetworkParameters(services)
return tx.toLedgerTransaction(services)
}
@ -174,6 +176,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@DeleteForDJVM
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
resolveAndCheckNetworkParameters(services)
when (coreTransaction) {
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures)
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures)
@ -181,6 +184,22 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
}
}
@DeleteForDJVM
private fun resolveAndCheckNetworkParameters(services: ServiceHub) {
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
?: throw TransactionResolutionException(id)
val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash }
groupedInputsAndRefs.map { entry ->
val tx = services.validatedTransactions.getTransaction(entry.key)?.coreTransaction
?: throw TransactionResolutionException(id)
val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash
val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id)
if (txNetworkParameters.epoch < params.epoch)
throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params)
}
}
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
@DeleteForDJVM
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {

View File

@ -190,7 +190,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException(id)
//keep resolvedInputs lazy and resolve the inputs separately here to get Version
// Keep resolvedInputs lazy and resolve the inputs separately here to get Version.
val inputStateContractClassToStateRefs: Map<ContractClassName, List<StateAndRef<ContractState>>> = serializedResolvedInputs.map {
it.toStateAndRef()
}.groupBy { it.state.contract }

View File

@ -1,5 +1,6 @@
package net.corda.core.internal
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
@ -8,7 +9,6 @@ import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
@ -25,12 +25,16 @@ import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
import net.corda.testing.node.internal.cordappForClasses
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
class NetworkParametersResolutionTest {
private lateinit var defaultParams: NetworkParameters
private lateinit var params2: NetworkParameters
private lateinit var params3: NetworkParameters
private val certKeyPair: CertificateAndKeyPair = createDevNetworkMapCa()
private lateinit var mockNet: MockNetwork
private lateinit var notaryNode: StartedMockNode
@ -43,14 +47,16 @@ class NetworkParametersResolutionTest {
@Before
fun setup() {
mockNet = MockNetwork(MockNetworkParameters(
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, cordappForClasses(ResolveTransactionsFlowTest.TestFlow::class.java, ResolveTransactionsFlowTest.TestResponseFlow::class.java))))
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, cordappForClasses(ResolveTransactionsFlowTest.TestFlow::class.java, ResolveTransactionsFlowTest.TestResponseFlow::class.java))))
notaryNode = mockNet.defaultNotaryNode
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB"))
notaryParty = mockNet.defaultNotaryIdentity
megaCorpParty = megaCorpNode.info.singleIdentity()
miniCorpParty = miniCorpNode.info.singleIdentity()
defaultParams = miniCorpNode.services.networkParameters
params2 = testNetworkParameters(epoch = 2, minimumPlatformVersion = 3, notaries = listOf((NotaryInfo(notaryParty, true))))
params3 = testNetworkParameters(epoch = 3, minimumPlatformVersion = 4, notaries = listOf((NotaryInfo(notaryParty, true))))
}
@After
@ -61,20 +67,18 @@ class NetworkParametersResolutionTest {
// This function is resolving and signing WireTransaction with special parameters.
private fun TransactionBuilder.toSignedTransactionWithParameters(parameters: NetworkParameters?, services: ServiceHub): SignedTransaction {
val wtx = toWireTransaction(services)
val wtxWithHash = SerializationFactory.defaultFactory.withCurrentContext(null) {
WireTransaction(
createComponentGroups(
wtx.inputs,
wtx.outputs,
wtx.commands,
wtx.attachments,
wtx.notary,
wtx.timeWindow,
wtx.references,
parameters?.serialize()?.hash),
wtx.privacySalt
)
}
val wtxWithHash = WireTransaction(
createComponentGroups(
wtx.inputs,
wtx.outputs,
wtx.commands,
wtx.attachments,
wtx.notary,
wtx.timeWindow,
wtx.references,
parameters?.serialize()?.hash),
wtx.privacySalt
)
val publicKey = services.myInfo.singleIdentity().owningKey
val signatureMetadata = SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(publicKey).schemeNumberID)
val signableData = SignableData(wtxWithHash.id, signatureMetadata)
@ -105,13 +109,52 @@ class NetworkParametersResolutionTest {
return Pair(dummy1, dummy2)
}
@Test
fun `parameters all null`() {
val (stx1, stx2) = makeTransactions(null, null)
assertThat(stx1.networkParametersHash).isNull()
assertThat(stx2.networkParametersHash).isNull()
val p = ResolveTransactionsFlowTest.TestFlow(setOf(stx2.id), megaCorpParty)
val future = miniCorpNode.startFlow(p)
mockNet.runNetwork()
future.getOrThrow()
miniCorpNode.transaction {
assertEquals(stx1, miniCorpNode.services.validatedTransactions.getTransaction(stx1.id))
assertEquals(stx2, miniCorpNode.services.validatedTransactions.getTransaction(stx2.id))
}
}
@Test
fun `transaction chain out of order parameters`() {
val hash2 = params2.serialize().hash
val hash3 = params3.serialize().hash
val (stx1, stx2) = makeTransactions(params3, params2)
assertThat(stx1.networkParametersHash).isEqualTo(hash3)
assertThat(stx2.networkParametersHash).isEqualTo(hash2)
miniCorpNode.transaction {
assertThat(miniCorpNode.services.networkParametersService.lookup(hash2)).isNull()
assertThat(miniCorpNode.services.networkParametersService.lookup(hash3)).isNull()
}
val p = ResolveTransactionsFlowTest.TestFlow(setOf(stx2.id), megaCorpParty)
val future = miniCorpNode.startFlow(p)
mockNet.runNetwork()
assertThatExceptionOfType(TransactionVerificationException.TransactionNetworkParameterOrderingException::class.java).isThrownBy {
future.getOrThrow()
}.withMessageContaining("The network parameters epoch (${params2.epoch}) of this transaction " +
"is older than the epoch (${params3.epoch}) of input state: ${stx2.inputs.first()}")
miniCorpNode.transaction {
assertThat(miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)).isNull()
assertThat(miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)).isNull()
// Even though the resolution failed, we should still have downloaded the parameters to the storage.
}
}
@Test
fun `request parameters that are not in the storage`() {
val params1 = miniCorpNode.services.networkParameters
val hash1 = params1.serialize().hash
val hash1 = defaultParams.serialize().hash
val hash2 = params2.serialize().hash
// Create two transactions on megaCorpNode
val (stx1, stx2) = makeTransactions(params1, params2)
val (stx1, stx2) = makeTransactions(defaultParams, params2)
assertThat(stx1.networkParametersHash).isEqualTo(hash1)
assertThat(stx2.networkParametersHash).isEqualTo(hash2)
miniCorpNode.transaction {
@ -125,8 +168,60 @@ class NetworkParametersResolutionTest {
future.getOrThrow()
miniCorpNode.transaction {
// Check that parameters were downloaded to the storage.
assertThat(miniCorpNode.services.networkParametersService.lookup(hash1)).isEqualTo(params1)
assertThat(miniCorpNode.services.networkParametersService.lookup(hash1)).isEqualTo(defaultParams)
assertThat(miniCorpNode.services.networkParametersService.lookup(hash2)).isEqualTo(params2)
}
}
@Test
fun `transaction chain out of order parameters with default`() {
val hash3 = params3.serialize().hash
// stx1 with epoch 3 -> stx2 with default epoch, which is 1
val (stx1, stx2) = makeTransactions(params3, null)
assertThat(stx2.networkParametersHash).isNull()
assertThat(stx1.networkParametersHash).isEqualTo(hash3)
val p = ResolveTransactionsFlowTest.TestFlow(setOf(stx2.id), megaCorpParty)
val future = miniCorpNode.startFlow(p)
mockNet.runNetwork()
assertThatExceptionOfType(TransactionVerificationException.TransactionNetworkParameterOrderingException::class.java).isThrownBy {
future.getOrThrow()
}.withMessageContaining("The network parameters epoch (${defaultParams.epoch}) of this transaction " +
"is older than the epoch (${params3.epoch}) of input state: ${stx2.inputs.first()}")
miniCorpNode.transaction {
assertThat(miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)).isNull()
assertThat(miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)).isNull()
assertThat(miniCorpNode.services.networkParametersService.lookup(hash3)).isEqualTo(params3)
}
}
@Test
fun `incorrect triangle of transactions`() {
// stx1 with epoch 2, stx2 with epoch 1, stx3 with epoch 3
// stx1 -> stx2, stx1 -> stx3, stx2 -> stx3
val stx1 = makeTransactions(params2, null).first
val stx2 = DummyContract.move(stx1.tx.outRef(0), miniCorpParty).let { builder ->
val ptx = builder.toSignedTransactionWithParameters(defaultParams, megaCorpNode.services)
notaryNode.services.addSignature(ptx, notaryParty.owningKey)
}
val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), miniCorpParty).let { builder ->
val ptx = builder.toSignedTransactionWithParameters(params3, megaCorpNode.services)
notaryNode.services.addSignature(ptx, notaryParty.owningKey)
}
megaCorpNode.transaction {
megaCorpNode.services.recordTransactions(stx2, stx3)
(megaCorpNode.services.networkParametersService as NetworkParametersStorage).saveParameters(certKeyPair.sign(defaultParams))
(megaCorpNode.services.networkParametersService as NetworkParametersStorage).saveParameters(certKeyPair.sign(params3))
}
val p = ResolveTransactionsFlowTest.TestFlow(setOf(stx3.id), megaCorpParty)
val future = miniCorpNode.startFlow(p)
mockNet.runNetwork()
assertThatExceptionOfType(TransactionVerificationException.TransactionNetworkParameterOrderingException::class.java).isThrownBy {
future.getOrThrow()
}.withMessageContaining("The network parameters epoch (${defaultParams.epoch}) of this transaction " +
"is older than the epoch (${params2.epoch}) of input state: ${stx2.inputs.first()}")
}
}

View File

@ -16,6 +16,7 @@ import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
@ -82,7 +83,7 @@ class DBNetworkParametersStorage(
override fun saveParameters(signedNetworkParameters: SignedNetworkParameters) {
log.trace { "Saving new network parameters to network parameters storage." }
val networkParameters = signedNetworkParameters.verified()
val networkParameters = signedNetworkParameters.verifiedNetworkMapCert(trustRoot)
val hash = signedNetworkParameters.raw.hash
log.trace { "Parameters to save $networkParameters with hash $hash" }
database.transaction {

View File

@ -17,7 +17,6 @@ import java.time.Instant
class MockNetworkParametersStorage(private var currentParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN)) : NetworkParametersStorage {
private val hashToParametersMap: HashMap<SecureHash, NetworkParameters> = HashMap()
private val hashToSignedParametersMap: HashMap<SecureHash, SignedNetworkParameters> = HashMap()
init {
storeCurrentParameters()
}
@ -44,8 +43,8 @@ class MockNetworkParametersStorage(private var currentParameters: NetworkParamet
}
}
override val defaultHash: SecureHash get() = currentHash
override fun getEpochFromHash(hash: SecureHash): Int? = lookup(hash)?.epoch
override fun lookup(hash: SecureHash): NetworkParameters? = hashToParametersMap[hash]
override fun getEpochFromHash(hash: SecureHash): Int? = lookup(hash)?.epoch
override fun saveParameters(signedNetworkParameters: SignedDataWithCert<NetworkParameters>) {
val networkParameters = signedNetworkParameters.verified()
val hash = signedNetworkParameters.raw.hash