diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a34d83e643..4541d6002e 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4034,7 +4034,7 @@ public interface net.corda.core.node.services.VaultService public abstract net.corda.core.concurrent.CordaFuture> 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 diff --git a/core-tests/src/test/kotlin/net/corda/coretests/contracts/TransactionVerificationExceptionSerialisationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/contracts/TransactionVerificationExceptionSerialisationTests.kt index e8bf2cf807..0167d85b2a 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/contracts/TransactionVerificationExceptionSerialisationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/TransactionVerificationExceptionSerialisationTests.kt @@ -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") diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 826a7d9eb4..e941f8adce 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -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. diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index f12b2619e0..c05ae94680 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -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() val Attachment.contractVersion: Version get() = if (this is ContractAttachment) version else CordappImpl.DEFAULT_CORDAPP_VERSION diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index 51b2794ddb..25a5b07794 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -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) { // 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]!! diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 7993b58317..4e4a501069 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -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(val states: Iterable>) { /** * 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 diff --git a/detekt-baseline.xml b/detekt-baseline.xml index a6d67187d1..f09e6e8739 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -1225,6 +1225,7 @@ MagicNumber:TransactionUtils.kt$4 MagicNumber:TransactionVerificationException.kt$TransactionVerificationException.ConstraintPropagationRejection$3 MagicNumber:TransactionVerifierServiceInternal.kt$Verifier$4 + MagicNumber:TransactionVerifierServiceInternal.kt$Verifier$5 MagicNumber:TransactionViewer.kt$TransactionViewer$15.0 MagicNumber:TransactionViewer.kt$TransactionViewer$20.0 MagicNumber:TransactionViewer.kt$TransactionViewer$200.0 @@ -1329,7 +1330,6 @@ MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingParameterList$data MaxLineLength:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser.ParseState.ParsingRawType$data 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 > >") - MaxLineLength:ANSIProgressRenderer.kt$ANSIProgressRenderer$ansi.a("${IntStream.range(indent, indent).mapToObj { "\t" }.toList().joinToString(separator = "") { s -> s }} $errorIcon ${error.message}") MaxLineLength:ANSIProgressRenderer.kt$StdoutANSIProgressRenderer$val consoleAppender = manager.configuration.appenders.values.filterIsInstance<ConsoleAppender>().singleOrNull { it.name == "Console-Selector" } MaxLineLength:ANSIProgressRendererTest.kt$ANSIProgressRendererTest$checkTrackingState(captor, 5, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL))) 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))) @@ -3547,6 +3547,7 @@ 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()) MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier$if (result.keys != contractClasses) throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first()) MaxLineLength:TransactionVerifierServiceInternal.kt$Verifier$val constraintAttachment = AttachmentWithContext(contractAttachment, contract, ltx.networkParameters!!.whitelistedContractImplementations) + 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).") } } 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() } MaxLineLength:TransactionViewer.kt$TransactionViewer$private MaxLineLength:TransactionViewer.kt$TransactionViewer$private fun ObservableList<StateAndRef<ContractState>>.getParties() @@ -3820,6 +3821,7 @@ NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map<SessionId, SerializedBytes<Any>>) NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction) NestedBlockDepth:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable + NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) SpreadOperator:AMQPSerializationScheme.kt$AbstractAMQPSerializationScheme$(*it.whitelist.toTypedArray()) SpreadOperator:AbstractNode.kt$FlowStarterImpl$(logicType, *args) diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 12e79b6401..9ce088c679 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -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. diff --git a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintGatingTests.kt b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintGatingTests.kt new file mode 100644 index 0000000000..ae1ca0ec72 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintGatingTests.kt @@ -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).") + } + } + } + +} \ No newline at end of file diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index e934760df8..3ba9d52575 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -11,4 +11,5 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v11.xml b/node/src/main/resources/migration/vault-schema.changelog-v11.xml new file mode 100644 index 0000000000..d095e19bce --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v11.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt index e2665e96dc..cba5d9a523 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt @@ -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 } - val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd) - logger.debug { "Signed Jar: $jarFile with public key $pk" } + + 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() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt index b5f27fe0a8..00a827c1e1 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt @@ -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) =