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:
Dominic Fox 2018-08-24 17:21:54 +01:00 committed by GitHub
parent bc330bd989
commit f81428eb53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 356 additions and 167 deletions

View File

@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.convertValue
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
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.PartialMerkleTree.PartialTree
import net.corda.core.identity.*
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.serialize
@ -80,10 +83,18 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
@Before
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()
cordappProvider = rigorousMock()
doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(testNetworkParameters()).whenever(services).networkParameters
doReturn(attachments).whenever(services).attachments
}
@Test

View File

@ -3,10 +3,13 @@ package net.corda.core.contracts
import net.corda.core.DoNotImplement
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
@CordaSerializable
@ -66,4 +69,18 @@ object AutomaticHashConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
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 })
}

View File

@ -5,15 +5,12 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializeAsTokenContext
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.CodeSigner
import java.security.cert.X509Certificate
import java.util.jar.JarInputStream
const val DEPLOYED_CORDAPP_UPLOADER = "app"
@ -35,9 +32,6 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
(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)
@ -47,24 +41,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
override fun open(): InputStream = attachmentData.inputStream()
override val signers by lazy {
// Can't start with empty set if we're doing intersections. Logically the null means "all possible signers":
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.
openAsJAR().use(JarSignatureCollector::collectSigningParties)
}
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)
}

View File

@ -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)

View File

@ -5,9 +5,7 @@ import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.ensureMinimumPlatformVersion
@ -111,20 +109,21 @@ open class TransactionBuilder @JvmOverloads constructor(
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
// will be available when building the transaction. In exceptional cases the TransactionStates must be created
// with an explicit [AttachmentConstraint]
/**
* Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s,
* [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter.
*
* 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 ->
when {
state.constraint !== AutomaticHashConstraint -> state
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
else -> {
services.cordappProvider.getContractAttachmentID(state.contract)?.let {
state.copy(constraint = HashAttachmentConstraint(it))
} ?: throw MissingContractAttachments(listOf(state))
}
}
state.withConstraint(when {
state.constraint !== AutomaticHashConstraint -> state.constraint
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) ->
WhitelistedByZoneAttachmentConstraint
else -> makeAttachmentConstraint(services, state)
})
}
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
@ -143,10 +142,28 @@ open class TransactionBuilder @JvmOverloads constructor(
}
}
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean {
return contractClassName in networkParameters.whitelistedContractImplementations.keys
private fun TransactionState<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
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.
* 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.
*/
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
/**

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -4,19 +4,22 @@ import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.CompositeKey
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.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentStorage
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.DummyCommandData
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.*
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -29,6 +32,7 @@ class TransactionBuilderTest {
private val notary = TestIdentity(DUMMY_NOTARY_NAME).party
private val services = rigorousMock<ServicesForResolution>()
private val contractAttachmentId = SecureHash.randomSHA256()
private val attachments = rigorousMock<AttachmentStorage>()
@Before
fun setup() {
@ -36,6 +40,7 @@ class TransactionBuilderTest {
doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
doReturn(attachments).whenever(services).attachments
}
@Test
@ -56,6 +61,8 @@ class TransactionBuilderTest {
@Test
fun `automatic hash constraint`() {
doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
val builder = TransactionBuilder()
.addOutputState(outputState)
@ -66,6 +73,8 @@ class TransactionBuilderTest {
@Test
fun `reference states`() {
doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1)
val builder = TransactionBuilder(notary)
@ -82,4 +91,40 @@ class TransactionBuilderTest {
val wtx = builder.toWireTransaction(services)
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()
}
}

View File

@ -1,12 +1,16 @@
package net.corda.nodeapi.internal
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
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 {
val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
doReturn(cordappProviderImpl).whenever(it).cordappProvider
doReturn(testNetworkParameters()).whenever(it).networkParameters
doReturn(attachments).whenever(it).attachments
}
@Test

View File

@ -553,7 +553,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
/** Block until all scheduled activity, active flows and network activity has ceased. */
fun waitQuiescent() {
busyLatch.await()
busyLatch.await(30000) // don't hang forever if for some reason things don't complete
}
override fun close() = stopNodes()