mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge pull request #2 from corda/feature/CORDA-1947/package_ownership
CORDA-1947 added packageOwnership
This commit is contained in:
commit
0234c1fcf8
@ -2,6 +2,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
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
|
||||
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@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
|
||||
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import java.security.PublicKey
|
||||
@ -163,4 +164,12 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
@DeleteForDJVM
|
||||
class InvalidNotaryChange(txId: SecureHash)
|
||||
: 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)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.utilities.days
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@ -22,6 +23,7 @@ import java.time.Instant
|
||||
* of parameters.
|
||||
* @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).
|
||||
* @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
|
||||
* during this period
|
||||
*/
|
||||
@ -35,7 +37,8 @@ data class NetworkParameters(
|
||||
val modifiedTime: Instant,
|
||||
val epoch: Int,
|
||||
val whitelistedContractImplementations: Map<String, List<AttachmentId>>,
|
||||
val eventHorizon: Duration
|
||||
val eventHorizon: Duration,
|
||||
val packageOwnership: Map<JavaPackageName, PublicKey>
|
||||
) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (minimumPlatformVersion: Int,
|
||||
@ -52,7 +55,28 @@ data class NetworkParameters(
|
||||
modifiedTime,
|
||||
epoch,
|
||||
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 {
|
||||
@ -63,6 +87,7 @@ data class NetworkParameters(
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" }
|
||||
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
|
||||
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
|
||||
}
|
||||
|
||||
fun copy(minimumPlatformVersion: Int,
|
||||
@ -83,6 +108,25 @@ data class NetworkParameters(
|
||||
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 {
|
||||
return """NetworkParameters {
|
||||
minimumPlatformVersion=$minimumPlatformVersion
|
||||
@ -94,9 +138,17 @@ data class NetworkParameters(
|
||||
}
|
||||
eventHorizon=$eventHorizon
|
||||
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.
|
||||
*/
|
||||
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}.") }
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
private fun verifyConstraints() {
|
||||
val attachmentForConstraintVerification = AttachmentWithContext(
|
||||
legacyContractAttachment as? ContractAttachment
|
||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName),
|
||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers),
|
||||
upgradedContract.legacyContract,
|
||||
networkParameters.whitelistedContractImplementations
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.transactions
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.castIfPossible
|
||||
@ -81,6 +82,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract()
|
||||
verifyConstraintsValidity(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.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
|
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal file
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal 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)
|
||||
}
|
@ -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()
|
@ -1,8 +1,11 @@
|
||||
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.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -10,30 +13,14 @@ 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.security.PublicKey
|
||||
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"
|
||||
@ -41,14 +28,11 @@ class JarSignatureCollectorTest {
|
||||
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.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
||||
dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
|
||||
|
||||
(dir / "_signable1").writeLines(listOf("signable1"))
|
||||
(dir / "_signable2").writeLines(listOf("signable2"))
|
||||
@ -75,49 +59,49 @@ class JarSignatureCollectorTest {
|
||||
@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())
|
||||
dir.createJar(FILENAME, "META-INF")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
|
||||
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
|
||||
fun `unsigned jar has no signers`() {
|
||||
createJar("_signable1")
|
||||
assertEquals(emptyList(), getJarSigners())
|
||||
dir.createJar(FILENAME, "_signable1")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
|
||||
updateJar("_signable2")
|
||||
assertEquals(emptyList(), getJarSigners())
|
||||
dir.updateJar(FILENAME, "_signable2")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one signer`() {
|
||||
createJar("_signable1", "_signable2")
|
||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||
val key = signAsAlice()
|
||||
assertEquals(listOf(key), getJarSigners())
|
||||
assertEquals(listOf(key), dir.getJarSigners(FILENAME))
|
||||
|
||||
(dir / "my-dir").createDirectory()
|
||||
updateJar("my-dir")
|
||||
assertEquals(listOf(key), getJarSigners()) // Unsigned directory is irrelevant.
|
||||
dir.updateJar(FILENAME, "my-dir")
|
||||
assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two signers`() {
|
||||
createJar("_signable1", "_signable2")
|
||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||
val key1 = signAsAlice()
|
||||
val key2 = signAsBob()
|
||||
|
||||
assertEquals(setOf(key1, key2), getJarSigners().toSet())
|
||||
assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all files must be signed by the same set of signers`() {
|
||||
createJar("_signable1")
|
||||
dir.createJar(FILENAME, "_signable1")
|
||||
val key1 = signAsAlice()
|
||||
assertEquals(listOf(key1), getJarSigners())
|
||||
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
|
||||
|
||||
updateJar("_signable2")
|
||||
dir.updateJar(FILENAME, "_signable2")
|
||||
signAsBob()
|
||||
assertFailsWith<InvalidJarSignersException>(
|
||||
"""
|
||||
@ -126,41 +110,23 @@ class JarSignatureCollectorTest {
|
||||
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() }
|
||||
) { dir.getJarSigners(FILENAME) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bad signature is caught even if the party would not qualify as a signer`() {
|
||||
(dir / "volatile").writeLines(listOf("volatile"))
|
||||
createJar("volatile")
|
||||
dir.createJar(FILENAME, "volatile")
|
||||
val key1 = signAsAlice()
|
||||
assertEquals(listOf(key1), getJarSigners())
|
||||
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
|
||||
|
||||
(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()
|
||||
// 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 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): 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
|
||||
|
||||
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
|
||||
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
|
||||
}
|
||||
|
@ -134,5 +134,5 @@ class TransactionBuilderTest {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
|
||||
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
|
||||
}, DummyContract.PROGRAM_ID)
|
||||
}, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey })
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
@ -129,7 +130,8 @@ class TransactionTests {
|
||||
id,
|
||||
null,
|
||||
timeWindow,
|
||||
privacySalt
|
||||
privacySalt,
|
||||
testNetworkParameters()
|
||||
)
|
||||
|
||||
transaction.verify()
|
||||
|
@ -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
|
||||
``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
|
||||
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.
|
||||
|
||||
|
@ -164,7 +164,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
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")
|
||||
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
|
||||
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
|
||||
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
||||
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", "")
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters)
|
||||
}
|
||||
|
@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
|
||||
output.writeString(obj.contract)
|
||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||
output.writeString(obj.uploader)
|
||||
kryo.writeClassAndObject(output, obj.signers)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
||||
if (kryo.serializationContext() != null) {
|
||||
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||
val context = kryo.serializationContext()!!
|
||||
val attachmentStorage = context.serviceHub.attachments
|
||||
|
||||
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
|
||||
override val id = attachmentHash
|
||||
}
|
||||
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
|
||||
} else {
|
||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import com.google.common.io.CountingInputStream
|
||||
import net.corda.core.ClientRelevantError
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
@ -31,6 +34,7 @@ import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
@ -46,6 +50,10 @@ class NodeAttachmentService(
|
||||
cacheFactory: NamedCacheFactory,
|
||||
private val database: CordaPersistence
|
||||
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
|
||||
|
||||
// This is to break the circular dependency.
|
||||
lateinit var servicesForResolution: ServicesForResolution
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
@ -95,7 +103,13 @@ class NodeAttachmentService(
|
||||
@Column(name = "contract_class_name", nullable = false)
|
||||
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
|
||||
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
|
||||
@ -213,11 +227,13 @@ class NodeAttachmentService(
|
||||
|
||||
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
|
||||
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 contracts = attachment.contractClassNames
|
||||
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 {
|
||||
it
|
||||
}
|
||||
@ -291,14 +307,19 @@ class NodeAttachmentService(
|
||||
val id = bytes.sha256()
|
||||
if (!hasAttachment(id)) {
|
||||
checkIsAValidJAR(bytes.inputStream())
|
||||
|
||||
val jarSigners = getSigners(bytes)
|
||||
|
||||
val session = currentDBSession()
|
||||
val attachment = NodeAttachmentService.DBAttachment(
|
||||
attId = id.toString(),
|
||||
content = bytes,
|
||||
uploader = uploader,
|
||||
filename = filename,
|
||||
contractClassNames = contractClassNames
|
||||
contractClassNames = contractClassNames,
|
||||
signers = jarSigners
|
||||
)
|
||||
|
||||
session.save(attachment)
|
||||
attachmentCount.inc()
|
||||
log.info("Stored new attachment $id")
|
||||
@ -310,6 +331,9 @@ class NodeAttachmentService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSigners(attachmentBytes: ByteArray) =
|
||||
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||
return try {
|
||||
|
@ -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()) }
|
||||
}
|
@ -14,4 +14,17 @@
|
||||
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
|
||||
</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>
|
@ -2,6 +2,8 @@ package net.corda.node.internal
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
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.getOrThrow
|
||||
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.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_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`() {
|
||||
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
|
||||
val notary = NotaryConfig(false)
|
||||
doReturn(notary).whenever(it).notary}))
|
||||
doReturn(notary).whenever(it).notary
|
||||
}))
|
||||
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
||||
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
||||
@ -87,6 +91,62 @@ class NetworkParametersTest {
|
||||
}.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
|
||||
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
|
||||
NetworkParametersCopier(params).install(dir)
|
||||
|
@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.jimfs.Configuration
|
||||
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.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
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.AttachmentSort
|
||||
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.nodeapi.internal.persistence.CordaPersistence
|
||||
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.rigorousMock
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.*
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarEntry
|
||||
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.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
|
||||
class NodeAttachmentServiceTest {
|
||||
|
||||
// Use an in memory file system for testing attachment storage.
|
||||
private lateinit var fs: FileSystem
|
||||
private lateinit var database: CordaPersistence
|
||||
private lateinit var storage: NodeAttachmentService
|
||||
private val services = rigorousMock<ServicesForResolution>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@ -52,18 +65,40 @@ class NodeAttachmentServiceTest {
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
|
||||
fs = Jimfs.newFileSystem(Configuration.unix())
|
||||
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
|
||||
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
|
||||
database.transaction {
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
storage.servicesForResolution = services
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
dir.list { subdir ->
|
||||
subdir.forEach(Path::deleteRecursively)
|
||||
}
|
||||
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
|
||||
fun `insert and retrieve`() {
|
||||
val (testJar, expectedHash) = makeTestJar()
|
||||
@ -289,7 +324,20 @@ class NodeAttachmentServiceTest {
|
||||
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()) {
|
||||
output.use {
|
||||
val jar = JarOutputStream(it)
|
||||
@ -305,5 +353,48 @@ class NodeAttachmentServiceTest {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.serialization.internal.GeneratedAttachment
|
||||
import net.corda.serialization.internal.amqp.CustomSerializer
|
||||
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.
|
||||
@ -23,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
|
||||
} catch (e: Exception) {
|
||||
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 {
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers)
|
||||
}
|
||||
|
||||
@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>)
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.BelongsToContract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
@BelongsToContract(DummyContract::class)
|
||||
data class DummyState @JvmOverloads constructor (
|
||||
/** Some information that the state represents for test purposes. **/
|
||||
val magicNumber: Int = 0,
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
@ -71,7 +72,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
val sha256 = attachmentId ?: bytes.sha256()
|
||||
if (sha256 !in files.keys) {
|
||||
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)
|
||||
}
|
||||
return sha256
|
||||
|
Loading…
Reference in New Issue
Block a user