mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Merge pull request #2 from corda/feature/CORDA-1947/package_ownership
CORDA-1947 added packageOwnership
This commit is contained in:
commit
0234c1fcf8
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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}.") }
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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.
|
||||||
|
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
|
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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" />
|
<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>
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user