mirror of
https://github.com/corda/corda.git
synced 2025-06-14 05:08:18 +00:00
Corda 1916: signature attachment constraints (#3839)
* Create constraint, extract Jar signature collection * Extract JarSignatureCollector into its own file * Jar signature collection throws exception if signatures are inconsistent * Focus testing in Jar signature collection * Extract some helper functions in test * Patch tests with mock attachment storage * Assert that generated constraint is satisfied by signed attachment * Clarify constraint selection logic * Explicit return types on extension methods * Link to docsite Signature Contrainsts documentation * Fix issue with shared JAR reading buffer
This commit is contained in:
@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode
|
|||||||
import com.fasterxml.jackson.databind.node.TextNode
|
import com.fasterxml.jackson.databind.node.TextNode
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||||
|
import com.nhaarman.mockito_kotlin.any
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.client.jackson.internal.childrenAs
|
import net.corda.client.jackson.internal.childrenAs
|
||||||
@ -18,9 +19,11 @@ import net.corda.core.crypto.*
|
|||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.PartialMerkleTree.PartialTree
|
import net.corda.core.crypto.PartialMerkleTree.PartialTree
|
||||||
import net.corda.core.identity.*
|
import net.corda.core.identity.*
|
||||||
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.internal.DigitalSignatureWithCert
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -80,10 +83,18 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
|
||||||
|
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val attachments = rigorousMock<AttachmentStorage>().also {
|
||||||
|
doReturn(unsignedAttachment).whenever(it).openAttachment(any())
|
||||||
|
}
|
||||||
services = rigorousMock()
|
services = rigorousMock()
|
||||||
cordappProvider = rigorousMock()
|
cordappProvider = rigorousMock()
|
||||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||||
|
doReturn(attachments).whenever(services).attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -3,10 +3,13 @@ package net.corda.core.contracts
|
|||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
|
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.internal.AttachmentWithContext
|
import net.corda.core.internal.AttachmentWithContext
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
@ -66,4 +69,18 @@ object AutomaticHashConstraint : AttachmentConstraint {
|
|||||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [AttachmentConstraint] that verifies that the attachment has signers that fulfil the provided [PublicKey].
|
||||||
|
* See: [Signature Constraints](https://docs.corda.net/design/data-model-upgrades/signature-constraints.html)
|
||||||
|
*
|
||||||
|
* @param key A [PublicKey] that must be fulfilled by the owning keys of the attachment's signing parties.
|
||||||
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
|
data class SignatureAttachmentConstraint(
|
||||||
|
val key: PublicKey
|
||||||
|
) : AttachmentConstraint {
|
||||||
|
override fun isSatisfiedBy(attachment: Attachment): Boolean =
|
||||||
|
key.isFulfilledBy(attachment.signers.map { it.owningKey })
|
||||||
}
|
}
|
@ -5,15 +5,12 @@ import net.corda.core.DeleteForDJVM
|
|||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.serialization.MissingAttachmentsException
|
import net.corda.core.serialization.MissingAttachmentsException
|
||||||
import net.corda.core.serialization.SerializeAsTokenContext
|
import net.corda.core.serialization.SerializeAsTokenContext
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.CodeSigner
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
const val DEPLOYED_CORDAPP_UPLOADER = "app"
|
const val DEPLOYED_CORDAPP_UPLOADER = "app"
|
||||||
@ -35,9 +32,6 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
|||||||
(a as? AbstractAttachment)?.attachmentData ?: a.open().readFully()
|
(a as? AbstractAttachment)?.attachmentData ?: a.open().readFully()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @see <https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File> */
|
|
||||||
private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val attachmentData: ByteArray by lazy(dataLoader)
|
protected val attachmentData: ByteArray by lazy(dataLoader)
|
||||||
@ -47,24 +41,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
|||||||
|
|
||||||
override fun open(): InputStream = attachmentData.inputStream()
|
override fun open(): InputStream = attachmentData.inputStream()
|
||||||
override val signers by lazy {
|
override val signers by lazy {
|
||||||
// Can't start with empty set if we're doing intersections. Logically the null means "all possible signers":
|
openAsJAR().use(JarSignatureCollector::collectSigningParties)
|
||||||
var attachmentSigners: MutableSet<CodeSigner>? = null
|
|
||||||
openAsJAR().use { jar ->
|
|
||||||
val shredder = ByteArray(1024)
|
|
||||||
while (true) {
|
|
||||||
val entry = jar.nextJarEntry ?: break
|
|
||||||
if (entry.isDirectory || unsignableEntryName.matches(entry.name)) continue
|
|
||||||
while (jar.read(shredder) != -1) { // Must read entry fully for codeSigners to be valid.
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
val entrySigners = entry.codeSigners ?: emptyArray()
|
|
||||||
attachmentSigners?.retainAll(entrySigners) ?: run { attachmentSigners = entrySigners.toMutableSet() }
|
|
||||||
if (attachmentSigners!!.isEmpty()) break // Performance short-circuit.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(attachmentSigners ?: emptySet<CodeSigner>()).map {
|
|
||||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
|
||||||
}.sortedBy { it.name.toString() } // Determinism.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
||||||
@ -86,3 +63,4 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
|||||||
}
|
}
|
||||||
throw FileNotFoundException(path)
|
throw FileNotFoundException(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import java.security.CodeSigner
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.jar.JarEntry
|
||||||
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class which provides the ability to extract a list of signing parties from a [JarInputStream].
|
||||||
|
*/
|
||||||
|
object JarSignatureCollector {
|
||||||
|
|
||||||
|
/** @see <https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File> */
|
||||||
|
private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an ordered list of every [Party] which has signed every signable item in the given [JarInputStream].
|
||||||
|
*
|
||||||
|
* @param jar The open [JarInputStream] to collect signing parties from.
|
||||||
|
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.
|
||||||
|
*/
|
||||||
|
fun collectSigningParties(jar: JarInputStream): List<Party> {
|
||||||
|
val signerSets = jar.fileSignerSets
|
||||||
|
if (signerSets.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val (firstFile, firstSignerSet) = signerSets.first()
|
||||||
|
for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) {
|
||||||
|
if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException(
|
||||||
|
"""
|
||||||
|
Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile
|
||||||
|
and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}.
|
||||||
|
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
|
||||||
|
constraints applied to attachment signatures.
|
||||||
|
""".trimIndent().replace('\n', ' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSignerSet.toPartiesOrderedByName()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val JarInputStream.fileSignerSets: List<Pair<String, Set<CodeSigner>>> get() =
|
||||||
|
entries.thatAreSignable.shreddedFrom(this).toFileSignerSet().toList()
|
||||||
|
|
||||||
|
private val Sequence<JarEntry>.thatAreSignable: Sequence<JarEntry> get() =
|
||||||
|
filterNot { entry -> entry.isDirectory || unsignableEntryName.matches(entry.name) }
|
||||||
|
|
||||||
|
private fun Sequence<JarEntry>.shreddedFrom(jar: JarInputStream): Sequence<JarEntry> = map { entry ->
|
||||||
|
val shredder = ByteArray(1024) // can't share or re-use this, as it's used to compute CRCs during shredding
|
||||||
|
entry.apply {
|
||||||
|
while (jar.read(shredder) != -1) { // Must read entry fully for codeSigners to be valid.
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Sequence<JarEntry>.toFileSignerSet(): Sequence<Pair<String, Set<CodeSigner>>> =
|
||||||
|
map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) }
|
||||||
|
|
||||||
|
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
||||||
|
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||||
|
}.sortedBy { it.name.toString() } // Sorted for determinism.
|
||||||
|
|
||||||
|
private val JarInputStream.entries get(): Sequence<JarEntry> = generateSequence(nextJarEntry) { nextJarEntry }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidJarSignersException(msg: String) : Exception(msg)
|
@ -5,9 +5,7 @@ import net.corda.core.CordaInternal
|
|||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.cordapp.CordappProvider
|
import net.corda.core.cordapp.CordappProvider
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SignableData
|
|
||||||
import net.corda.core.crypto.SignatureMetadata
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.internal.ensureMinimumPlatformVersion
|
import net.corda.core.internal.ensureMinimumPlatformVersion
|
||||||
@ -111,20 +109,21 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
services.ensureMinimumPlatformVersion(4, "Reference states")
|
services.ensureMinimumPlatformVersion(4, "Reference states")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint based on a global parameter.
|
/**
|
||||||
// The AutomaticHashConstraint allows for less boiler plate when constructing transactions since for the typical case the named contract
|
* Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s,
|
||||||
// will be available when building the transaction. In exceptional cases the TransactionStates must be created
|
* [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter.
|
||||||
// with an explicit [AttachmentConstraint]
|
*
|
||||||
|
* The [AutomaticHashConstraint] allows for less boiler plate when constructing transactions since for the
|
||||||
|
* typical case the named contract will be available when building the transaction. In exceptional cases the
|
||||||
|
* [TransactionStates] must be created with an explicit [AttachmentConstraint]
|
||||||
|
*/
|
||||||
val resolvedOutputs = outputs.map { state ->
|
val resolvedOutputs = outputs.map { state ->
|
||||||
when {
|
state.withConstraint(when {
|
||||||
state.constraint !== AutomaticHashConstraint -> state
|
state.constraint !== AutomaticHashConstraint -> state.constraint
|
||||||
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
|
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) ->
|
||||||
else -> {
|
WhitelistedByZoneAttachmentConstraint
|
||||||
services.cordappProvider.getContractAttachmentID(state.contract)?.let {
|
else -> makeAttachmentConstraint(services, state)
|
||||||
state.copy(constraint = HashAttachmentConstraint(it))
|
})
|
||||||
} ?: throw MissingContractAttachments(listOf(state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
@ -143,10 +142,28 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean {
|
private fun TransactionState<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
|
||||||
return contractClassName in networkParameters.whitelistedContractImplementations.keys
|
if (newConstraint == constraint) this else copy(constraint = newConstraint)
|
||||||
|
|
||||||
|
private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState<ContractState>): AttachmentConstraint {
|
||||||
|
val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract)
|
||||||
|
?: throw MissingContractAttachments(listOf(state))
|
||||||
|
|
||||||
|
val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers
|
||||||
|
?: throw MissingContractAttachments(listOf(state))
|
||||||
|
|
||||||
|
return when {
|
||||||
|
attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId)
|
||||||
|
else -> makeSignatureAttachmentConstraint(attachmentSigners)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<Party>) =
|
||||||
|
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build())
|
||||||
|
|
||||||
|
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
|
||||||
|
contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attachments added to the current transaction contain only the hashes of the current cordapps.
|
* The attachments added to the current transaction contain only the hashes of the current cordapps.
|
||||||
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
|
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
|
||||||
@ -276,6 +293,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
* signing [PublicKey]s.
|
* signing [PublicKey]s.
|
||||||
*/
|
*/
|
||||||
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
|
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
|
||||||
|
|
||||||
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
|
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
package net.corda.core.internal
|
|
||||||
|
|
||||||
import net.corda.testing.core.ALICE_NAME
|
|
||||||
import net.corda.testing.core.BOB_NAME
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.AfterClass
|
|
||||||
import org.junit.BeforeClass
|
|
||||||
import org.junit.Test
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class AbstractAttachmentTest {
|
|
||||||
companion object {
|
|
||||||
private val dir = Files.createTempDirectory(AbstractAttachmentTest::class.simpleName)
|
|
||||||
private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
|
||||||
private val shredder = (dir / "_shredder").toFile() // No need to delete after each test.
|
|
||||||
fun execute(vararg command: String) {
|
|
||||||
assertEquals(0, ProcessBuilder()
|
|
||||||
.inheritIO()
|
|
||||||
.redirectOutput(shredder)
|
|
||||||
.directory(dir.toFile())
|
|
||||||
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
|
||||||
.start()
|
|
||||||
.waitFor())
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
@JvmStatic
|
|
||||||
fun beforeClass() {
|
|
||||||
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", "alice", "-keypass", "alicepass", "-dname", ALICE_NAME.toString())
|
|
||||||
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", "bob", "-keypass", "bobpass", "-dname", BOB_NAME.toString())
|
|
||||||
(dir / "_signable1").writeLines(listOf("signable1"))
|
|
||||||
(dir / "_signable2").writeLines(listOf("signable2"))
|
|
||||||
(dir / "_signable3").writeLines(listOf("signable3"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load(name: String) = object : AbstractAttachment((dir / name)::readAll) {
|
|
||||||
override val id get() = throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterClass
|
|
||||||
@JvmStatic
|
|
||||||
fun afterClass() {
|
|
||||||
dir.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
dir.list {
|
|
||||||
it.filter { !it.fileName.toString().startsWith("_") }.forEach(Path::deleteRecursively)
|
|
||||||
}
|
|
||||||
assertThat(dir.list()).hasSize(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `empty jar has no signers`() {
|
|
||||||
(dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this.
|
|
||||||
execute("jar", "cvf", "attachment.jar", "META-INF")
|
|
||||||
assertEquals(emptyList(), load("attachment.jar").signers)
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice")
|
|
||||||
assertEquals(emptyList(), load("attachment.jar").signers) // There needs to have been a file for ALICE to sign.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `unsigned jar has no signers`() {
|
|
||||||
execute("jar", "cvf", "attachment.jar", "_signable1")
|
|
||||||
assertEquals(emptyList(), load("attachment.jar").signers)
|
|
||||||
execute("jar", "uvf", "attachment.jar", "_signable2")
|
|
||||||
assertEquals(emptyList(), load("attachment.jar").signers)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `one signer`() {
|
|
||||||
execute("jar", "cvf", "attachment.jar", "_signable1", "_signable2")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice")
|
|
||||||
assertEquals(listOf(ALICE_NAME), load("attachment.jar").signers.map { it.name }) // We only reused ALICE's distinguished name, so the keys will be different.
|
|
||||||
(dir / "my-dir").createDirectory()
|
|
||||||
execute("jar", "uvf", "attachment.jar", "my-dir")
|
|
||||||
assertEquals(listOf(ALICE_NAME), load("attachment.jar").signers.map { it.name }) // Unsigned directory is irrelevant.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `two signers`() {
|
|
||||||
execute("jar", "cvf", "attachment.jar", "_signable1", "_signable2")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob")
|
|
||||||
assertEquals(listOf(ALICE_NAME, BOB_NAME), load("attachment.jar").signers.map { it.name })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a party must sign all the files in the attachment to be a signer`() {
|
|
||||||
execute("jar", "cvf", "attachment.jar", "_signable1")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice")
|
|
||||||
assertEquals(listOf(ALICE_NAME), load("attachment.jar").signers.map { it.name })
|
|
||||||
execute("jar", "uvf", "attachment.jar", "_signable2")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob")
|
|
||||||
assertEquals(listOf(BOB_NAME), load("attachment.jar").signers.map { it.name }) // ALICE hasn't signed the new file.
|
|
||||||
execute("jar", "uvf", "attachment.jar", "_signable3")
|
|
||||||
assertEquals(emptyList(), load("attachment.jar").signers) // Neither party has signed the new file.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `bad signature is caught even if the party would not qualify as a signer`() {
|
|
||||||
(dir / "volatile").writeLines(listOf("volatile"))
|
|
||||||
execute("jar", "cvf", "attachment.jar", "volatile")
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice")
|
|
||||||
assertEquals(listOf(ALICE_NAME), load("attachment.jar").signers.map { it.name })
|
|
||||||
(dir / "volatile").writeLines(listOf("garbage"))
|
|
||||||
execute("jar", "uvf", "attachment.jar", "volatile", "_signable1") // ALICE's signature on volatile is now bad.
|
|
||||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob")
|
|
||||||
val a = load("attachment.jar")
|
|
||||||
// The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature:
|
|
||||||
assertThatThrownBy { a.signers }.isInstanceOf(SecurityException::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,161 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.AfterClass
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.jar.JarInputStream
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class JarSignatureCollectorTest {
|
||||||
|
companion object {
|
||||||
|
private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName)
|
||||||
|
private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
||||||
|
private val shredder = (dir / "_shredder").toFile() // No need to delete after each test.
|
||||||
|
|
||||||
|
fun execute(vararg command: String) {
|
||||||
|
assertEquals(0, ProcessBuilder()
|
||||||
|
.inheritIO()
|
||||||
|
.redirectOutput(shredder)
|
||||||
|
.directory(dir.toFile())
|
||||||
|
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
||||||
|
.start()
|
||||||
|
.waitFor())
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val FILENAME = "attachment.jar"
|
||||||
|
private const val ALICE = "alice"
|
||||||
|
private const val ALICE_PASS = "alicepass"
|
||||||
|
private const val BOB = "bob"
|
||||||
|
private const val BOB_PASS = "bobpass"
|
||||||
|
|
||||||
|
private fun generateKey(alias: String, password: String, name: CordaX500Name) =
|
||||||
|
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name.toString())
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun beforeClass() {
|
||||||
|
generateKey(ALICE, ALICE_PASS, ALICE_NAME)
|
||||||
|
generateKey(BOB, BOB_PASS, BOB_NAME)
|
||||||
|
|
||||||
|
(dir / "_signable1").writeLines(listOf("signable1"))
|
||||||
|
(dir / "_signable2").writeLines(listOf("signable2"))
|
||||||
|
(dir / "_signable3").writeLines(listOf("signable3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
@JvmStatic
|
||||||
|
fun afterClass() {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val List<Party>.names get() = map { it.name }
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
dir.list {
|
||||||
|
it.filter { !it.fileName.toString().startsWith("_") }.forEach(Path::deleteRecursively)
|
||||||
|
}
|
||||||
|
assertThat(dir.list()).hasSize(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty jar has no signers`() {
|
||||||
|
(dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this.
|
||||||
|
createJar("META-INF")
|
||||||
|
assertEquals(emptyList(), getJarSigners())
|
||||||
|
|
||||||
|
signAsAlice()
|
||||||
|
assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unsigned jar has no signers`() {
|
||||||
|
createJar("_signable1")
|
||||||
|
assertEquals(emptyList(), getJarSigners())
|
||||||
|
|
||||||
|
updateJar("_signable2")
|
||||||
|
assertEquals(emptyList(), getJarSigners())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `one signer`() {
|
||||||
|
createJar("_signable1", "_signable2")
|
||||||
|
signAsAlice()
|
||||||
|
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different.
|
||||||
|
|
||||||
|
(dir / "my-dir").createDirectory()
|
||||||
|
updateJar("my-dir")
|
||||||
|
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // Unsigned directory is irrelevant.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `two signers`() {
|
||||||
|
createJar("_signable1", "_signable2")
|
||||||
|
signAsAlice()
|
||||||
|
signAsBob()
|
||||||
|
|
||||||
|
assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all files must be signed by the same set of signers`() {
|
||||||
|
createJar("_signable1")
|
||||||
|
signAsAlice()
|
||||||
|
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
|
||||||
|
|
||||||
|
updateJar("_signable2")
|
||||||
|
signAsBob()
|
||||||
|
assertFailsWith<InvalidJarSignersException>(
|
||||||
|
"""
|
||||||
|
Mismatch between signers [O=Alice Corp, L=Madrid, C=ES, O=Bob Plc, L=Rome, C=IT] for file _signable1
|
||||||
|
and signers [O=Bob Plc, L=Rome, C=IT] for file _signable2.
|
||||||
|
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
|
||||||
|
constraints applied to attachment signatures.
|
||||||
|
""".trimIndent().replace('\n', ' ')
|
||||||
|
) { getJarSigners() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bad signature is caught even if the party would not qualify as a signer`() {
|
||||||
|
(dir / "volatile").writeLines(listOf("volatile"))
|
||||||
|
createJar("volatile")
|
||||||
|
signAsAlice()
|
||||||
|
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
|
||||||
|
|
||||||
|
(dir / "volatile").writeLines(listOf("garbage"))
|
||||||
|
updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad.
|
||||||
|
signAsBob()
|
||||||
|
// The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature:
|
||||||
|
assertFailsWith<SecurityException> { getJarSigners() }
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Helper functions
|
||||||
|
private fun createJar(vararg contents: String) =
|
||||||
|
execute(*(arrayOf("jar", "cvf", FILENAME) + contents))
|
||||||
|
|
||||||
|
private fun updateJar(vararg contents: String) =
|
||||||
|
execute(*(arrayOf("jar", "uvf", FILENAME) + contents))
|
||||||
|
|
||||||
|
private fun signJar(alias: String, password: String) =
|
||||||
|
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias)
|
||||||
|
|
||||||
|
private fun signAsAlice() = signJar(ALICE, ALICE_PASS)
|
||||||
|
private fun signAsBob() = signJar(BOB, BOB_PASS)
|
||||||
|
|
||||||
|
private fun getJarSigners() =
|
||||||
|
JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigningParties)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
}
|
@ -4,19 +4,22 @@ import com.nhaarman.mockito_kotlin.doReturn
|
|||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.cordapp.CordappProvider
|
import net.corda.core.cordapp.CordappProvider
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.ZoneVersionTooLowException
|
import net.corda.core.node.ZoneVersionTooLowException
|
||||||
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.core.DummyCommandData
|
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -29,6 +32,7 @@ class TransactionBuilderTest {
|
|||||||
private val notary = TestIdentity(DUMMY_NOTARY_NAME).party
|
private val notary = TestIdentity(DUMMY_NOTARY_NAME).party
|
||||||
private val services = rigorousMock<ServicesForResolution>()
|
private val services = rigorousMock<ServicesForResolution>()
|
||||||
private val contractAttachmentId = SecureHash.randomSHA256()
|
private val contractAttachmentId = SecureHash.randomSHA256()
|
||||||
|
private val attachments = rigorousMock<AttachmentStorage>()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
@ -36,6 +40,7 @@ class TransactionBuilderTest {
|
|||||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||||
|
doReturn(attachments).whenever(services).attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -56,6 +61,8 @@ class TransactionBuilderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `automatic hash constraint`() {
|
fun `automatic hash constraint`() {
|
||||||
|
doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
|
||||||
|
|
||||||
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
|
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
|
||||||
val builder = TransactionBuilder()
|
val builder = TransactionBuilder()
|
||||||
.addOutputState(outputState)
|
.addOutputState(outputState)
|
||||||
@ -66,6 +73,8 @@ class TransactionBuilderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `reference states`() {
|
fun `reference states`() {
|
||||||
|
doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
|
||||||
|
|
||||||
val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||||
val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1)
|
val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1)
|
||||||
val builder = TransactionBuilder(notary)
|
val builder = TransactionBuilder(notary)
|
||||||
@ -82,4 +91,40 @@ class TransactionBuilderTest {
|
|||||||
val wtx = builder.toWireTransaction(services)
|
val wtx = builder.toWireTransaction(services)
|
||||||
assertThat(wtx.references).containsOnly(referenceStateRef)
|
assertThat(wtx.references).containsOnly(referenceStateRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `automatic signature constraint`() {
|
||||||
|
val aliceParty = TestIdentity(ALICE_NAME).party
|
||||||
|
val bobParty = TestIdentity(BOB_NAME).party
|
||||||
|
val compositeKey = CompositeKey.Builder().addKeys(aliceParty.owningKey, bobParty.owningKey).build()
|
||||||
|
val expectedConstraint = SignatureAttachmentConstraint(compositeKey)
|
||||||
|
val signedAttachment = signedAttachment(aliceParty, bobParty)
|
||||||
|
|
||||||
|
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
|
||||||
|
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
|
||||||
|
|
||||||
|
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
|
||||||
|
|
||||||
|
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
|
||||||
|
val builder = TransactionBuilder()
|
||||||
|
.addOutputState(outputState)
|
||||||
|
.addCommand(DummyCommandData, notary.owningKey)
|
||||||
|
val wtx = builder.toWireTransaction(services)
|
||||||
|
|
||||||
|
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
|
||||||
|
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val signers: List<Party> get() = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) {
|
||||||
|
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val signers: List<Party> get() = parties.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package net.corda.nodeapi.internal
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.any
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
@ -66,11 +70,20 @@ class AttachmentsClassLoaderStaticContractTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
|
||||||
|
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachments = rigorousMock<AttachmentStorage>().also {
|
||||||
|
doReturn(unsignedAttachment).whenever(it).openAttachment(any())
|
||||||
|
}
|
||||||
|
|
||||||
private val serviceHub = rigorousMock<ServicesForResolution>().also {
|
private val serviceHub = rigorousMock<ServicesForResolution>().also {
|
||||||
val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())
|
val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())
|
||||||
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
|
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
|
||||||
doReturn(cordappProviderImpl).whenever(it).cordappProvider
|
doReturn(cordappProviderImpl).whenever(it).cordappProvider
|
||||||
doReturn(testNetworkParameters()).whenever(it).networkParameters
|
doReturn(testNetworkParameters()).whenever(it).networkParameters
|
||||||
|
doReturn(attachments).whenever(it).attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -553,7 +553,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
|||||||
|
|
||||||
/** Block until all scheduled activity, active flows and network activity has ceased. */
|
/** Block until all scheduled activity, active flows and network activity has ceased. */
|
||||||
fun waitQuiescent() {
|
fun waitQuiescent() {
|
||||||
busyLatch.await()
|
busyLatch.await(30000) // don't hang forever if for some reason things don't complete
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() = stopNodes()
|
override fun close() = stopNodes()
|
||||||
|
Reference in New Issue
Block a user