Merge pull request #2 from corda/feature/CORDA-1947/package_ownership

CORDA-1947 added packageOwnership
This commit is contained in:
Tudor Malene 2018-10-19 15:24:13 +01:00 committed by GitHub
commit 0234c1fcf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 561 additions and 110 deletions

View File

@ -191,10 +191,10 @@ class NodeMonitorModel : AutoCloseable {
val retryInterval = 5.seconds val retryInterval = 5.seconds
val client = CordaRPCClient( val client = CordaRPCClient(
nodeHostAndPort, nodeHostAndPort,
CordaRPCClientConfiguration.DEFAULT.copy( CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = retryInterval connectionMaxRetryInterval = retryInterval
) )
) )
do { do {
val connection = try { val connection = try {

View File

@ -2,6 +2,7 @@ package net.corda.core.contracts
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
/** /**
* Wrap an attachment in this if it is to be used as an executable contract attachment * Wrap an attachment in this if it is to be used as an executable contract attachment
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
*/ */
@KeepForDJVM @KeepForDJVM
@CordaSerializable @CordaSerializable
class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment { class ContractAttachment @JvmOverloads constructor(
val attachment: Attachment,
val contract: ContractClassName,
val additionalContracts: Set<ContractClassName> = emptySet(),
val uploader: String? = null,
override val signers: List<PublicKey> = emptyList()) : Attachment by attachment {
val allContracts: Set<ContractClassName> get() = additionalContracts + contract val allContracts: Set<ContractClassName> get() = additionalContracts + contract

View File

@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.NonEmptySet
import java.security.PublicKey import java.security.PublicKey
@ -163,4 +164,12 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
@DeleteForDJVM @DeleteForDJVM
class InvalidNotaryChange(txId: SecureHash) class InvalidNotaryChange(txId: SecureHash)
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
/**
* Thrown to indicate that a contract attachment is not signed by the network-wide package owner.
*/
class ContractAttachmentNotSignedByPackageOwnerException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String) : TransactionVerificationException(txId,
"""The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner specified in the network parameters.
Please check the source of this attachment and if it is malicious contact your zone operator to report this incident.
For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null)
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.utilities.days import net.corda.core.utilities.days
import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@ -22,6 +23,7 @@ import java.time.Instant
* of parameters. * of parameters.
* @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class. * @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class.
* This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html). * This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html).
* @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
* @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen
* during this period * during this period
*/ */
@ -35,7 +37,8 @@ data class NetworkParameters(
val modifiedTime: Instant, val modifiedTime: Instant,
val epoch: Int, val epoch: Int,
val whitelistedContractImplementations: Map<String, List<AttachmentId>>, val whitelistedContractImplementations: Map<String, List<AttachmentId>>,
val eventHorizon: Duration val eventHorizon: Duration,
val packageOwnership: Map<JavaPackageName, PublicKey>
) { ) {
@DeprecatedConstructorForDeserialization(1) @DeprecatedConstructorForDeserialization(1)
constructor (minimumPlatformVersion: Int, constructor (minimumPlatformVersion: Int,
@ -52,7 +55,28 @@ data class NetworkParameters(
modifiedTime, modifiedTime,
epoch, epoch,
whitelistedContractImplementations, whitelistedContractImplementations,
Int.MAX_VALUE.days Int.MAX_VALUE.days,
emptyMap()
)
@DeprecatedConstructorForDeserialization(2)
constructor (minimumPlatformVersion: Int,
notaries: List<NotaryInfo>,
maxMessageSize: Int,
maxTransactionSize: Int,
modifiedTime: Instant,
epoch: Int,
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
eventHorizon: Duration
) : this(minimumPlatformVersion,
notaries,
maxMessageSize,
maxTransactionSize,
modifiedTime,
epoch,
whitelistedContractImplementations,
eventHorizon,
emptyMap()
) )
init { init {
@ -63,6 +87,7 @@ data class NetworkParameters(
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" } require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" }
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" } require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
} }
fun copy(minimumPlatformVersion: Int, fun copy(minimumPlatformVersion: Int,
@ -83,20 +108,47 @@ data class NetworkParameters(
eventHorizon = eventHorizon) eventHorizon = eventHorizon)
} }
fun copy(minimumPlatformVersion: Int,
notaries: List<NotaryInfo>,
maxMessageSize: Int,
maxTransactionSize: Int,
modifiedTime: Instant,
epoch: Int,
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
eventHorizon: Duration
): NetworkParameters {
return copy(minimumPlatformVersion = minimumPlatformVersion,
notaries = notaries,
maxMessageSize = maxMessageSize,
maxTransactionSize = maxTransactionSize,
modifiedTime = modifiedTime,
epoch = epoch,
whitelistedContractImplementations = whitelistedContractImplementations,
eventHorizon = eventHorizon)
}
override fun toString(): String { override fun toString(): String {
return """NetworkParameters { return """NetworkParameters {
minimumPlatformVersion=$minimumPlatformVersion minimumPlatformVersion=$minimumPlatformVersion
notaries=$notaries notaries=$notaries
maxMessageSize=$maxMessageSize maxMessageSize=$maxMessageSize
maxTransactionSize=$maxTransactionSize maxTransactionSize=$maxTransactionSize
whitelistedContractImplementations { whitelistedContractImplementations {
${whitelistedContractImplementations.entries.joinToString("\n ")} ${whitelistedContractImplementations.entries.joinToString("\n ")}
} }
eventHorizon=$eventHorizon eventHorizon=$eventHorizon
modifiedTime=$modifiedTime modifiedTime=$modifiedTime
epoch=$epoch epoch=$epoch,
}""" packageOwnership= {
${packageOwnership.keys.joinToString()}}
}
}"""
} }
/**
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
*/
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull()
} }
/** /**
@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
* version. * version.
*/ */
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
/**
* A wrapper for a legal java package. Used by the network parameters to store package ownership.
*/
@CordaSerializable
data class JavaPackageName(val name: String) {
init {
require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" }
}
/**
* Returns true if the [fullClassName] is in a subpackage of the current package.
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
*
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
*/
fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true)
}
// Check if a string is a legal Java package name.
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
}
// Make sure that packages don't overlap so that ownership is clear.
private fun noOverlap(packages: Collection<JavaPackageName>) = packages.all { currentPackage ->
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") }
}

View File

@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction(
private fun verifyConstraints() { private fun verifyConstraints() {
val attachmentForConstraintVerification = AttachmentWithContext( val attachmentForConstraintVerification = AttachmentWithContext(
legacyContractAttachment as? ContractAttachment legacyContractAttachment as? ContractAttachment
?: ContractAttachment(legacyContractAttachment, legacyContractClassName), ?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers),
upgradedContract.legacyContract, upgradedContract.legacyContract,
networkParameters.whitelistedContractImplementations networkParameters.whitelistedContractImplementations
) )

View File

@ -3,6 +3,7 @@ package net.corda.core.transactions
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.castIfPossible import net.corda.core.internal.castIfPossible
@ -81,6 +82,7 @@ data class LedgerTransaction @JvmOverloads constructor(
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract() val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
// TODO - verify for version downgrade // TODO - verify for version downgrade
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract() validateStatesAgainstContract()
verifyConstraintsValidity(contractAttachmentsByContract) verifyConstraintsValidity(contractAttachmentsByContract)
verifyConstraints(contractAttachmentsByContract) verifyConstraints(contractAttachmentsByContract)
@ -105,6 +107,30 @@ data class LedgerTransaction @JvmOverloads constructor(
} }
} }
/**
* Verify that for each contract the network wide package owner is respected.
*
* TODO - revisit once transaction contains network parameters.
*/
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// This should never happen once we have network parameters in the transaction.
if (networkParameters == null) {
return
}
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it }
}.toMap()
contractsAndOwners.forEach { contract, owner ->
val attachment = contractAttachmentsByContract[contract]!!
if (!owner.isFulfilledBy(attachment.signers)) {
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
}
}
}
/** /**
* Enforces the validity of the actual constraints. * Enforces the validity of the actual constraints.
* * Constraints should be one of the valid supported ones. * * Constraints should be one of the valid supported ones.

View File

@ -0,0 +1,45 @@
package net.corda.core
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.div
import net.corda.nodeapi.internal.crypto.loadKeyStore
import java.io.FileInputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
import java.util.jar.JarInputStream
import kotlin.test.assertEquals
object JarSignatureTestUtils {
val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
fun Path.executeProcess(vararg command: String) {
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
assertEquals(0, ProcessBuilder()
.inheritIO()
.redirectOutput(shredder)
.redirectError(shredder)
.directory(this.toFile())
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
.start()
.waitFor())
}
fun Path.generateKey(alias: String, password: String, name: String) =
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name)
fun Path.createJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
fun Path.updateJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
return ks.getCertificate(alias).publicKey
}
fun Path.getJarSigners(fileName: String) =
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
}

View File

@ -0,0 +1,95 @@
package net.corda.core.contracts
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.JavaPackageName
import net.corda.core.transactions.LedgerTransaction
import net.corda.finance.POUNDS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import org.junit.Rule
import org.junit.Test
class PackageOwnershipVerificationTests {
private companion object {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
val ALICE_PARTY get() = ALICE.party
val ALICE_PUBKEY get() = ALICE.publicKey
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
val BOB_PARTY get() = BOB.party
val BOB_PUBKEY get() = BOB.publicKey
val dummyContract = "net.corda.core.contracts.DummyContract"
val OWNER_KEY_PAIR = Crypto.generateKeyPair()
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val ledgerServices = MockServices(
cordappPackages = listOf("net.corda.finance.contracts.asset"),
initialIdentity = ALICE,
identityService = rigorousMock<IdentityServiceInternal>().also {
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
},
networkParameters = testNetworkParameters()
.copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public))
)
@Test
fun `Happy path - Transaction validates when package signed by owner`() {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachment(dummyContract, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public))
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
command(ALICE_PUBKEY, DummyIssue())
verifies()
}
}
}
@Test
fun `Transaction validation fails when the selected attachment is not signed by the owner`() {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachment(dummyContract, SecureHash.allOnesHash, listOf(ALICE_PUBKEY))
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
command(ALICE_PUBKEY, DummyIssue())
failsWith("is not signed by the owner specified in the network parameters")
}
}
}
}
@BelongsToContract(DummyContract::class)
class DummyContractState : ContractState {
override val participants: List<AbstractParty>
get() = emptyList()
}
class DummyContract : Contract {
interface Commands : CommandData
class Create : Commands
override fun verify(tx: LedgerTransaction) {
//do nothing
}
}
class DummyIssue : TypeOnlyCommandData()

View File

@ -1,8 +1,11 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.identity.CordaX500Name import net.corda.core.JarSignatureTestUtils.createJar
import net.corda.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.getJarSigners
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.JarSignatureTestUtils.updateJar
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -10,30 +13,14 @@ import org.junit.After
import org.junit.AfterClass import org.junit.AfterClass
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import java.io.FileInputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
import java.util.jar.JarInputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class JarSignatureCollectorTest { class JarSignatureCollectorTest {
companion object { companion object {
private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName) 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 FILENAME = "attachment.jar"
private const val ALICE = "alice" private const val ALICE = "alice"
@ -41,14 +28,11 @@ class JarSignatureCollectorTest {
private const val BOB = "bob" private const val BOB = "bob"
private const val BOB_PASS = "bobpass" 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 @BeforeClass
@JvmStatic @JvmStatic
fun beforeClass() { fun beforeClass() {
generateKey(ALICE, ALICE_PASS, ALICE_NAME) dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
generateKey(BOB, BOB_PASS, BOB_NAME) dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
(dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable1").writeLines(listOf("signable1"))
(dir / "_signable2").writeLines(listOf("signable2")) (dir / "_signable2").writeLines(listOf("signable2"))
@ -75,49 +59,49 @@ class JarSignatureCollectorTest {
@Test @Test
fun `empty jar has no signers`() { fun `empty jar has no signers`() {
(dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this. (dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this.
createJar("META-INF") dir.createJar(FILENAME, "META-INF")
assertEquals(emptyList(), getJarSigners()) assertEquals(emptyList(), dir.getJarSigners(FILENAME))
signAsAlice() signAsAlice()
assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign. assertEquals(emptyList(), dir.getJarSigners(FILENAME)) // There needs to have been a file for ALICE to sign.
} }
@Test @Test
fun `unsigned jar has no signers`() { fun `unsigned jar has no signers`() {
createJar("_signable1") dir.createJar(FILENAME, "_signable1")
assertEquals(emptyList(), getJarSigners()) assertEquals(emptyList(), dir.getJarSigners(FILENAME))
updateJar("_signable2") dir.updateJar(FILENAME, "_signable2")
assertEquals(emptyList(), getJarSigners()) assertEquals(emptyList(), dir.getJarSigners(FILENAME))
} }
@Test @Test
fun `one signer`() { fun `one signer`() {
createJar("_signable1", "_signable2") dir.createJar(FILENAME, "_signable1", "_signable2")
val key = signAsAlice() val key = signAsAlice()
assertEquals(listOf(key), getJarSigners()) assertEquals(listOf(key), dir.getJarSigners(FILENAME))
(dir / "my-dir").createDirectory() (dir / "my-dir").createDirectory()
updateJar("my-dir") dir.updateJar(FILENAME, "my-dir")
assertEquals(listOf(key), getJarSigners()) // Unsigned directory is irrelevant. assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant.
} }
@Test @Test
fun `two signers`() { fun `two signers`() {
createJar("_signable1", "_signable2") dir.createJar(FILENAME, "_signable1", "_signable2")
val key1 = signAsAlice() val key1 = signAsAlice()
val key2 = signAsBob() val key2 = signAsBob()
assertEquals(setOf(key1, key2), getJarSigners().toSet()) assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet())
} }
@Test @Test
fun `all files must be signed by the same set of signers`() { fun `all files must be signed by the same set of signers`() {
createJar("_signable1") dir.createJar(FILENAME, "_signable1")
val key1 = signAsAlice() val key1 = signAsAlice()
assertEquals(listOf(key1), getJarSigners()) assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
updateJar("_signable2") dir.updateJar(FILENAME, "_signable2")
signAsBob() signAsBob()
assertFailsWith<InvalidJarSignersException>( assertFailsWith<InvalidJarSignersException>(
""" """
@ -126,41 +110,23 @@ class JarSignatureCollectorTest {
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
constraints applied to attachment signatures. constraints applied to attachment signatures.
""".trimIndent().replace('\n', ' ') """.trimIndent().replace('\n', ' ')
) { getJarSigners() } ) { dir.getJarSigners(FILENAME) }
} }
@Test @Test
fun `bad signature is caught even if the party would not qualify as a signer`() { fun `bad signature is caught even if the party would not qualify as a signer`() {
(dir / "volatile").writeLines(listOf("volatile")) (dir / "volatile").writeLines(listOf("volatile"))
createJar("volatile") dir.createJar(FILENAME, "volatile")
val key1 = signAsAlice() val key1 = signAsAlice()
assertEquals(listOf(key1), getJarSigners()) assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
(dir / "volatile").writeLines(listOf("garbage")) (dir / "volatile").writeLines(listOf("garbage"))
updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad. dir.updateJar(FILENAME, "volatile", "_signable1") // ALICE's signature on volatile is now bad.
signAsBob() 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: // 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() } assertFailsWith<SecurityException> { dir.getJarSigners(FILENAME) }
} }
//region Helper functions private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
private fun createJar(vararg contents: String) = private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
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): PublicKey {
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias)
val ks = loadKeyStore(dir.resolve("_teststore"), "storepass")
return ks.getCertificate(alias).publicKey
}
private fun signAsAlice() = signJar(ALICE, ALICE_PASS)
private fun signAsBob() = signJar(BOB, BOB_PASS)
private fun getJarSigners() =
JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigners)
//endregion
} }

View File

@ -134,5 +134,5 @@ class TransactionBuilderTest {
override val id: SecureHash get() = throw UnsupportedOperationException() override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<PublicKey> get() = parties.map { it.owningKey } override val signers: List<PublicKey> get() = parties.map { it.owningKey }
}, DummyContract.PROGRAM_ID) }, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey })
} }

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.createWireTransaction import net.corda.testing.internal.createWireTransaction
@ -129,7 +130,8 @@ class TransactionTests {
id, id,
null, null,
timeWindow, timeWindow,
privacySalt privacySalt,
testNetworkParameters()
) )
transaction.verify() transaction.verify()

View File

@ -125,6 +125,13 @@ The current set of network parameters:
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their :eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node. ``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners.
Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same.
Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported.
The transaction verification logic will throw an exception when this happens.
Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`.
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on. connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.

View File

@ -164,7 +164,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
@Suppress("LeakingThis") @Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize() val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage) val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
attachments.servicesForResolution = it
}
@Suppress("LeakingThis") @Suppress("LeakingThis")
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
@ -1052,7 +1054,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else. // either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters)
} }

View File

@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
output.writeString(obj.contract) output.writeString(obj.contract)
kryo.writeClassAndObject(output, obj.additionalContracts) kryo.writeClassAndObject(output, obj.additionalContracts)
output.writeString(obj.uploader) output.writeString(obj.uploader)
kryo.writeClassAndObject(output, obj.signers)
} }
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment { override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
if (kryo.serializationContext() != null) { if (kryo.serializationContext() != null) {
val attachmentHash = SecureHash.SHA256(input.readBytes(32)) val attachmentHash = SecureHash.SHA256(input.readBytes(32))
val contract = input.readString() val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName> val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString() val uploader = input.readString()
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val context = kryo.serializationContext()!! val context = kryo.serializationContext()!!
val attachmentStorage = context.serviceHub.attachments val attachmentStorage = context.serviceHub.attachments
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
override val id = attachmentHash override val id = attachmentHash
} }
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader) return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
} else { } else {
val attachment = GeneratedAttachment(input.readBytesWithLength()) val attachment = GeneratedAttachment(input.readBytesWithLength())
val contract = input.readString() val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName> val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString() val uploader = input.readString()
return ContractAttachment(attachment, contract, additionalContracts, uploader) val signers = kryo.readClassAndObject(input) as List<PublicKey>
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
} }
} }
} }

View File

@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
import com.google.common.hash.Hashing import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream import com.google.common.io.CountingInputStream
import net.corda.core.ClientRelevantError
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.AttachmentSort
@ -31,6 +34,7 @@ import java.io.FilterInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.file.Paths import java.nio.file.Paths
import java.security.PublicKey
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
@ -46,6 +50,10 @@ class NodeAttachmentService(
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
private val database: CordaPersistence private val database: CordaPersistence
) : AttachmentStorageInternal, SingletonSerializeAsToken() { ) : AttachmentStorageInternal, SingletonSerializeAsToken() {
// This is to break the circular dependency.
lateinit var servicesForResolution: ServicesForResolution
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -95,7 +103,13 @@ class NodeAttachmentService(
@Column(name = "contract_class_name", nullable = false) @Column(name = "contract_class_name", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))], @CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__ctr_class__attachments")) foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null var contractClassNames: List<ContractClassName>? = null,
@ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER)
@Column(name = "signer", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__signers__attachments"))
var signers: List<PublicKey>? = null
) )
@VisibleForTesting @VisibleForTesting
@ -213,11 +227,13 @@ class NodeAttachmentService(
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? { private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
return database.transaction { return database.transaction {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
?: return@transaction null
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let { val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) { if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader) ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers
?: emptyList())
} else { } else {
it it
} }
@ -291,14 +307,19 @@ class NodeAttachmentService(
val id = bytes.sha256() val id = bytes.sha256()
if (!hasAttachment(id)) { if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream()) checkIsAValidJAR(bytes.inputStream())
val jarSigners = getSigners(bytes)
val session = currentDBSession() val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment( val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(), attId = id.toString(),
content = bytes, content = bytes,
uploader = uploader, uploader = uploader,
filename = filename, filename = filename,
contractClassNames = contractClassNames contractClassNames = contractClassNames,
signers = jarSigners
) )
session.save(attachment) session.save(attachment)
attachmentCount.inc() attachmentCount.inc()
log.info("Stored new attachment $id") log.info("Stored new attachment $id")
@ -310,6 +331,9 @@ class NodeAttachmentService(
} }
} }
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId { override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try { return try {

View File

@ -0,0 +1,18 @@
package net.corda.node.services.persistence
import net.corda.core.crypto.Crypto
import net.corda.core.utilities.hexToByteArray
import net.corda.core.utilities.toHex
import java.security.PublicKey
import javax.persistence.AttributeConverter
import javax.persistence.Converter
/**
* Converts to and from a Public key into a hex encoded string.
* Used by JPA to automatically map a [PublicKey] to a text column
*/
@Converter(autoApply = true)
class PublicKeyToTextConverter : AttributeConverter<PublicKey, String> {
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex()
override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) }
}

View File

@ -14,4 +14,17 @@
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" /> <renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="add_signers">
<createTable tableName="node_attachments_signers">
<column name="att_id" type="NVARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="signer" type="NVARCHAR(1024)"/>
</createTable>
<addForeignKeyConstraint baseColumnNames="att_id" baseTableName="node_attachments_signers"
constraintName="FK__signers__attachments"
referencedColumnNames="att_id" referencedTableName="node_attachments"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@ -2,6 +2,8 @@ package net.corda.node.internal
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.crypto.generateKeyPair
import net.corda.core.node.JavaPackageName
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.core.node.NotaryInfo import net.corda.core.node.NotaryInfo
import net.corda.core.utilities.days
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -65,7 +68,8 @@ class NetworkParametersTest {
fun `choosing notary not specified in network parameters will fail`() { fun `choosing notary not specified in network parameters will fail`() {
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = { val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
val notary = NotaryConfig(false) val notary = NotaryConfig(false)
doReturn(notary).whenever(it).notary})) doReturn(notary).whenever(it).notary
}))
val fakeNotaryId = fakeNotary.info.singleIdentity() val fakeNotaryId = fakeNotary.info.singleIdentity()
val alice = mockNet.createPartyNode(ALICE_NAME) val alice = mockNet.createPartyNode(ALICE_NAME)
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId) assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
@ -87,6 +91,62 @@ class NetworkParametersTest {
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize") }.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
} }
@Test
fun `package ownership checks are correct`() {
val key1 = generateKeyPair().public
val key2 = generateKeyPair().public
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.!example.stuff") to key2
)
)
}.withMessageContaining("Attempting to whitelist illegal java package")
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.example.stuff") to key2
)
)
}.withMessage("multiple packages added to the packageOwnership overlap.")
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.examplestuff") to key2
)
)
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
}
// Helpers // Helpers
private fun dropParametersToDir(dir: Path, params: NetworkParameters) { private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
NetworkParametersCopier(params).install(dir) NetworkParametersCopier(params).install(dir)

View File

@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.JarSignatureTestUtils.createJar
import net.corda.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder import net.corda.core.node.services.vault.Builder
@ -18,32 +24,39 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.node.utilities.TestingNamedCacheFactory
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.LogHelper import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After import org.junit.*
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.URI
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException import java.nio.file.*
import java.nio.file.FileSystem import java.security.PublicKey
import java.nio.file.Path
import java.util.jar.JarEntry import java.util.jar.JarEntry
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import javax.tools.JavaFileObject
import javax.tools.SimpleJavaFileObject
import javax.tools.StandardLocation
import javax.tools.ToolProvider
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNull import kotlin.test.assertNull
class NodeAttachmentServiceTest { class NodeAttachmentServiceTest {
// Use an in memory file system for testing attachment storage. // Use an in memory file system for testing attachment storage.
private lateinit var fs: FileSystem private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService private lateinit var storage: NodeAttachmentService
private val services = rigorousMock<ServicesForResolution>()
@Before @Before
fun setUp() { fun setUp() {
@ -52,18 +65,40 @@ class NodeAttachmentServiceTest {
val dataSourceProperties = makeTestDataSourceProperties() val dataSourceProperties = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
fs = Jimfs.newFileSystem(Configuration.unix()) fs = Jimfs.newFileSystem(Configuration.unix())
doReturn(testNetworkParameters()).whenever(services).networkParameters
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
database.transaction { database.transaction {
it.start() it.start()
} }
} }
storage.servicesForResolution = services
} }
@After @After
fun tearDown() { fun tearDown() {
dir.list { subdir ->
subdir.forEach(Path::deleteRecursively)
}
database.close() database.close()
} }
@Test
fun `importing a signed jar saves the signers to the storage`() {
val jarAndSigner = makeTestSignedContractJar("com.example.MyContract")
val signedJar = jarAndSigner.first
val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null)
assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash })
}
@Test
fun `importing a non-signed jar will save no signers`() {
val jarName = makeTestContractJar("com.example.MyContract")
val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null)
assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size)
}
@Test @Test
fun `insert and retrieve`() { fun `insert and retrieve`() {
val (testJar, expectedHash) = makeTestJar() val (testJar, expectedHash) = makeTestJar()
@ -289,7 +324,20 @@ class NodeAttachmentServiceTest {
return Pair(file, file.readAll().sha256()) return Pair(file, file.readAll().sha256())
} }
private companion object { companion object {
private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName)
@BeforeClass
@JvmStatic
fun beforeClass() {
}
@AfterClass
@JvmStatic
fun afterClass() {
dir.deleteRecursively()
}
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) { private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
output.use { output.use {
val jar = JarOutputStream(it) val jar = JarOutputStream(it)
@ -305,5 +353,48 @@ class NodeAttachmentServiceTest {
jar.closeEntry() jar.closeEntry()
} }
} }
private fun makeTestSignedContractJar(contractName: String): Pair<Path, PublicKey> {
val alias = "testAlias"
val pwd = "testPassword"
dir.generateKey(alias, pwd, ALICE_NAME.toString())
val jarName = makeTestContractJar(contractName)
val signer = dir.signJar(jarName, alias, pwd)
return dir.resolve(jarName) to signer
}
private fun makeTestContractJar(contractName: String): String {
val packages = contractName.split(".")
val jarName = "testattachment.jar"
val className = packages.last()
createTestClass(className, packages.subList(0, packages.size - 1))
dir.createJar(jarName, "${contractName.replace(".", "/")}.class")
return jarName
}
private fun createTestClass(className: String, packages: List<String>): Path {
val newClass = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*;
import net.corda.core.transactions.*;
public class $className implements Contract {
@Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
}
}
""".trimIndent()
val compiler = ToolProvider.getSystemJavaCompiler()
val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) {
override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence {
return newClass
}
}
val fileManager = compiler.getStandardFileManager(null, null, null)
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile()))
val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call()
return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name)
}
} }
} }

View File

@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException
import net.corda.serialization.internal.GeneratedAttachment import net.corda.serialization.internal.GeneratedAttachment
import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.CustomSerializer
import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.SerializerFactory
import java.security.PublicKey
/** /**
* A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly. * A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly.
@ -23,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
} catch (e: Exception) { } catch (e: Exception) {
throw MissingAttachmentsException(listOf(obj.id)) throw MissingAttachmentsException(listOf(obj.id))
} }
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader) return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signers)
} }
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment { override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader) return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers)
} }
@KeepForDJVM @KeepForDJVM
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?) data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?, val signers: List<PublicKey>)
} }

View File

@ -1,11 +1,13 @@
package net.corda.testing.contracts package net.corda.testing.contracts
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
/** /**
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. * Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
*/ */
@BelongsToContract(DummyContract::class)
data class DummyState @JvmOverloads constructor ( data class DummyState @JvmOverloads constructor (
/** Some information that the state represents for test purposes. **/ /** Some information that the state represents for test purposes. **/
val magicNumber: Int = 0, val magicNumber: Int = 0,

View File

@ -7,6 +7,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.readFully import net.corda.core.internal.readFully
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
@ -71,7 +72,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
val sha256 = attachmentId ?: bytes.sha256() val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) { if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256, signers) val baseAttachment = MockAttachment({ bytes }, sha256, signers)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader) val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers)
_files[sha256] = Pair(attachment, bytes) _files[sha256] = Pair(attachment, bytes)
} }
return sha256 return sha256