CORDA-1010: Send a request signature in addition to a transaction to the notary (#2527)

CORDA-1010: Notary flow - clients now send a signature over a notarisation
request in addition to the transaction. This will be logged by the notary
to be able to prove that a particular party has requested the consumption
of a particular state.
This commit is contained in:
Andrius Dagys
2018-02-16 16:14:06 +00:00
committed by GitHub
parent fee89c044f
commit 5b93abdc57
13 changed files with 387 additions and 94 deletions

View File

@ -12,13 +12,19 @@ import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.flows.NotarisationPayload
import net.corda.core.flows.NotarisationRequest
import net.corda.core.node.services.NotaryService
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.utilities.*
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.utilities.AppendOnlyPersistentMap
@ -67,25 +73,25 @@ class BFTNonValidatingNotaryService(
replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return.
}
fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide)
fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide)
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
@Suspendable
override fun call(): Void? {
val stx = otherSideSession.receive<FilteredTransaction>().unwrap { it }
val signatures = commit(stx)
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
val signatures = commit(payload)
otherSideSession.send(signatures)
return null
}
private fun commit(stx: FilteredTransaction): List<DigitalSignature> {
val response = service.commitTransaction(stx, otherSideSession.counterparty)
private fun commit(payload: NotarisationPayload): List<DigitalSignature> {
val response = service.commitTransaction(payload, otherSideSession.counterparty)
when (response) {
is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
is BFTSMaRt.ClusterResponse.Signatures -> {
log.debug("All input states of transaction ${stx.id} have been committed")
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
return response.txSignatures
}
}
@ -132,28 +138,34 @@ class BFTNonValidatingNotaryService(
notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) {
override fun executeCommand(command: ByteArray): ByteArray {
val request = command.deserialize<BFTSMaRt.CommitRequest>()
val ftx = request.tx as FilteredTransaction
val response = verifyAndCommitTx(ftx, request.callerIdentity)
val commitRequest = command.deserialize<BFTSMaRt.CommitRequest>()
verifyRequest(commitRequest)
val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity)
return response.serialize().bytes
}
fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
return try {
val id = ftx.id
val inputs = ftx.inputs
val notary = ftx.notary
NotaryService.validateTimeWindow(services.clock, ftx.timeWindow)
val id = transaction.id
val inputs = transaction.inputs
val notary = transaction.notary
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary)
commitInputStates(inputs, id, callerIdentity)
log.debug { "Inputs committed successfully, signing $id" }
BFTSMaRt.ReplicaResponse.Signature(sign(ftx))
BFTSMaRt.ReplicaResponse.Signature(sign(id))
} catch (e: NotaryException) {
log.debug { "Error processing transaction: ${e.error}" }
BFTSMaRt.ReplicaResponse.Error(e.error)
}
}
private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) {
val transaction = commitRequest.payload.coreTransaction
val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id)
notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity)
// TODO: persist the signature for traceability.
}
}
override fun start() {

View File

@ -17,6 +17,7 @@ import net.corda.core.crypto.*
import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.identity.Party
import net.corda.core.flows.NotarisationPayload
import net.corda.core.internal.declaredField
import net.corda.core.internal.toTypedArray
import net.corda.core.node.services.UniquenessProvider
@ -25,8 +26,6 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.api.ServiceHubInternal
@ -52,7 +51,7 @@ import java.util.*
object BFTSMaRt {
/** Sent from [Client] to [Replica]. */
@CordaSerializable
data class CommitRequest(val tx: Any, val callerIdentity: Party)
data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party)
/** Sent from [Replica] to [Client]. */
@CordaSerializable
@ -101,13 +100,12 @@ object BFTSMaRt {
* Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every
* replica, and block until a sufficient number of replies are received.
*/
fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse {
require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" }
fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse {
awaitClientConnectionToCluster()
cluster.waitUntilAllReplicasHaveInitialized()
val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes
val requestBytes = CommitRequest(payload, otherSide).serialize().bytes
val responseBytes = proxy.invokeOrdered(requestBytes)
return responseBytes.deserialize<ClusterResponse>()
return responseBytes.deserialize()
}
/** A comparator to check if replies from two replicas are the same. */
@ -242,12 +240,15 @@ object BFTSMaRt {
}
}
/** Generates a signature over an arbitrary array of bytes. */
protected fun sign(bytes: ByteArray): DigitalSignature.WithKey {
return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) }
}
protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature {
return services.database.transaction { services.createSignature(filteredTransaction, notaryIdentityKey) }
/** Generates a transaction signature over the specified transaction [txId]. */
protected fun sign(txId: SecureHash): TransactionSignature {
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
return services.keyManagementService.sign(signableData, notaryIdentityKey)
}
// TODO:

View File

@ -1,11 +1,15 @@
package net.corda.node.services.transactions
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowSession
import net.corda.core.contracts.ComponentGroupEnum
import net.corda.core.flows.FlowSession
import net.corda.core.flows.NotaryFlow
import net.corda.core.flows.TransactionParts
import net.corda.core.flows.NotarisationPayload
import net.corda.core.flows.NotarisationRequest
import net.corda.core.internal.validateRequest
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.utilities.unwrap
@ -21,22 +25,30 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
*/
@Suspendable
override fun receiveAndVerifyTx(): TransactionParts {
val parts = otherSideSession.receive<Any>().unwrap {
when (it) {
is FilteredTransaction -> {
it.verify()
it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
val notary = it.notary
TransactionParts(it.id, it.inputs, it.timeWindow, notary)
}
is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null, it.notary)
else -> {
throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," +
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
return otherSideSession.receive<NotarisationPayload>().unwrap { payload ->
val transaction = payload.coreTransaction
val request = NotarisationRequest(transaction.inputs, transaction.id)
validateRequest(request, payload.requestSignature)
extractParts(transaction)
}
}
private fun extractParts(tx: CoreTransaction): TransactionParts {
return when (tx) {
is FilteredTransaction -> {
tx.apply {
verify()
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
}
val notary = tx.notary
TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary)
}
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
else -> {
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
}
}
return parts
}
}

View File

@ -4,8 +4,14 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.flows.*
import net.corda.core.flows.NotarisationPayload
import net.corda.core.flows.NotarisationRequest
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.validateRequest
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionWithSignatures
import net.corda.core.utilities.unwrap
import java.security.SignatureException
/**
@ -22,15 +28,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
@Suspendable
override fun receiveAndVerifyTx(): TransactionParts {
try {
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
val stx = receiveTransaction()
val notary = stx.notary
checkNotary(notary)
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
null
else
stx.tx.timeWindow
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
checkSignatures(transactionWithSignatures)
resolveAndContractVerify(stx)
verifySignatures(stx)
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
} catch (e: Exception) {
throw when (e) {
@ -41,6 +47,26 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
}
}
@Suspendable
private fun receiveTransaction(): SignedTransaction {
return otherSideSession.receive<NotarisationPayload>().unwrap {
val stx = it.signedTransaction
validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature)
stx
}
}
@Suspendable
private fun resolveAndContractVerify(stx: SignedTransaction) {
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
stx.verify(serviceHub, false)
}
private fun verifySignatures(stx: SignedTransaction) {
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
checkSignatures(transactionWithSignatures)
}
private fun checkSignatures(tx: TransactionWithSignatures) {
try {
tx.verifySignaturesExcept(service.notaryIdentityKey)

View File

@ -3,24 +3,35 @@ package net.corda.node.services.transactions
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.sign
import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.flows.NotaryFlow
import net.corda.core.identity.Party
import net.corda.core.flows.NotarisationPayload
import net.corda.core.flows.NotarisationRequest
import net.corda.core.flows.NotarisationRequestSignature
import net.corda.core.internal.generateSignature
import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.node.services.api.StartedNodeServices
import net.corda.testing.core.ALICE_NAME
import net.corda.node.services.messaging.Message
import net.corda.node.services.statemachine.InitialSessionMessage
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.dummyCommand
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.startFlow
import net.corda.testing.node.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
@ -34,6 +45,7 @@ import kotlin.test.assertTrue
class NotaryServiceTests {
private lateinit var mockNet: MockNetwork
private lateinit var notaryServices: StartedNodeServices
private lateinit var aliceNode: StartedMockNode
private lateinit var aliceServices: StartedNodeServices
private lateinit var notary: Party
private lateinit var alice: Party
@ -41,7 +53,8 @@ class NotaryServiceTests {
@Before
fun setup() {
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
aliceServices = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)).services
aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
aliceServices = aliceNode.services
notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that
notary = mockNet.defaultNotaryIdentity
alice = aliceServices.myInfo.singleIdentity()
@ -159,6 +172,70 @@ class NotaryServiceTests {
notaryError.conflict.verified()
}
@Test
fun `should reject when notarisation request not signed by the requesting party`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.signedTransaction
val randomKeyPair = Crypto.generateKeyPair()
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceServices.myInfo.platformVersion)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
@Test
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.signedTransaction
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
val request = NotarisationRequest(wrongInputs, transaction.id)
val modifiedSignature = request.generateSignature(aliceServices)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
@Test
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
runNotarisationAndInterceptClientPayload { originalPayload ->
val transaction = originalPayload.signedTransaction
val wrongTransactionId = SecureHash.randomSHA256()
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
val modifiedSignature = request.generateSignature(aliceServices)
originalPayload.copy(requestSignature = modifiedSignature)
}
}
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) {
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, additionalHeaders: Map<String, String>) {
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
val payload = messageData?.firstPayload!!.deserialize()
if (payload is NotarisationPayload) {
val alteredPayload = payloadModifier(payload)
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
val alteredMessage = InMemoryMessagingNetwork.InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
messagingService.send(alteredMessage, target, retryId)
} else {
messagingService.send(message, target, retryId)
}
}
})
val stx = run {
val inputState = issueState(aliceServices, alice)
val tx = TransactionBuilder(notary)
.addInputState(inputState)
.addCommand(dummyCommand(alice.owningKey))
aliceServices.signInitialTransaction(tx)
}
val future = runNotaryClient(stx)
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
}
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
val flow = NotaryFlow.Client(stx)
val future = aliceServices.startFlow(flow)