From f81428eb53dcbc89462c052a94e893bb61e24749 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Fri, 24 Aug 2018 17:21:54 +0100 Subject: [PATCH 1/4] 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 --- .../client/jackson/JacksonSupportTest.kt | 11 ++ .../core/contracts/AttachmentConstraint.kt | 17 ++ .../corda/core/internal/AbstractAttachment.kt | 26 +-- .../core/internal/JarSignatureCollector.kt | 66 +++++++ .../core/transactions/TransactionBuilder.kt | 54 ++++-- .../core/internal/AbstractAttachmentTest.kt | 120 ------------- .../internal/JarSignatureCollectorTest.kt | 161 ++++++++++++++++++ .../transactions/TransactionBuilderTest.kt | 53 +++++- ...tachmentsClassLoaderStaticContractTests.kt | 13 ++ .../node/internal/InternalMockNetwork.kt | 2 +- 10 files changed, 356 insertions(+), 167 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt delete mode 100644 core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt create mode 100644 core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 62f69be446..ae64ab4004 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -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().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 diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 4a07afa1be..b4fbe24ef4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -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 }) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 94a1789ba5..3c72e5d0ef 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -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 */ - 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? = 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()).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) } + diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt new file mode 100644 index 0000000000..70d8c84873 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -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 */ + 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 { + 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>> get() = + entries.thatAreSignable.shreddedFrom(this).toFileSignerSet().toList() + + private val Sequence.thatAreSignable: Sequence get() = + filterNot { entry -> entry.isDirectory || unsignableEntryName.matches(entry.name) } + + private fun Sequence.shreddedFrom(jar: JarInputStream): Sequence = 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.toFileSignerSet(): Sequence>> = + map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) } + + private fun Set.toPartiesOrderedByName(): List = map { + Party(it.signerCertPath.certificates[0] as X509Certificate) + }.sortedBy { it.name.toString() } // Sorted for determinism. + + private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } +} + +class InvalidJarSignersException(msg: String) : Exception(msg) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 831a6e098e..9b54509064 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -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.withConstraint(newConstraint: AttachmentConstraint) = + if (newConstraint == constraint) this else copy(constraint = newConstraint) + + private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState): 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) = + 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) = addCommand(Command(data, keys)) /** diff --git a/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt b/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt deleted file mode 100644 index bb759bc8d8..0000000000 --- a/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt +++ /dev/null @@ -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) - } -} diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt new file mode 100644 index 0000000000..b8620082d8 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -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.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( + """ + 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 { 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 + +} diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index c24a485776..f779c85678 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -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() private val contractAttachmentId = SecureHash.randomSHA256() + private val attachments = rigorousMock() @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 get() = emptyList() + } + + private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) { + override val id: SecureHash get() = throw UnsupportedOperationException() + + override val signers: List get() = parties.toList() + } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 5da0217f23..0909707ac4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -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().also { + doReturn(unsignedAttachment).whenever(it).openAttachment(any()) + } + private val serviceHub = rigorousMock().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 diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index cffe37a027..96050e777f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -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() From 36bfe268af615b7a4bac9e7dd0d9d066e196e405 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 28 Aug 2018 11:04:40 +0200 Subject: [PATCH 2/4] Revert back to using "relevancy" as "modifiable" states are not permissible by definition on an immutable ledger. (#3847) --- .../corda/core/node/services/VaultService.kt | 15 ++++++------ .../core/node/services/vault/QueryCriteria.kt | 12 +++++----- .../flows/WithReferencedStatesFlowTests.kt | 2 +- .../corda/finance/contracts/GetBalances.kt | 4 ++-- .../cash/selection/CashSelectionH2Impl.kt | 4 ++-- .../selection/CashSelectionPostgreSQLImpl.kt | 4 ++-- .../selection/CashSelectionSQLServerImpl.kt | 4 ++-- .../vault/HibernateQueryCriteriaParser.kt | 12 +++++----- .../node/services/vault/NodeVaultService.kt | 24 +++++++++---------- .../corda/node/services/vault/VaultSchema.kt | 6 ++--- .../migration/vault-schema.changelog-v5.xml | 8 +++---- .../services/vault/NodeVaultServiceTest.kt | 16 ++++++------- 12 files changed, 55 insertions(+), 56 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index fb695b7c44..a05dbf7f37 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -11,7 +11,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.Vault.StateModificationStatus.* +import net.corda.core.node.services.Vault.RelevancyStatus.* import net.corda.core.node.services.Vault.StateStatus import net.corda.core.node.services.vault.* import net.corda.core.serialization.CordaSerializable @@ -107,23 +107,22 @@ class Vault(val states: Iterable>) { } /** - * If the querying node is a participant in a state then it is classed as [MODIFIABLE], although technically the - * state is only _potentially_ modifiable as the contract code may forbid them from performing any actions. + * If the querying node is a participant in a state then it is classed as [RELEVANT]. * - * If the querying node is not a participant in a state then it is classed as [NOT_MODIFIABLE]. These types of + * If the querying node is not a participant in a state then it is classed as [NOT_RELEVANT]. These types of * states can still be recorded in the vault if the transaction containing them was recorded with the * [StatesToRecord.ALL_VISIBLE] flag. This will typically happen for things like reference data which can be * referenced in transactions as a [ReferencedStateAndRef] but cannot be modified by any party but the maintainer. * - * If both [MODIFIABLE] and [NOT_MODIFIABLE] states are required to be returned from a query, then the [ALL] flag + * If both [RELEVANT] and [NOT_RELEVANT] states are required to be returned from a query, then the [ALL] flag * can be used. * * NOTE: Default behaviour is for ALL STATES to be returned as this is how Corda behaved before the introduction of * this query criterion. */ @CordaSerializable - enum class StateModificationStatus { - MODIFIABLE, NOT_MODIFIABLE, ALL + enum class RelevancyStatus { + RELEVANT, NOT_RELEVANT, ALL } @CordaSerializable @@ -161,7 +160,7 @@ class Vault(val states: Iterable>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val isModifiable: Vault.StateModificationStatus? + val isRelevant: Vault.RelevancyStatus? ) { constructor(ref: StateRef, contractStateClassName: String, diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index dd6bb54a8e..d8a6f9374b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -73,7 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -90,7 +90,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null, - override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -125,14 +125,14 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { constructor( participants: List? = null, linearId: List? = null, status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, contractStateTypes: Set>? = null, - isRelevant: Vault.StateModificationStatus + isRelevant: Vault.RelevancyStatus ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant) constructor( @@ -175,7 +175,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -215,7 +215,7 @@ sealed class QueryCriteria : GenericQueryCriteria, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index b4237b1b93..e96eb9cd76 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -105,7 +105,7 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic(query).states.single() return subFlow(FinalityFlow( diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt index b1ff47295a..b35bb0fb41 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -22,7 +22,7 @@ private fun generateCashSumCriteria(currency: Currency): QueryCriteria { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). - val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isModifiable = Vault.StateModificationStatus.MODIFIABLE) + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isRelevant = Vault.RelevancyStatus.RELEVANT) return sumCriteria.and(ccyCriteria) } @@ -32,7 +32,7 @@ private fun generateCashSumsCriteria(): QueryCriteria { orderBy = Sort.Direction.DESC) } // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). - return QueryCriteria.VaultCustomQueryCriteria(sum, isModifiable = Vault.StateModificationStatus.MODIFIABLE) + return QueryCriteria.VaultCustomQueryCriteria(sum, isRelevant = Vault.RelevancyStatus.RELEVANT) } private fun rowsToAmount(currency: Currency, rows: Vault.Page>): Amount { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index b59be03fb3..af56a852b7 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -34,13 +34,13 @@ class CashSelectionH2Impl : AbstractCashSelection() { connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") } // state_status = 0 -> UNCONSUMED. - // is_modifiable = 0 -> MODIFIABLE. + // is_relevant = 0 -> RELEVANT. val selectJoin = """ SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND vs.is_modifiable = 0 + AND vs.is_relevant = 0 AND ccs.ccy_code = ? and @t < ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt index b08ddfbbfc..35feeec332 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt @@ -32,7 +32,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { // 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { // state_status = 0 -> UNCONSUMED. - // is_modifiable = 0 -> MODIFIABLE. + // is_relevant = 0 -> RELEVANT. val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies, nested.total+nested.pennies as total_pennies, nested.lock_id FROM @@ -42,7 +42,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND vs.is_modifiable = 0 + AND vs.is_relevant = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt index c8f457c91c..0ca0d2d668 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt @@ -43,7 +43,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() { override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { val sb = StringBuilder() // state_status = 0 -> UNCONSUMED. - // is_modifiable = 0 -> MODIFIABLE. + // is_relevant = 0 -> RELEVANT. sb.append( """ ;WITH CTE AS ( @@ -58,7 +58,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() { ON vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index WHERE vs.state_status = 0 - AND vs.is_modifiable = 0 + AND vs.is_relevant = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id IS NULL) """ diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index a3c6de8d20..76c4e2b09d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -487,16 +487,16 @@ class HibernateQueryCriteriaParser(val contractStateType: Class(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable)) + if (existingStatus != criteria.isRelevant) { + log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::isRelevant.name}] value $existingStatus with ${criteria.status}") + commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant)) } } else { - commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable) + commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant) } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 04d9fdf817..5c59c1e28d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -113,28 +113,28 @@ class NodeVaultService( // For EVERY state to be committed to the vault, this checks whether it is spendable by the recording // node. The behaviour is as follows: // - // 1) All vault updates marked as MODIFIABLE will, of, course all have isModifiable = true. - // 2) For ALL_VISIBLE updates, those which are not modifiable will have isModifiable = false. + // 1) All vault updates marked as RELEVANT will, of, course all have isRelevant = true. + // 2) For ALL_VISIBLE updates, those which are not relevant according to the relevancy rules will have isRelevant = false. // - // This is useful when it comes to querying for fungible states, when we do not want non-modifiable states + // This is useful when it comes to querying for fungible states, when we do not want non-relevant states // included in the result. // // The same functionality could be obtained by passing in a list of participants to the vault query, // however this: // // * requires a join on the participants table which results in slow queries - // * states may flip from being non-modifiable to modifiable + // * states may flip from being non-relevant to relevant // * it's more complicated for CorDapp developers // // Adding a new column in the "VaultStates" table was considered the best approach. val keys = stateOnly.participants.map { it.owningKey } - val isModifiable = isModifiable(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) + val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) val stateToAdd = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, recordedTime = clock.instant(), - isModifiable = if (isModifiable) Vault.StateModificationStatus.MODIFIABLE else Vault.StateModificationStatus.NOT_MODIFIABLE + isRelevant = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT ) stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) session.save(stateToAdd) @@ -188,7 +188,7 @@ class NodeVaultService( val ourNewStates = when (statesToRecord) { StatesToRecord.NONE -> throw AssertionError("Should not reach here") StatesToRecord.ONLY_RELEVANT -> tx.outputs.withIndex().filter { - isModifiable(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet()) + isRelevant(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet()) } StatesToRecord.ALL_VISIBLE -> tx.outputs.withIndex() }.map { tx.outRef(it.index) } @@ -217,7 +217,7 @@ class NodeVaultService( val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) } val (consumedStateAndRefs, producedStates) = ltx.inputs.zip(ltx.outputs).filter { (_, output) -> if (statesToRecord == StatesToRecord.ONLY_RELEVANT) { - isModifiable(output.data, myKeys.toSet()) + isRelevant(output.data, myKeys.toSet()) } else { true } @@ -396,13 +396,13 @@ class NodeVaultService( } // Enrich QueryCriteria with additional default attributes (such as soft locks). - // We only want to return MODIFIABLE states here. + // We only want to return RELEVANT states here. val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) val enrichedCriteria = QueryCriteria.VaultQueryCriteria( contractStateTypes = setOf(contractStateType), softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), - isModifiable = Vault.StateModificationStatus.MODIFIABLE + isRelevant = Vault.RelevancyStatus.RELEVANT ) val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) @@ -426,7 +426,7 @@ class NodeVaultService( } @VisibleForTesting - internal fun isModifiable(state: ContractState, myKeys: Set): Boolean { + internal fun isRelevant(state: ContractState, myKeys: Set): Boolean { val keysToCheck = when (state) { // Sometimes developers forget to add the owning key to participants for OwnableStates. // TODO: This logic should probably be moved to OwnableState so we can just do a simple intersection here. @@ -510,7 +510,7 @@ class NodeVaultService( vaultState.notary, vaultState.lockId, vaultState.lockUpdateTime, - vaultState.isModifiable)) + vaultState.isRelevant)) } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 5ade98461d..23072b2996 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -60,9 +60,9 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Column(name = "lock_id", nullable = true) var lockId: String? = null, - /** Used to determine whether a state is modifiable by the recording node */ - @Column(name = "is_modifiable", nullable = false) - var isModifiable: Vault.StateModificationStatus, + /** Used to determine whether a state abides by the relevancy rules of the recording node */ + @Column(name = "is_relevant", nullable = false) + var isRelevant: Vault.RelevancyStatus, /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) diff --git a/node/src/main/resources/migration/vault-schema.changelog-v5.xml b/node/src/main/resources/migration/vault-schema.changelog-v5.xml index 86a31530c8..6ed365a363 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-v5.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-v5.xml @@ -3,13 +3,13 @@ xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + - + - + diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 2ce6be39ba..da7081315a 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -529,17 +529,17 @@ class NodeVaultServiceTest { val amount = Amount(1000, Issued(BOC.ref(1), GBP)) val wellKnownCash = Cash.State(amount, identity.party) val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey)) - assertTrue { service.isModifiable(wellKnownCash, myKeys.toSet()) } + assertTrue { service.isRelevant(wellKnownCash, myKeys.toSet()) } val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) val anonymousCash = Cash.State(amount, anonymousIdentity.party) val anonymousKeys = services.keyManagementService.filterMyKeys(listOf(anonymousCash.owner.owningKey)) - assertTrue { service.isModifiable(anonymousCash, anonymousKeys.toSet()) } + assertTrue { service.isRelevant(anonymousCash, anonymousKeys.toSet()) } val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) val thirdPartyCash = Cash.State(amount, thirdPartyIdentity) val thirdPartyKeys = services.keyManagementService.filterMyKeys(listOf(thirdPartyCash.owner.owningKey)) - assertFalse { service.isModifiable(thirdPartyCash, thirdPartyKeys.toSet()) } + assertFalse { service.isRelevant(thirdPartyCash, thirdPartyKeys.toSet()) } } // TODO: Unit test linear state relevancy checks @@ -751,19 +751,19 @@ class NodeVaultServiceTest { services.recordTransactions(StatesToRecord.NONE, listOf(createTx(7, bankOfCorda.party))) // Test one. - // StateModificationStatus is MODIFIABLE by default. This should return two states. + // RelevancyStatus is RELEVANT by default. This should return two states. val resultOne = vaultService.queryBy().states.getNumbers() assertEquals(setOf(1, 3, 4, 5, 6), resultOne) // Test two. - // StateModificationStatus set to NOT_MODIFIABLE. - val criteriaTwo = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.NOT_MODIFIABLE) + // RelevancyStatus set to NOT_RELEVANT. + val criteriaTwo = VaultQueryCriteria(isRelevant = Vault.RelevancyStatus.NOT_RELEVANT) val resultTwo = vaultService.queryBy(criteriaTwo).states.getNumbers() assertEquals(setOf(4, 5), resultTwo) // Test three. - // StateModificationStatus set to ALL. - val criteriaThree = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.MODIFIABLE) + // RelevancyStatus set to ALL. + val criteriaThree = VaultQueryCriteria(isRelevant = Vault.RelevancyStatus.RELEVANT) val resultThree = vaultService.queryBy(criteriaThree).states.getNumbers() assertEquals(setOf(1, 3, 6), resultThree) From 63ebc394bfa4a5aafcd2f4db5ec1f06981bdb535 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Tue, 28 Aug 2018 12:05:54 +0100 Subject: [PATCH 3/4] Update docs to reflect constraint implementation (#3852) --- .../design/data-model-upgrades/signature-constraints.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/design/data-model-upgrades/signature-constraints.md b/docs/source/design/data-model-upgrades/signature-constraints.md index 9ee011532d..67f5de1b95 100644 --- a/docs/source/design/data-model-upgrades/signature-constraints.md +++ b/docs/source/design/data-model-upgrades/signature-constraints.md @@ -73,11 +73,11 @@ The proposed data structure for the new constraint type is as follows: ```kotlin data class SignatureAttachmentConstraint( - val key: CompositeKey + val key: PublicKey ) : AttachmentConstraint ``` -Therefore if a state advertises this constraint, along with a class name of `com.foo.Bar` then the definition of Bar must reside in an attachment with signatures sufficient to meet the given composite key. Multiple signers of a JAR is useful for decentralised administration of an app that wishes to have a threat model in which one of the app developers may go bad, but not a majority of them. For example there could be a 2-of-3 threshold of {app developer, auditor, R3} in which R3 is legally bound to only sign an upgrade if the auditor is unavailable e.g. has gone bankrupt. However, we anticipate that most constraints will be one-of-one for now. +Therefore if a state advertises this constraint, along with a class name of `com.foo.Bar` then the definition of Bar must reside in an attachment with signatures sufficient to meet the given public key. Note that the `key` may be a `CompositeKey` which is fulfilled by multiple signers. Multiple signers of a JAR is useful for decentralised administration of an app that wishes to have a threat model in which one of the app developers may go bad, but not a majority of them. For example there could be a 2-of-3 threshold of {app developer, auditor, R3} in which R3 is legally bound to only sign an upgrade if the auditor is unavailable e.g. has gone bankrupt. However, we anticipate that most constraints will be one-of-one for now. We will add a `signers` field to the `ContractAttachment` class that will be filled out at load time if the JAR is signed. The signers will be computed by checking the certificate chain for every file in the JAR, and any unsigned files will cause an exception to be thrown. @@ -147,9 +147,9 @@ There are some further issues to think through here: 3. Indirecting through package names increases centralisation somewhat, because now the zone operator has to agree to you taking ownership of a part of the namespace. This is also a privacy leak, it may expose what apps are being used on the network. *However* what it really exposes is application *developers* and not actual apps, and the zone op doesn't get to veto specific apps once they approved an app developer. More problematically unless an additional indirection is added to the network parameters, every change to the package ownership list requires a "hard fork" acceptance of new parameters. -### Using X.500 names in the constraint instead of CompositeKey +### Using X.500 names in the constraint instead of PublicKey -We advertise a `CompositeKey` in the constraint and *not* a set of `CordaX500Name` objects. This means that apps can be developed by entities that aren't in the network map (i.e. not a part of your zone), and it enables threshold keys, *but* the downside is there's no way to rotate or revoke a compromised key beyond adjusting the states themselves. We lose the indirection-through-identity. +We advertise a `PublicKey` (which may be a `CompositeKey`) in the constraint and *not* a set of `CordaX500Name` objects. This means that apps can be developed by entities that aren't in the network map (i.e. not a part of your zone), and it enables threshold keys, *but* the downside is there's no way to rotate or revoke a compromised key beyond adjusting the states themselves. We lose the indirection-through-identity. We could introduce such an indirection. This would disconnect the constraint from a particular public key. However then each zone an app is deployed to requires a new JAR signature by the creator, using a certificate issued by the zone operator. Because JARs can be signed by multiple certificates, this is OK, a JAR can be resigned N times if it's to be used in N zones. But it means that effectively zone operators get a power of veto over application developers, increasing centralisation and it increases required logistical efforts. From bdc12341cb0b90860c0d0557b0a26071bd6a3750 Mon Sep 17 00:00:00 2001 From: Dominic Fox Date: Thu, 30 Aug 2018 11:45:57 +0100 Subject: [PATCH 4/4] Restore ENT-only logic clobbered in merge --- .../net/corda/node/services/vault/NodeVaultService.kt | 2 ++ .../node/services/vault/VaultSoftLockManagerTest.kt | 10 ---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 01fdf4865a..92cb35eb57 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -173,6 +173,8 @@ class NodeVaultService( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, + lockId = uuid, + lockUpdateTime = if (uuid == null) null else now, recordedTime = clock.instant(), isRelevant = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT ) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index b680237a63..6784d43362 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -1,13 +1,3 @@ -/* - * R3 Proprietary and Confidential - * - * Copyright (c) 2018 R3 Limited. All rights reserved. - * - * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law. - * - * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. - */ - package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable