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
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.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

View File

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

View File

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

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.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))
/** /**

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

View File

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

View File

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