CORDA-3350: Increase size of constraints column (#5639)

* CORDA-3350: Increase size of constraints column (#5639)

* Detekt

* Update api file with new threshold

* Add check in transaction builder

* Revert "Add check in transaction builder"

This reverts commit ca3128f44c.

* Add check for max number of keys

* Update api file

* Address Tudor's comments

* Remove check for pre-5 and add test for EC keys

* fix typo and rename liquibase script

* updated docs with measurement numbers for composite keys

* Make detekt happy again
This commit is contained in:
Dimos Raptis 2019-11-05 11:00:26 +00:00 committed by Jonathan Locke
parent c193aa46f0
commit 485feb2d6c
13 changed files with 188 additions and 15 deletions

View File

@ -4034,7 +4034,7 @@ public interface net.corda.core.node.services.VaultService
public abstract net.corda.core.concurrent.CordaFuture<net.corda.core.node.services.Vault$Update<net.corda.core.contracts.ContractState>> whenConsumed(net.corda.core.contracts.StateRef)
##
public final class net.corda.core.node.services.VaultServiceKt extends java.lang.Object
public static final int MAX_CONSTRAINT_DATA_SIZE = 563
public static final int MAX_CONSTRAINT_DATA_SIZE = 20000
##
@CordaSerializable
public final class net.corda.core.node.services.vault.AggregateFunctionType extends java.lang.Enum

View File

@ -96,6 +96,20 @@ class TransactionVerificationExceptionSerialisationTests {
assertEquals(exception.txId, exception2.txId)
}
@Test
fun invalidConstraintRejectionError() {
val exception = TransactionVerificationException.InvalidConstraintRejection(txid, "Some contract class", "for being too funny")
val exceptionAfterSerialisation = DeserializationInput(factory).deserialize(
SerializationOutput(factory).serialize(exception, context),
context
)
assertEquals(exception.message, exceptionAfterSerialisation.message)
assertEquals(exception.cause?.message, exceptionAfterSerialisation.cause?.message)
assertEquals(exception.contractClass, exceptionAfterSerialisation.contractClass)
assertEquals(exception.reason, exceptionAfterSerialisation.reason)
}
@Test
fun contractCreationErrorTest() {
val cause = Throwable("wibble")

View File

@ -92,6 +92,16 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
/**
* A constraint attached to a state was invalid, e.g. due to size limitations.
*
* @property contractClass The fully qualified class name of the failing contract.
* @property reason a message containing the reason the constraint is invalid included in thrown the exception.
*/
@KeepForDJVM
class InvalidConstraintRejection(txId: SecureHash, val contractClass: String, val reason: String)
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass. $reason", null)
/**
* A state requested a contract class via its [TransactionState.contract] field that didn't appear in any attached
* JAR at all. This usually implies the attachments were forgotten or a version mismatch.

View File

@ -10,6 +10,13 @@ import net.corda.core.utilities.loggerFor
*/
typealias Version = Int
/**
* The maximum number of keys in a signature constraint that the platform supports.
*
* Attention: this value affects consensus, so it requires a minimum platform version bump in order to be changed.
*/
const val MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT = 20
private val log = loggerFor<AttachmentConstraint>()
val Attachment.contractVersion: Version get() = if (this is ContractAttachment) version else CordappImpl.DEFAULT_CORDAPP_VERSION

View File

@ -4,6 +4,7 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
import net.corda.core.crypto.CompositeKey
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.contextLogger
@ -328,8 +329,23 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// For each contract/constraint pair check that the relevant attachment is valid.
allStates.map { it.contract to it.constraint }.toSet().forEach { (contract, constraint) ->
if (constraint is SignatureAttachmentConstraint)
if (constraint is SignatureAttachmentConstraint) {
/**
* Support for signature constraints has been added on min. platform version >= 4.
* On minimum platform version >= 5, an explicit check has been introduced on the supported number of leaf keys
* in composite keys of signature constraints in order to harden consensus.
*/
checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints")
val constraintKey = constraint.key
if (ltx.networkParameters?.minimumPlatformVersion ?: 1 >= 5) {
if (constraintKey is CompositeKey && constraintKey.leafKeys.size > MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT) {
throw TransactionVerificationException.InvalidConstraintRejection(ltx.id, contract,
"Signature constraint contains composite key with ${constraintKey.leafKeys.size} leaf keys, " +
"which is more than the maximum allowed number of keys " +
"($MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT).")
}
}
}
// We already checked that there is one and only one attachment.
val contractAttachment = contractAttachmentsByContract[contract]!!

View File

@ -10,6 +10,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.DataFeed
import net.corda.core.node.services.Vault.RelevancyStatus.*
@ -256,9 +257,15 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
/**
* The maximum permissible size of contract constraint type data (for storage in vault states database table).
* Maximum value equates to a CompositeKey with 10 EDDSA_ED25519_SHA512 keys stored in.
*
* This value establishes an upper limit of a CompositeKey with up to [MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT] keys stored in.
* However, note this assumes a rather conservative upper bound per key.
* For reference, measurements have shown the following numbers for each algorithm:
* - 2048-bit RSA keys: 1 key -> 294 bytes, 2 keys -> 655 bytes, 3 keys -> 961 bytes
* - 256-bit ECDSA (k1) keys: 1 key -> 88 bytes, 2 keys -> 231 bytes, 3 keys -> 331 bytes
* - 256-bit EDDSA keys: 1 key -> 44 bytes, 2 keys -> 140 bytes, 3 keys -> 195 bytes
*/
const val MAX_CONSTRAINT_DATA_SIZE = 563
const val MAX_CONSTRAINT_DATA_SIZE = 1_000 * MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT
/**
* A [VaultService] is responsible for securely and safely persisting the current state of a vault to storage. The

View File

@ -1225,6 +1225,7 @@
<ID>MagicNumber:TransactionUtils.kt$4</ID>
<ID>MagicNumber:TransactionVerificationException.kt$TransactionVerificationException.ConstraintPropagationRejection$3</ID>
<ID>MagicNumber:TransactionVerifierServiceInternal.kt$Verifier$4</ID>
<ID>MagicNumber:TransactionVerifierServiceInternal.kt$Verifier$5</ID>
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$15.0</ID>
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$20.0</ID>
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$200.0</ID>
@ -1329,7 +1330,6 @@
<ID>MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingParameterList$data</ID>
<ID>MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingRawType$data</ID>
<ID>MaxLineLength:AMQPTypeIdentifierParserTests.kt$AMQPTypeIdentifierParserTests$verify(" java.util.Map &lt; java.util.Map&lt; java.lang.String, java.lang.Integer &gt;, java.util.Map &lt; java.lang.Long , java.lang.String &gt; &gt;")</ID>
<ID>MaxLineLength:ANSIProgressRenderer.kt$ANSIProgressRenderer$ansi.a("${IntStream.range(indent, indent).mapToObj { "\t" }.toList().joinToString(separator = "") { s -&gt; s }} $errorIcon ${error.message}")</ID>
<ID>MaxLineLength:ANSIProgressRenderer.kt$StdoutANSIProgressRenderer$val consoleAppender = manager.configuration.appenders.values.filterIsInstance&lt;ConsoleAppender&gt;().singleOrNull { it.name == "Console-Selector" }</ID>
<ID>MaxLineLength:ANSIProgressRendererTest.kt$ANSIProgressRendererTest$checkTrackingState(captor, 5, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL)))</ID>
<ID>MaxLineLength:ANSIProgressRendererTest.kt$ANSIProgressRendererTest$checkTrackingState(captor, 6, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL), stepNotRun(STEP_4_LABEL)))</ID>
@ -3547,6 +3547,7 @@
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier$if (ltx.attachments.size != ltx.attachments.toSet().size) throw TransactionVerificationException.DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size &gt; 1 }.keys.first())</ID>
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier$if (result.keys != contractClasses) throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first())</ID>
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier$val constraintAttachment = AttachmentWithContext(contractAttachment, contract, ltx.networkParameters!!.whitelistedContractImplementations)</ID>
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier${ /** * Signature constraints are supported on min. platform version &gt;= 4, but this only includes support for a single key per constraint. * Signature contstraints with composite keys containing more than 1 leaf key are supported on min. platform version &gt;= 5. */ checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints") val constraintKey = constraint.key if (constraintKey is CompositeKey &amp;&amp; constraintKey.leafKeys.size &gt; 1) { checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 5, "Composite keys for signature constraints") val leafKeysNumber = constraintKey.leafKeys.size if (leafKeysNumber &gt; MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT) throw TransactionVerificationException.InvalidConstraintRejection(ltx.id, contract, "Signature constraint contains composite key with $leafKeysNumber leaf keys, " + "which is more than the maximum allowed number of keys " + "($MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT).") } }</ID>
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier${ // checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs" // list, the contents of which need to be deserialized under the correct classloader. checkNoNotaryChange() checkEncumbrancesValid() // The following checks ensure the integrity of the current transaction and also of the future chain. // See: https://docs.corda.net/head/api-contract-constraints.html // A transaction contains both the data and the code that must be executed to validate the transition of the data. // Transactions can be created by malicious adversaries, who can try to use code that allows them to create transactions that appear valid but are not. // 1. Check that there is one and only one attachment for each relevant contract. val contractAttachmentsByContract = getUniqueContractAttachmentsByContract() // 2. Check that the attachments satisfy the constraints of the states. (The contract verification code is correct.) verifyConstraints(contractAttachmentsByContract) // 3. Check that the actual state constraints are correct. This is necessary because transactions can be built by potentially malicious nodes // who can create output states with a weaker constraint which can be exploited in a future transaction. verifyConstraintsValidity(contractAttachmentsByContract) // 4. Check that the [TransactionState] objects are correctly formed. validateStatesAgainstContract() // 5. Final step is to run the contract code. After the first 4 steps we are now sure that we are running the correct code. verifyContracts() }</ID>
<ID>MaxLineLength:TransactionViewer.kt$TransactionViewer$private</ID>
<ID>MaxLineLength:TransactionViewer.kt$TransactionViewer$private fun ObservableList&lt;StateAndRef&lt;ContractState&gt;&gt;.getParties()</ID>
@ -3820,6 +3821,7 @@
<ID>NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map&lt;SessionId, SerializedBytes&lt;Any&gt;&gt;)</ID>
<ID>NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</ID>
<ID>NestedBlockDepth:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable</ID>
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraints(contractAttachmentsByContract: Map&lt;ContractClassName, ContractAttachment&gt;)</ID>
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map&lt;ContractClassName, ContractAttachment&gt;)</ID>
<ID>SpreadOperator:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$(*it.whitelist.toTypedArray())</ID>
<ID>SpreadOperator:AbstractNode.kt$FlowStarterImpl$(logicType, *args)</ID>

View File

@ -100,6 +100,9 @@ Expanding on the previous section, for an app to use Signature Constraints, it m
The signers of the app can consist of a single organisation or multiple organisations. Once the app has been signed, it can be distributed
across the nodes that intend to use it.
.. note:: The platform currently supports ``CompositeKey``\s with up to 20 keys maximum.
This maximum limit is assuming keys that are either 2048-bit ``RSA`` keys or 256-bit elliptic curve (``EC``) keys.
Each transaction received by a node will then verify that the apps attached to it have the correct signers as specified by its
Signature Constraints. This ensures that the version of each app is acceptable to the transaction's input states.

View File

@ -0,0 +1,89 @@
package net.corda.contracts
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueFlow
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class SignatureConstraintGatingTests {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test
fun `signature constraints can be used with up to the maximum allowed number of (RSA) keys`() {
tempFolder.root.toPath().let {path ->
val financeCordapp = cordappWithPackages("net.corda.finance.contracts", "net.corda.finance.schemas")
.signed(keyStorePath = path, numberOfSignatures = 20, keyAlgorithm = "RSA")
driver(DriverParameters(
networkParameters = testNetworkParameters().copy(minimumPlatformVersion = 5),
cordappsForAllNodes = setOf(financeCordapp, FINANCE_WORKFLOWS_CORDAPP),
startNodesInProcess = true,
inMemoryDB = true
)) {
val node = startNode().getOrThrow()
node.rpc.startFlowDynamic(CashIssueFlow::class.java, 10.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity)
.returnValue.getOrThrow()
}
}
}
@Test
fun `signature constraints can be used with up to the maximum allowed number of (EC) keys`() {
tempFolder.root.toPath().let {path ->
val financeCordapp = cordappWithPackages("net.corda.finance.contracts", "net.corda.finance.schemas")
.signed(keyStorePath = path, numberOfSignatures = 20, keyAlgorithm = "EC")
driver(DriverParameters(
networkParameters = testNetworkParameters().copy(minimumPlatformVersion = 5),
cordappsForAllNodes = setOf(financeCordapp, FINANCE_WORKFLOWS_CORDAPP),
startNodesInProcess = true,
inMemoryDB = true
)) {
val node = startNode().getOrThrow()
node.rpc.startFlowDynamic(CashIssueFlow::class.java, 10.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity)
.returnValue.getOrThrow()
}
}
}
@Test
fun `signature constraints cannot be used with more than the maximum allowed number of keys`() {
tempFolder.root.toPath().let {path ->
val financeCordapp = cordappWithPackages("net.corda.finance.contracts", "net.corda.finance.schemas")
.signed(keyStorePath = path, numberOfSignatures = 21)
driver(DriverParameters(
networkParameters = testNetworkParameters().copy(minimumPlatformVersion = 5),
cordappsForAllNodes = setOf(financeCordapp, FINANCE_WORKFLOWS_CORDAPP),
startNodesInProcess = true,
inMemoryDB = true
)) {
val node = startNode().getOrThrow()
Assertions.assertThatThrownBy {
node.rpc.startFlowDynamic(CashIssueFlow::class.java, 10.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity)
.returnValue.getOrThrow()
}
.isInstanceOf(TransactionVerificationException.InvalidConstraintRejection::class.java)
.hasMessageContaining("Signature constraint contains composite key with 21 leaf keys, " +
"which is more than the maximum allowed number of keys (20).")
}
}
}
}

View File

@ -11,4 +11,5 @@
<include file="migration/vault-schema.changelog-v6.xml"/>
<include file="migration/vault-schema.changelog-v7.xml"/>
<include file="migration/vault-schema.changelog-v8.xml"/>
<include file="migration/vault-schema.changelog-v11.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="expand_constraint_data_size">
<modifyDataType tableName="vault_states"
columnName="constraint_data"
newDataType="varbinary(20000)"/>
</changeSet>
</databaseChangeLog>

View File

@ -7,6 +7,7 @@ import net.corda.core.internal.cordapp.set
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.containsKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import java.nio.file.Path
import java.nio.file.Paths
@ -43,7 +44,8 @@ data class CustomCordapp(
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes)
fun signed(keyStorePath: Path? = null): CustomCordapp = copy(signingInfo = SigningInfo(keyStorePath))
fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
@VisibleForTesting
internal fun packageAsJar(file: Path) {
@ -73,20 +75,21 @@ data class CustomCordapp(
private fun signJar(jarFile: Path) {
if (signingInfo != null) {
val testKeystore = "_teststore"
val alias = "Test"
val pwd = "secret!"
val keyStorePathToUse = if (signingInfo.keyStorePath != null) {
signingInfo.keyStorePath
} else {
defaultJarSignerDirectory.createDirectories()
if (!(defaultJarSignerDirectory / testKeystore).exists()) {
defaultJarSignerDirectory.generateKey(alias, pwd, "O=Test Company Ltd,OU=Test,L=London,C=GB")
}
defaultJarSignerDirectory
}
for (i in 1 .. signingInfo.numberOfSignatures) {
val alias = "alias$i"
val pwd = "secret!"
if (!keyStorePathToUse.containsKey(alias, pwd))
keyStorePathToUse.generateKey(alias, pwd, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", signingInfo.keyAlgorithm)
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
logger.debug { "Signed Jar: $jarFile with public key $pk" }
}
} else {
logger.debug { "Unsigned Jar: $jarFile" }
}
@ -111,7 +114,7 @@ data class CustomCordapp(
return ZipEntry(name).setCreationTime(epochFileTime).setLastAccessTime(epochFileTime).setLastModifiedTime(epochFileTime)
}
data class SigningInfo(val keyStorePath: Path? = null)
data class SigningInfo(val keyStorePath: Path?, val numberOfSignatures: Int, val keyAlgorithm: String)
companion object {
private val logger = contextLogger()

View File

@ -9,6 +9,7 @@ import java.io.Closeable
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
@ -72,6 +73,15 @@ object JarSignatureTestUtils {
return ks.getCertificate(alias).publicKey
}
fun Path.containsKey(alias: String, storePassword: String, storeName: String = "_teststore"): Boolean {
return try {
val ks = loadKeyStore(this.resolve(storeName), storePassword)
ks.containsAlias(alias)
} catch (e: NoSuchFileException) {
false
}
}
fun Path.getPublicKey(alias: String, storePassword: String) = getPublicKey(alias, "_teststore", storePassword)
fun Path.getJarSigners(fileName: String) =