Feature/corda 1947/add package ownership (#4097)

* Upgrade hibernate and fix tests

CORDA-1947 Address code review changes

CORDA-1947 Address code review changes

(cherry picked from commit ab98c03d1a)

* ENT-2506 Changes signers field type

ENT-2506 Clean up some docs

ENT-2506 Fix tests and api

ENT-2506 Fix compilation error

ENT-2506 Fix compilation error

(cherry picked from commit 32f279a243)

* CORDA-1947 added packageOwnership parameter

CORDA-1947 add signers field to DbAttachment. Add check when importing attachments

CORDA-1947 add signers field to DbAttachment. Add check when importing attachments

CORDA-1947 add tests

CORDA-1947 fix comment

CORDA-1947 Fix test

CORDA-1947 fix serialiser

CORDA-1947 fix tests

CORDA-1947 fix tests

CORDA-1947 fix serialiser

CORDA-1947 Address code review changes

CORDA-1947 Address code review changes

CORDA-1947 Revert test fixes

CORDA-1947 address code review comments

CORDA-1947 move verification logic to LedgerTransaction.verify

CORDA-1947 fix test

CORDA-1947 fix tests

CORDA-1947 fix tests

CORDA-1947 address code review comments

CORDA-1947 address code review comments

(cherry picked from commit 86bc0d9606)

CORDA-1947 fix merge
This commit is contained in:
Tudor Malene 2018-10-22 15:00:08 +01:00 committed by GitHub
parent ba7727a4e1
commit 391c6bf66f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 707 additions and 181 deletions

View File

@ -429,7 +429,7 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte
public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash
public void extractFile(String, java.io.OutputStream)
@NotNull
public abstract java.util.List<net.corda.core.identity.Party> getSigners()
public abstract java.util.List<java.security.PublicKey> getSigners()
public abstract int getSize()
@NotNull
public abstract java.io.InputStream open()
@ -537,7 +537,7 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang
@NotNull
public net.corda.core.crypto.SecureHash getId()
@NotNull
public java.util.List<net.corda.core.identity.Party> getSigners()
public java.util.List<java.security.PublicKey> getSigners()
public int getSize()
@Nullable
public final String getUploader()

View File

@ -43,7 +43,7 @@ buildscript {
ext.hamkrest_version = '1.4.2.2'
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
ext.hibernate_version = '5.3.6.Final'
ext.h2_version = '1.4.197' // Update docs if renamed or removed.
ext.postgresql_version = '42.1.4'
ext.rxjava_version = '1.3.8'

View File

@ -42,8 +42,8 @@ class AttachmentTest {
attachment = object : Attachment {
override val id: SecureHash
get() = SecureHash.allOnesHash
override val signers: List<Party>
get() = listOf(ALICE)
override val signers: List<PublicKey>
get() = listOf(ALICE_KEY)
override val size: Int
get() = jarData.size

View File

@ -3,13 +3,13 @@ package net.corda.deterministic.verifier
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.PublicKey
@CordaSerializable
class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List<Party> = ArrayList()) : Attachment {
class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List<PublicKey> = emptyList()) : Attachment {
override fun open(): InputStream = ByteArrayInputStream(id.bytes)
override val size = id.size
}

View File

@ -7,6 +7,7 @@ import net.corda.core.serialization.CordaSerializable
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
import java.util.jar.JarInputStream
/**
@ -51,10 +52,10 @@ interface Attachment : NamedByHash {
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
/**
* The parties that have correctly signed the whole attachment.
* The keys that have correctly signed the whole attachment.
* Can be empty, for example non-contract attachments won't be necessarily be signed.
*/
val signers: List<Party>
val signers: List<PublicKey>
/**
* Attachment size in bytes.

View File

@ -82,5 +82,5 @@ data class SignatureAttachmentConstraint(
val key: PublicKey
) : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean =
key.isFulfilledBy(attachment.signers.map { it.owningKey })
key.isFulfilledBy(attachment.signers.map { it })
}

View File

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

View File

@ -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
@ -169,4 +170,12 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
@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)
}

View File

@ -1,4 +1,5 @@
@file:KeepForDJVM
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
@ -11,6 +12,7 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
import java.util.jar.JarInputStream
const val DEPLOYED_CORDAPP_UPLOADER = "app"
@ -40,8 +42,8 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
override val size: Int get() = attachmentData.size
override fun open(): InputStream = attachmentData.inputStream()
override val signers by lazy {
openAsJAR().use(JarSignatureCollector::collectSigningParties)
override val signers: List<PublicKey> by lazy {
openAsJAR().use(JarSignatureCollector::collectSigners)
}
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id

View File

@ -2,6 +2,7 @@ package net.corda.core.internal
import net.corda.core.identity.Party
import java.security.CodeSigner
import java.security.PublicKey
import java.security.cert.X509Certificate
import java.util.jar.JarEntry
import java.util.jar.JarInputStream
@ -23,22 +24,25 @@ object JarSignatureCollector {
* @param jar The open [JarInputStream] to collect signing parties from.
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.
*/
fun collectSigningParties(jar: JarInputStream): List<Party> {
fun collectSigners(jar: JarInputStream): List<PublicKey> = getSigners(jar).toOrderedPublicKeys()
fun collectSigningParties(jar: JarInputStream): List<Party> = getSigners(jar).toPartiesOrderedByName()
private fun getSigners(jar: JarInputStream): Set<CodeSigner> {
val signerSets = jar.fileSignerSets
if (signerSets.isEmpty()) return emptyList()
if (signerSets.isEmpty()) return emptySet()
val (firstFile, firstSignerSet) = signerSets.first()
for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) {
if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException(
"""
Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile
and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}.
Mismatch between signers ${firstSignerSet.toOrderedPublicKeys()} for file $firstFile
and signers ${otherSignerSet.toOrderedPublicKeys()} for file ${otherFile}.
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
constraints applied to attachment signatures.
""".trimIndent().replace('\n', ' '))
}
return firstSignerSet.toPartiesOrderedByName()
return firstSignerSet
}
private val JarInputStream.fileSignerSets: List<Pair<String, Set<CodeSigner>>> get() =
@ -63,6 +67,10 @@ object JarSignatureCollector {
Party(it.signerCertPath.certificates[0] as X509Certificate)
}.sortedBy { it.name.toString() } // Sorted for determinism.
private fun Set<CodeSigner>.toOrderedPublicKeys(): List<PublicKey> = map {
(it.signerCertPath.certificates[0] as X509Certificate).publicKey
}.sortedBy { it.hash} // Sorted for determinism.
private val JarInputStream.entries get(): Sequence<JarEntry> = generateSequence(nextJarEntry) { nextJarEntry }
}

View File

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

View File

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

View File

@ -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
@ -74,6 +75,9 @@ data class LedgerTransaction @JvmOverloads constructor(
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.map { it.state.data }
private val inputAndOutputStates = inputs.map { it.state } + outputs
private val allStates = inputAndOutputStates + references.map { it.state }
/**
* Returns the typed input StateAndRef at the specified index
* @param index The index into the inputs.
@ -88,10 +92,37 @@ data class LedgerTransaction @JvmOverloads constructor(
*/
@Throws(TransactionVerificationException::class)
fun verify() {
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
validatePackageOwnership(contractAttachmentsByContract)
verifyConstraints()
verifyContracts()
}
/**
* Verify that package ownership 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)
}
}
}
/**
* Verify that all contract constraints are valid for each state before running any contract code
*
@ -123,6 +154,29 @@ data class LedgerTransaction @JvmOverloads constructor(
}
}
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
val result = mutableMapOf<ContractClassName, ContractAttachment>()
for (attachment in attachments) {
if (attachment !is ContractAttachment) continue
for (contract in attachment.allContracts) {
result.compute(contract) { _, previousAttachment ->
when {
previousAttachment == null -> attachment
attachment.id == previousAttachment.id -> previousAttachment
// In case multiple attachments have been added for the same contract, fail because this
// transaction will not be able to be verified because it will break the no-overlap rule
// that we have implemented in our Classloaders
else -> throw TransactionVerificationException.ConflictingAttachmentsRejection(id, contract)
}
}
}
}
return result
}
/**
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.

View File

@ -158,8 +158,8 @@ open class TransactionBuilder @JvmOverloads constructor(
}
}
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<Party>) =
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build())
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
contractClassName in networkParameters.whitelistedContractImplementations.keys

View File

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

View File

@ -0,0 +1,91 @@
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.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")
}
}
}
}
class DummyContractState : ContractState {
override val participants: List<AbstractParty>
get() = emptyList()
}
class DummyContract : Contract {
interface Commands : CommandData
class Create : Commands
override fun verify(tx: LedgerTransaction) {
//do nothing
}
}
class DummyIssue : TypeOnlyCommandData()

View File

@ -1,6 +1,10 @@
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.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
@ -10,29 +14,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.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"
@ -42,15 +31,11 @@ class JarSignatureCollectorTest {
private const val CHARLIE = "Charlie"
private const val CHARLIE_PASS = "charliepass"
private fun generateKey(alias: String, password: String, name: CordaX500Name, keyalg: String = "RSA") =
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name.toString())
@BeforeClass
@JvmStatic
fun beforeClass() {
generateKey(ALICE, ALICE_PASS, ALICE_NAME)
generateKey(BOB, BOB_PASS, BOB_NAME)
generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME, "EC")
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"))
@ -64,7 +49,7 @@ class JarSignatureCollectorTest {
}
}
private val List<Party>.names get() = map { it.name }
private val List<Party>.keys get() = map { it.owningKey }
@After
fun tearDown() {
@ -77,49 +62,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")
signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different.
dir.createJar(FILENAME, "_signable1", "_signable2")
val key = signAsAlice()
assertEquals(listOf(key), dir.getJarSigners(FILENAME))
(dir / "my-dir").createDirectory()
updateJar("my-dir")
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // 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")
signAsAlice()
signAsBob()
dir.createJar(FILENAME, "_signable1", "_signable2")
val key1 = signAsAlice()
val key2 = signAsBob()
assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names)
assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet())
}
@Test
fun `all files must be signed by the same set of signers`() {
createJar("_signable1")
signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
dir.createJar(FILENAME, "_signable1")
val key1 = signAsAlice()
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
updateJar("_signable2")
dir.updateJar(FILENAME, "_signable2")
signAsBob()
assertFailsWith<InvalidJarSignersException>(
"""
@ -128,50 +113,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")
signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
dir.createJar(FILENAME, "volatile")
val key1 = signAsAlice()
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) }
}
// Signing using EC algorithm produced JAR File spec incompatible signature block (META-INF/*.EC) which is anyway accepted by jarsiner, see [JarSignatureCollector]
@Test
fun `one signer with EC sign algorithm`() {
createJar("_signable1", "_signable2")
signJar(CHARLIE, CHARLIE_PASS)
assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // We only reused CHARLIE's distinguished name, so the keys will be different.
(dir / "my-dir").createDirectory()
updateJar("my-dir")
assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // Unsigned directory is irrelevant.
}
//region Helper functions
private fun createJar(vararg contents: String) =
execute(*(arrayOf("jar", "cvf", FILENAME) + contents))
private fun updateJar(vararg contents: String) =
execute(*(arrayOf("jar", "uvf", FILENAME) + contents))
private fun signJar(alias: String, password: String) =
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias)
private fun signAsAlice() = signJar(ALICE, ALICE_PASS)
private fun signAsBob() = signJar(BOB, BOB_PASS)
private fun getJarSigners() =
JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigningParties)
//endregion
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
}

View File

@ -23,6 +23,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.security.PublicKey
class TransactionBuilderTest {
@Rule
@ -40,7 +41,15 @@ class TransactionBuilderTest {
doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
doReturn(attachments).whenever(services).attachments
val attachmentStorage = rigorousMock<AttachmentStorage>()
doReturn(attachmentStorage).whenever(services).attachments
val attachment = rigorousMock<ContractAttachment>()
doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId)
doReturn(contractAttachmentId).whenever(attachment).id
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
doReturn("app").whenever(attachment).uploader
doReturn(emptyList<Party>()).whenever(attachment).signers
}
@Test
@ -103,6 +112,7 @@ class TransactionBuilderTest {
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
doReturn(attachments).whenever(services).attachments
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
@ -112,19 +122,17 @@ class TransactionBuilderTest {
val wtx = builder.toWireTransaction(services)
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
}
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<Party> get() = emptyList()
}
override val signers: List<PublicKey> get() = emptyList()
}, DummyContract.PROGRAM_ID)
private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) {
private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<Party> get() = parties.toList()
}
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
}, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey })
}

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.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()

View File

@ -125,6 +125,13 @@ The current set of network parameters:
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
``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.

View File

@ -1,14 +1,8 @@
package net.corda.nodeapi.internal.persistence
import org.hibernate.stat.*
import javax.management.MXBean
import org.hibernate.stat.Statistics
import org.hibernate.stat.SecondLevelCacheStatistics
import org.hibernate.stat.QueryStatistics
import org.hibernate.stat.NaturalIdCacheStatistics
import org.hibernate.stat.EntityStatistics
import org.hibernate.stat.CollectionStatistics
/**
* Exposes Hibernate [Statistics] contract as JMX resource.
*/
@ -20,6 +14,25 @@ interface StatisticsService : Statistics
* session factory.
*/
class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService {
override fun getNaturalIdStatistics(entityName: String?): NaturalIdStatistics {
return delegate.getNaturalIdStatistics(entityName)
}
override fun getDomainDataRegionStatistics(regionName: String?): CacheRegionStatistics {
return delegate.getDomainDataRegionStatistics(regionName)
}
override fun getQueryRegionStatistics(regionName: String?): CacheRegionStatistics {
return delegate.getQueryRegionStatistics(regionName)
}
override fun getNaturalIdQueryExecutionMaxTimeEntity(): String {
return delegate.getNaturalIdQueryExecutionMaxTimeEntity()
}
override fun getCacheRegionStatistics(regionName: String?): CacheRegionStatistics {
return delegate.getCacheRegionStatistics(regionName)
}
override fun clear() {
delegate.clear()

View File

@ -170,7 +170,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, cacheFactory)
@ -1036,7 +1038,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, cacheFactory, attributeConverters)
}

View File

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

View File

@ -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
@ -30,6 +33,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
@ -45,6 +49,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()
@ -94,7 +102,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
@ -212,11 +226,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
}
@ -290,14 +306,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")
@ -309,6 +330,9 @@ class NodeAttachmentService(
}
}
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
@Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try {

View File

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

View File

@ -505,7 +505,8 @@ class NodeVaultService(
// Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases.
// So the floor may be necessary anyway.
query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize)
query.maxResults = paging.pageSize + 1 // detection too many results
val pageSize = paging.pageSize + 1
query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow
// execution
val results = query.resultList

View File

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

View File

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

View File

@ -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
@ -17,33 +23,40 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.testing.internal.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.internal.configureDatabase
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)
}
}
}

View File

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

View File

@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
@ -147,7 +148,11 @@ data class TestTransactionDSLInterpreter private constructor(
override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl()
override fun _attachment(contractClassName: ContractClassName) {
(services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage))
}
override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>){
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers))
}
}

View File

@ -1,9 +1,18 @@
package net.corda.testing.dsl
import net.corda.core.DoNotImplement
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import java.security.PublicKey
@ -80,6 +89,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
* @param contractClassName The contract class to attach
*/
fun _attachment(contractClassName: ContractClassName)
/**
* Attaches an attachment containing the named contract to the transaction
* @param contractClassName The contract class to attach
* @param attachmentId The attachment
*/
fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>)
}
/**
@ -186,5 +202,8 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
*/
fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName)
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>) = _attachment(contractClassName, attachmentId, signers)
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList())
fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) }
}

View File

@ -3,6 +3,7 @@ package net.corda.testing.internal
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.services.AttachmentId
@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths
import java.security.PublicKey
import java.util.*
class MockCordappProvider(
@ -21,7 +23,7 @@ class MockCordappProvider(
private val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
val cordapp = CordappImpl(
contractClassNames = listOf(contractClassName),
initiatedFlows = emptyList(),
@ -36,23 +38,23 @@ class MockCordappProvider(
info = CordappImpl.Info.UNKNOWN,
allFlows = emptyList(),
jarHash = SecureHash.allOnesHash)
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments)))
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers)))
}
return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!!
}
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? {
return cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName)
}
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second
?: super.getContractAttachmentID(contractClassName)
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
val existingAttachment = attachments.files.filter {
Arrays.equals(it.value.second, data)
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List<PublicKey>): AttachmentId {
val existingAttachment = attachments.files.filter { (attachmentId, content) ->
contractHash == attachmentId
}
return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first()
} else {
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream())
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers)
}
}
}

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
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
@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.nodeapi.internal.withContractsInJar
import java.io.InputStream
import java.security.PublicKey
import java.util.*
import java.util.jar.JarInputStream
@ -53,22 +55,23 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
}
}
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames)
@JvmOverloads
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers)
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader)
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List<PublicKey>) : AbstractAttachment(dataLoader)
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null): AttachmentId {
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
require(jar !is JarInputStream)
val bytes = jar.readFully()
val sha256 = bytes.sha256()
val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader)
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers)
_files[sha256] = Pair(attachment, bytes)
}
return sha256