3
votes

Recently I've been looking into the security and vulnerability aspects of contract commands in Corda. A debate arose as to whether some contract command constraints should be strict or whether they should be relaxed in order to allow transaction composition of different inputs, outputs and commands.

The problem is that whilst I can see the benefit of allowing transaction composition, I feel like relaxed contract command constraints actually pose security vulnerabilities, and in my opinion, it would be better to secure against these vulnerabilities at the contract level, in such that the signing participants of the command reach consensus through contract verification as a whole, rather than relying on flow level checks, which could be overlooked by a developer or circumvented by a malicious node.

Example - Bankruptcy Declaration

This example allows nodes on the network to declare bankruptcy. Assume in this case that a bankruptcy declaration state is simply the identity of the node declaring bankruptcy, and a reason.

@BelongsToContract(BankruptcyDeclarationContract::class)
data class BankruptcyDeclarationState(
    override val owner: AbstractParty,
    val reason: String    
) : OwnableState { ... }

Strict Verification

Strict verification requires that, on issuance...

  • Zero input states must be consumed.
  • One output state must be created.
  • Only the owner must sign.
fun verifyIssue(tx: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
    "Zero input states must be consumed." using (tx.inputs.isEmpty())
    "One output state must be created." using (tx.outputs.size == 1)

    val state = tx.outputsOfType<BankruptcyDeclarationState>().single()
    "Only the owner must sign." using (state.owner.owningKey == signers.single())
}

Relaxed Verification

Relaxed verification requires that, on issuance...

  • Zero input states of type BankruptcyDeclarationState must be consumed.
  • One output state of type BankruptcyDeclarationState must be created.
  • Only the owner must sign.
fun verifyIssue(tx: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
    val inputs = tx.inputsOfType<BankruptcyDeclarationState>()
    val outputs = tx.outputsOfType<BankruptcyDeclarationState>()

    "Zero input states of type BankruptcyDeclarationState must be consumed." using 
        (inputs.isEmpty())
    "One output state of type BankruptcyDeclarationState must be created." using 
        (outputs.size == 1)
    "Only the owner must sign." using (outputs.single().owner.owningKey == signers.single())
}

Observations

  • Strict verification ensures that the inputs and outputs are checked globally, rather than checking for specific input and output types, however this has a disadvantage that transaction composition of inputs and outputs is impossible.
  • Relaxed verification ensures that only inputs and outputs of the required state type are checked, which would allow transaction composition of different input and output types.
  • The key here is that only the node declaring bankruptcy must sign, which means that issuance of a BankruptcyDeclarationState can only occur from that node. Nobody else should be allowed to declare bankruptcy on behalf of another node on the network.

Identifying The Vulnerability

Suppose we chose to model our contract command constraints to be relaxed, so that we can compose transactions. Also, suppose that we have a contract command for some ObligationState which when issued, requires that:

  • Zero input states of type ObligationState must be consumed.
  • One output state of type ObligationState must be created.
  • The obligor and obligee must sign.

Now that we have two state types and two contract commands, we can compose a transaction that uses both, and identify the vulnerability. Assume here that bob is initiating this transaction.

val transaction = with(TransactionBuilder(notary)) {

    addOutputState(ObligationState(alice, bob), ObligationContract.ID)
    addCommand(ObligationContract.Issue(), aliceKey, bobKey)

    addOutputState(BankruptcyDeclarationState(alice, "..."), BankruptcyDeclarationContract.ID)
    addCommand(BankruptcyDeclarationContract.Issue(), aliceKey)
}

Remember that only the owner of a BankruptcyDeclarationState must sign, but the obligor and obligee of an ObligationState must sign, therefore this initiating flow will collect signatures from the required counter-parties. The vulnerability here is that bob initiates this transaction, but includes an output of type BankruptcyDeclarationState which is owned by alice. He shouldn't be allowed to do that because only the owner should be allowed to issue BankruptcyDeclarationState but in this case alice will unwittingly sign because of the requirement to sign for the ObligationState.

There is an argument to be made here that the flows could be written in such a way that alice would check the transaction before signing to ensure that certain states were not included, but I do not feel like this is enough. This requires developers and node administrators to carry out due diligence of flows ensuring their security.

In contrast, strict contract command constraints would prevent these vulnerabilities in what I believe to be a much more secure way - due diligence is therefore only required at a contract level, rather than from every developer writing flows that consume the contracts.

What I am looking for in this respect is some definitive guide to whether contract command constraints should be strict, relaxed, or whether there are other considerations to be made that I've missed. Thanks.

1

1 Answers

1
votes

As you pointed out correctly, the identical contract code is shared among all transacting parties. That is the only agreement among them. But each party is responsible for his/her action (signing) by developing his/her own secure flows. The fundamental in writing flows is to verify the transaction against the contract code before signing. Who would sign anything digitally or otherwise without reading/checking the contract? Did I miss anything?