CORDA-3124: Ensure transaction is fully verified before sending to notary (#5516)

NotaryFlow.Client flow assumes the passed transaction is verified, which is
the case when it's invoked as a sub-flow in FinalityFlow. However,
NotaryFlow.Client can also be called directly from any custom flow, so
we need to ensure it performs transaction verification to avoid accidentally
sending an invalid transaction to a non-validating notary.
This commit is contained in:
Andrius Dagys 2019-09-27 13:09:36 +01:00 committed by GitHub
parent 2e148f96a2
commit ef57e9909c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 32 additions and 13 deletions

View File

@ -216,7 +216,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
private fun notariseAndRecord(): SignedTransaction { private fun notariseAndRecord(): SignedTransaction {
val notarised = if (needsNotarySignature(transaction)) { val notarised = if (needsNotarySignature(transaction)) {
progressTracker.currentStep = NOTARISING progressTracker.currentStep = NOTARISING
val notarySignatures = subFlow(NotaryFlow.Client(transaction)) val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true))
transaction + notarySignatures transaction + notarySignatures
} else { } else {
logger.info("No need to notarise this transaction.") logger.info("No need to notarise this transaction.")
@ -249,6 +249,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
val notary = transaction.tx.notary val notary = transaction.tx.notary
// The notary signature(s) are allowed to be missing but no others. // The notary signature(s) are allowed to be missing but no others.
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures() if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
// TODO= [CORDA-3267] Remove duplicate signature verification
val ltx = transaction.toLedgerTransaction(serviceHub, false) val ltx = transaction.toLedgerTransaction(serviceHub, false)
ltx.verify() ltx.verify()
return ltx return ltx

View File

@ -8,9 +8,12 @@ import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.BackpressureAwareTimedFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.NetworkParametersStorage
import net.corda.core.internal.notary.generateSignature import net.corda.core.internal.notary.generateSignature
import net.corda.core.internal.notary.validateSignatures import net.corda.core.internal.notary.validateSignatures
import net.corda.core.internal.pushToLoggingContext
import net.corda.core.transactions.* import net.corda.core.transactions.*
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
@ -35,9 +38,16 @@ class NotaryFlow {
@InitiatingFlow @InitiatingFlow
open class Client( open class Client(
private val stx: SignedTransaction, private val stx: SignedTransaction,
override val progressTracker: ProgressTracker override val progressTracker: ProgressTracker,
/**
* Set to *true* if the [stx] has already been verified for signature and contract validity,
* to prevent re-verification.
*/
private val skipVerification: Boolean = false
) : BackpressureAwareTimedFlow<List<TransactionSignature>>() { ) : BackpressureAwareTimedFlow<List<TransactionSignature>>() {
constructor(stx: SignedTransaction) : this(stx, tracker()) @JvmOverloads
constructor(stx: SignedTransaction, skipVerification: Boolean = false) : this(stx, tracker(), skipVerification)
constructor(stx: SignedTransaction, progressTracker: ProgressTracker): this(stx, progressTracker, false)
companion object { companion object {
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service") object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
@ -70,16 +80,21 @@ class NotaryFlow {
* Checks that the transaction specifies a valid notary, and verifies that it contains all required signatures * Checks that the transaction specifies a valid notary, and verifies that it contains all required signatures
* apart from the notary's. * apart from the notary's.
*/ */
// TODO: [CORDA-2274] Perform full transaction verification once verification caching is enabled.
protected fun checkTransaction(): Party { protected fun checkTransaction(): Party {
val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" } check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" }
check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) { check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) {
"Input states and reference input states must have the same Notary" "Input states and reference input states must have the same Notary"
} }
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
if (!skipVerification) {
// TODO= [CORDA-3267] Remove duplicate signature verification
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
stx.verify(serviceHub, false)
}
return notaryParty return notaryParty
} }
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */ /** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
@Throws(NotaryException::class) @Throws(NotaryException::class)
@Suspendable @Suspendable
@ -107,7 +122,6 @@ class NotaryFlow {
val historicNotary = (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty) val historicNotary = (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty)
?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.") ?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
historicNotary.validating historicNotary.validating
} else serviceHub.networkMapCache.isValidatingNotary(notaryParty) } else serviceHub.networkMapCache.isValidatingNotary(notaryParty)
} }

View File

@ -6,6 +6,7 @@ release, see :doc:`app-upgrade-notes`.
Unreleased Unreleased
---------- ----------
* Moved and renamed the testing web server to the ``testing`` subproject. Also renamed the published artifact to ``corda-testserver.jar``. * Moved and renamed the testing web server to the ``testing`` subproject. Also renamed the published artifact to ``corda-testserver.jar``.
* Support for Java 11 (compatibility mode). Please read https://github.com/corda/corda/pull/5356. * Support for Java 11 (compatibility mode). Please read https://github.com/corda/corda/pull/5356.
@ -33,6 +34,10 @@ Unreleased
* Introduced a new low level flow diagnostics tool: checkpoint agent (that can be used standalone or in conjunction with the ``checkpoints dump`` shell command). * Introduced a new low level flow diagnostics tool: checkpoint agent (that can be used standalone or in conjunction with the ``checkpoints dump`` shell command).
See :doc:`checkpoint-tooling` for more information. See :doc:`checkpoint-tooling` for more information.
* ``NotaryFlow.Client`` now performs transaction verification by default to prevent accidentally sending an invalid transaction to a
non-validating notary. The behaviour can be controlled by passing a constructor parameter flag ``skipVerification``.
Note: this only affects flows that invoke ``NotaryFlow.Client`` directly no behavioural change if using ``FinalityFlow``.
* The MockNet now supports setting a custom Notary class name, as was already supported by normal node config. See :doc:`tutorial-custom-notary`. * The MockNet now supports setting a custom Notary class name, as was already supported by normal node config. See :doc:`tutorial-custom-notary`.

View File

@ -167,7 +167,7 @@ class NotaryWhitelistTests(
} }
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> { private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
val flow = NotaryFlow.Client(stx) val flow = NotaryFlow.Client(stx, skipVerification = true)
val future = aliceNode.services.startFlow(flow).resultFuture val future = aliceNode.services.startFlow(flow).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
return future return future

View File

@ -91,13 +91,12 @@ class ValidatingNotaryServiceTests {
aliceNode.services.signInitialTransaction(tx) aliceNode.services.signInitialTransaction(tx)
} }
// Expecting SignaturesMissingException instead of NotaryException, since the exception should originate from val ex = assertFailsWith<NotaryException> {
// the client flow.
val ex = assertFailsWith<SignedTransaction.SignaturesMissingException> {
val future = runNotaryClient(stx) val future = runNotaryClient(stx)
future.getOrThrow() future.getOrThrow()
} }
val missingKeys = ex.missing val exception = (ex.error as NotaryError.TransactionInvalid).cause as SignedTransaction.SignaturesMissingException
val missingKeys = exception.missing
assertEquals(setOf(expectedMissingKey), missingKeys) assertEquals(setOf(expectedMissingKey), missingKeys)
} }
@ -349,7 +348,7 @@ class ValidatingNotaryServiceTests {
} }
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> { private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
val flow = NotaryFlow.Client(stx) val flow = NotaryFlow.Client(stx, skipVerification = true)
val future = aliceNode.services.startFlow(flow).resultFuture val future = aliceNode.services.startFlow(flow).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
return future return future