mirror of
https://github.com/corda/corda.git
synced 2024-12-24 15:16:45 +00:00
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:
parent
c193aa46f0
commit
485feb2d6c
@ -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 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 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
|
@CordaSerializable
|
||||||
public final class net.corda.core.node.services.vault.AggregateFunctionType extends java.lang.Enum
|
public final class net.corda.core.node.services.vault.AggregateFunctionType extends java.lang.Enum
|
||||||
|
@ -96,6 +96,20 @@ class TransactionVerificationExceptionSerialisationTests {
|
|||||||
assertEquals(exception.txId, exception2.txId)
|
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
|
@Test
|
||||||
fun contractCreationErrorTest() {
|
fun contractCreationErrorTest() {
|
||||||
val cause = Throwable("wibble")
|
val cause = Throwable("wibble")
|
||||||
|
@ -92,6 +92,16 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
|||||||
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
|
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
|
||||||
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
: 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
|
* 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.
|
* JAR at all. This usually implies the attachments were forgotten or a version mismatch.
|
||||||
|
@ -10,6 +10,13 @@ import net.corda.core.utilities.loggerFor
|
|||||||
*/
|
*/
|
||||||
typealias Version = Int
|
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>()
|
private val log = loggerFor<AttachmentConstraint>()
|
||||||
|
|
||||||
val Attachment.contractVersion: Version get() = if (this is ContractAttachment) version else CordappImpl.DEFAULT_CORDAPP_VERSION
|
val Attachment.contractVersion: Version get() = if (this is ContractAttachment) version else CordappImpl.DEFAULT_CORDAPP_VERSION
|
||||||
|
@ -4,6 +4,7 @@ import net.corda.core.DeleteForDJVM
|
|||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.utilities.contextLogger
|
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>) {
|
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||||
// For each contract/constraint pair check that the relevant attachment is valid.
|
// For each contract/constraint pair check that the relevant attachment is valid.
|
||||||
allStates.map { it.contract to it.constraint }.toSet().forEach { (contract, constraint) ->
|
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")
|
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.
|
// We already checked that there is one and only one attachment.
|
||||||
val contractAttachment = contractAttachmentsByContract[contract]!!
|
val contractAttachment = contractAttachmentsByContract[contract]!!
|
||||||
|
@ -10,6 +10,7 @@ import net.corda.core.crypto.SecureHash
|
|||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.AbstractParty
|
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.internal.concurrent.doneFuture
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.node.services.Vault.RelevancyStatus.*
|
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).
|
* 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
|
* A [VaultService] is responsible for securely and safely persisting the current state of a vault to storage. The
|
||||||
|
@ -1225,6 +1225,7 @@
|
|||||||
<ID>MagicNumber:TransactionUtils.kt$4</ID>
|
<ID>MagicNumber:TransactionUtils.kt$4</ID>
|
||||||
<ID>MagicNumber:TransactionVerificationException.kt$TransactionVerificationException.ConstraintPropagationRejection$3</ID>
|
<ID>MagicNumber:TransactionVerificationException.kt$TransactionVerificationException.ConstraintPropagationRejection$3</ID>
|
||||||
<ID>MagicNumber:TransactionVerifierServiceInternal.kt$Verifier$4</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$15.0</ID>
|
||||||
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$20.0</ID>
|
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$20.0</ID>
|
||||||
<ID>MagicNumber:TransactionViewer.kt$TransactionViewer$200.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.ParsingParameterList$data</ID>
|
||||||
<ID>MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingRawType$data</ID>
|
<ID>MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingRawType$data</ID>
|
||||||
<ID>MaxLineLength:AMQPTypeIdentifierParserTests.kt$AMQPTypeIdentifierParserTests$verify(" java.util.Map < java.util.Map< java.lang.String, java.lang.Integer >, java.util.Map < java.lang.Long , java.lang.String > >")</ID>
|
<ID>MaxLineLength:AMQPTypeIdentifierParserTests.kt$AMQPTypeIdentifierParserTests$verify(" java.util.Map < java.util.Map< java.lang.String, java.lang.Integer >, java.util.Map < java.lang.Long , java.lang.String > >")</ID>
|
||||||
<ID>MaxLineLength:ANSIProgressRenderer.kt$ANSIProgressRenderer$ansi.a("${IntStream.range(indent, indent).mapToObj { "\t" }.toList().joinToString(separator = "") { s -> s }} $errorIcon ${error.message}")</ID>
|
|
||||||
<ID>MaxLineLength:ANSIProgressRenderer.kt$StdoutANSIProgressRenderer$val consoleAppender = manager.configuration.appenders.values.filterIsInstance<ConsoleAppender>().singleOrNull { it.name == "Console-Selector" }</ID>
|
<ID>MaxLineLength:ANSIProgressRenderer.kt$StdoutANSIProgressRenderer$val consoleAppender = manager.configuration.appenders.values.filterIsInstance<ConsoleAppender>().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, 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>
|
<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 > 1 }.keys.first())</ID>
|
<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 > 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$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$val constraintAttachment = AttachmentWithContext(contractAttachment, contract, ltx.networkParameters!!.whitelistedContractImplementations)</ID>
|
||||||
|
<ID>MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier${ /** * Signature constraints are supported on min. platform version >= 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 >= 5. */ checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints") val constraintKey = constraint.key if (constraintKey is CompositeKey && constraintKey.leafKeys.size > 1) { checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 5, "Composite keys for signature constraints") val leafKeysNumber = constraintKey.leafKeys.size if (leafKeysNumber > 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: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</ID>
|
||||||
<ID>MaxLineLength:TransactionViewer.kt$TransactionViewer$private fun ObservableList<StateAndRef<ContractState>>.getParties()</ID>
|
<ID>MaxLineLength:TransactionViewer.kt$TransactionViewer$private fun ObservableList<StateAndRef<ContractState>>.getParties()</ID>
|
||||||
@ -3820,6 +3821,7 @@
|
|||||||
<ID>NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map<SessionId, SerializedBytes<Any>>)</ID>
|
<ID>NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map<SessionId, SerializedBytes<Any>>)</ID>
|
||||||
<ID>NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</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:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable</ID>
|
||||||
|
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>)</ID>
|
||||||
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>)</ID>
|
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>)</ID>
|
||||||
<ID>SpreadOperator:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$(*it.whitelist.toTypedArray())</ID>
|
<ID>SpreadOperator:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$(*it.whitelist.toTypedArray())</ID>
|
||||||
<ID>SpreadOperator:AbstractNode.kt$FlowStarterImpl$(logicType, *args)</ID>
|
<ID>SpreadOperator:AbstractNode.kt$FlowStarterImpl$(logicType, *args)</ID>
|
||||||
|
@ -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
|
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.
|
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
|
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.
|
Signature Constraints. This ensures that the version of each app is acceptable to the transaction's input states.
|
||||||
|
|
||||||
|
@ -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).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -11,4 +11,5 @@
|
|||||||
<include file="migration/vault-schema.changelog-v6.xml"/>
|
<include file="migration/vault-schema.changelog-v6.xml"/>
|
||||||
<include file="migration/vault-schema.changelog-v7.xml"/>
|
<include file="migration/vault-schema.changelog-v7.xml"/>
|
||||||
<include file="migration/vault-schema.changelog-v8.xml"/>
|
<include file="migration/vault-schema.changelog-v8.xml"/>
|
||||||
|
<include file="migration/vault-schema.changelog-v11.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
@ -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>
|
@ -7,6 +7,7 @@ import net.corda.core.internal.cordapp.set
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
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 net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -43,7 +44,8 @@ data class CustomCordapp(
|
|||||||
|
|
||||||
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes)
|
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
|
@VisibleForTesting
|
||||||
internal fun packageAsJar(file: Path) {
|
internal fun packageAsJar(file: Path) {
|
||||||
@ -73,20 +75,21 @@ data class CustomCordapp(
|
|||||||
|
|
||||||
private fun signJar(jarFile: Path) {
|
private fun signJar(jarFile: Path) {
|
||||||
if (signingInfo != null) {
|
if (signingInfo != null) {
|
||||||
val testKeystore = "_teststore"
|
|
||||||
val alias = "Test"
|
|
||||||
val pwd = "secret!"
|
|
||||||
val keyStorePathToUse = if (signingInfo.keyStorePath != null) {
|
val keyStorePathToUse = if (signingInfo.keyStorePath != null) {
|
||||||
signingInfo.keyStorePath
|
signingInfo.keyStorePath
|
||||||
} else {
|
} else {
|
||||||
defaultJarSignerDirectory.createDirectories()
|
defaultJarSignerDirectory.createDirectories()
|
||||||
if (!(defaultJarSignerDirectory / testKeystore).exists()) {
|
|
||||||
defaultJarSignerDirectory.generateKey(alias, pwd, "O=Test Company Ltd,OU=Test,L=London,C=GB")
|
|
||||||
}
|
|
||||||
defaultJarSignerDirectory
|
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)
|
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
|
||||||
logger.debug { "Signed Jar: $jarFile with public key $pk" }
|
logger.debug { "Signed Jar: $jarFile with public key $pk" }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug { "Unsigned Jar: $jarFile" }
|
logger.debug { "Unsigned Jar: $jarFile" }
|
||||||
}
|
}
|
||||||
@ -111,7 +114,7 @@ data class CustomCordapp(
|
|||||||
return ZipEntry(name).setCreationTime(epochFileTime).setLastAccessTime(epochFileTime).setLastModifiedTime(epochFileTime)
|
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 {
|
companion object {
|
||||||
private val logger = contextLogger()
|
private val logger = contextLogger()
|
||||||
|
@ -9,6 +9,7 @@ import java.io.Closeable
|
|||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -72,6 +73,15 @@ object JarSignatureTestUtils {
|
|||||||
return ks.getCertificate(alias).publicKey
|
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.getPublicKey(alias: String, storePassword: String) = getPublicKey(alias, "_teststore", storePassword)
|
||||||
|
|
||||||
fun Path.getJarSigners(fileName: String) =
|
fun Path.getJarSigners(fileName: String) =
|
||||||
|
Loading…
Reference in New Issue
Block a user