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

* 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 32f279a24372e31b07cfddac53edf805175fc971)

* 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 86bc0d9606922d48a30d395af2a21d6ce7dfc03b)

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

View File

@ -43,7 +43,7 @@ buildscript {
ext.hamkrest_version = '1.4.2.2' ext.hamkrest_version = '1.4.2.2'
ext.jopt_simple_version = '5.0.2' ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14' 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.h2_version = '1.4.197' // Update docs if renamed or removed.
ext.postgresql_version = '42.1.4' ext.postgresql_version = '42.1.4'
ext.rxjava_version = '1.3.8' ext.rxjava_version = '1.3.8'

View File

@ -42,8 +42,8 @@ class AttachmentTest {
attachment = object : Attachment { attachment = object : Attachment {
override val id: SecureHash override val id: SecureHash
get() = SecureHash.allOnesHash get() = SecureHash.allOnesHash
override val signers: List<Party> override val signers: List<PublicKey>
get() = listOf(ALICE) get() = listOf(ALICE_KEY)
override val size: Int override val size: Int
get() = jarData.size 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.Attachment
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.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey
@CordaSerializable @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 fun open(): InputStream = ByteArrayInputStream(id.bytes)
override val size = id.size override val size = id.size
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.serialization.CordaSerializable
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.PublicKey
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
/** /**
@ -51,10 +52,10 @@ interface Attachment : NamedByHash {
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) } 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. * 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. * Attachment size in bytes.

View File

@ -82,5 +82,5 @@ data class SignatureAttachmentConstraint(
val key: PublicKey val key: PublicKey
) : AttachmentConstraint { ) : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean = 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.KeepForDJVM
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
/** /**
* Wrap an attachment in this if it is to be used as an executable contract attachment * Wrap an attachment in this if it is to be used as an executable contract attachment
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
*/ */
@KeepForDJVM @KeepForDJVM
@CordaSerializable @CordaSerializable
class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment { class ContractAttachment @JvmOverloads constructor(
val attachment: Attachment,
val contract: ContractClassName,
val additionalContracts: Set<ContractClassName> = emptySet(),
val uploader: String? = null,
override val signers: List<PublicKey> = emptyList()) : Attachment by attachment {
val allContracts: Set<ContractClassName> get() = additionalContracts + contract val allContracts: Set<ContractClassName> get() = additionalContracts + contract

View File

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

View File

@ -1,4 +1,5 @@
@file:KeepForDJVM @file:KeepForDJVM
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
@ -11,6 +12,7 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.PublicKey
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
const val DEPLOYED_CORDAPP_UPLOADER = "app" const val DEPLOYED_CORDAPP_UPLOADER = "app"
@ -40,8 +42,8 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
override val size: Int get() = attachmentData.size override val size: Int get() = attachmentData.size
override fun open(): InputStream = attachmentData.inputStream() override fun open(): InputStream = attachmentData.inputStream()
override val signers by lazy { override val signers: List<PublicKey> by lazy {
openAsJAR().use(JarSignatureCollector::collectSigningParties) openAsJAR().use(JarSignatureCollector::collectSigners)
} }
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id 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 net.corda.core.identity.Party
import java.security.CodeSigner import java.security.CodeSigner
import java.security.PublicKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.jar.JarEntry import java.util.jar.JarEntry
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
@ -23,22 +24,25 @@ object JarSignatureCollector {
* @param jar The open [JarInputStream] to collect signing parties from. * @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. * @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 val signerSets = jar.fileSignerSets
if (signerSets.isEmpty()) return emptyList() if (signerSets.isEmpty()) return emptySet()
val (firstFile, firstSignerSet) = signerSets.first() val (firstFile, firstSignerSet) = signerSets.first()
for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) { for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) {
if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException( if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException(
""" """
Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile Mismatch between signers ${firstSignerSet.toOrderedPublicKeys()} for file $firstFile
and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}. and signers ${otherSignerSet.toOrderedPublicKeys()} for file ${otherFile}.
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', ' '))
} }
return firstSignerSet
return firstSignerSet.toPartiesOrderedByName()
} }
private val JarInputStream.fileSignerSets: List<Pair<String, Set<CodeSigner>>> get() = private val JarInputStream.fileSignerSets: List<Pair<String, Set<CodeSigner>>> get() =
@ -63,6 +67,10 @@ object JarSignatureCollector {
Party(it.signerCertPath.certificates[0] as X509Certificate) Party(it.signerCertPath.certificates[0] as X509Certificate)
}.sortedBy { it.name.toString() } // Sorted for determinism. }.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 } 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.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,6 +108,25 @@ 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
@ -94,9 +138,17 @@ data class NetworkParameters(
} }
eventHorizon=$eventHorizon eventHorizon=$eventHorizon
modifiedTime=$modifiedTime modifiedTime=$modifiedTime
epoch=$epoch epoch=$epoch,
}""" packageOwnership= {
${packageOwnership.keys.joinToString()}}
} }
}"""
}
/**
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
*/
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull()
} }
/** /**
@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
* version. * version.
*/ */
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
/**
* A wrapper for a legal java package. Used by the network parameters to store package ownership.
*/
@CordaSerializable
data class JavaPackageName(val name: String) {
init {
require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" }
}
/**
* Returns true if the [fullClassName] is in a subpackage of the current package.
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
*
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
*/
fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true)
}
// Check if a string is a legal Java package name.
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
}
// Make sure that packages don't overlap so that ownership is clear.
private fun noOverlap(packages: Collection<JavaPackageName>) = packages.all { currentPackage ->
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") }
}

View File

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

View File

@ -3,6 +3,7 @@ package net.corda.core.transactions
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.castIfPossible import net.corda.core.internal.castIfPossible
@ -74,6 +75,9 @@ data class LedgerTransaction @JvmOverloads constructor(
val inputStates: List<ContractState> get() = inputs.map { it.state.data } val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.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 * Returns the typed input StateAndRef at the specified index
* @param index The index into the inputs. * @param index The index into the inputs.
@ -88,10 +92,37 @@ data class LedgerTransaction @JvmOverloads constructor(
*/ */
@Throws(TransactionVerificationException::class) @Throws(TransactionVerificationException::class)
fun verify() { fun verify() {
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
validatePackageOwnership(contractAttachmentsByContract)
verifyConstraints() verifyConstraints()
verifyContracts() 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 * 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. * 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. * 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>) = private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build()) SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
contractClassName in networkParameters.whitelistedContractImplementations.keys 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 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.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
@ -10,29 +14,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.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"
@ -42,15 +31,11 @@ class JarSignatureCollectorTest {
private const val CHARLIE = "Charlie" private const val CHARLIE = "Charlie"
private const val CHARLIE_PASS = "charliepass" 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 @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())
generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME, "EC")
(dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable1").writeLines(listOf("signable1"))
(dir / "_signable2").writeLines(listOf("signable2")) (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 @After
fun tearDown() { fun tearDown() {
@ -77,49 +62,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")
signAsAlice() val key = signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different. assertEquals(listOf(key), dir.getJarSigners(FILENAME))
(dir / "my-dir").createDirectory() (dir / "my-dir").createDirectory()
updateJar("my-dir") dir.updateJar(FILENAME, "my-dir")
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // 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")
signAsAlice() val key1 = signAsAlice()
signAsBob() val key2 = signAsBob()
assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names) 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")
signAsAlice() val key1 = signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names) assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
updateJar("_signable2") dir.updateJar(FILENAME, "_signable2")
signAsBob() signAsBob()
assertFailsWith<InvalidJarSignersException>( assertFailsWith<InvalidJarSignersException>(
""" """
@ -128,50 +113,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")
signAsAlice() val key1 = signAsAlice()
assertEquals(listOf(ALICE_NAME), getJarSigners().names) 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) }
} }
// Signing using EC algorithm produced JAR File spec incompatible signature block (META-INF/*.EC) which is anyway accepted by jarsiner, see [JarSignatureCollector] private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
@Test private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
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
} }

View File

@ -23,6 +23,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.security.PublicKey
class TransactionBuilderTest { class TransactionBuilderTest {
@Rule @Rule
@ -40,7 +41,15 @@ class TransactionBuilderTest {
doReturn(cordappProvider).whenever(services).cordappProvider doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters 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 @Test
@ -103,6 +112,7 @@ class TransactionBuilderTest {
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment)) assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment)) assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
doReturn(attachments).whenever(services).attachments
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId) doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
@ -112,19 +122,17 @@ class TransactionBuilderTest {
val wtx = builder.toWireTransaction(services) val wtx = builder.toWireTransaction(services)
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
} }
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
override val id: SecureHash get() = throw UnsupportedOperationException() 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 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.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.createWireTransaction import net.corda.testing.internal.createWireTransaction
@ -129,7 +130,8 @@ class TransactionTests {
id, id,
null, null,
timeWindow, timeWindow,
privacySalt privacySalt,
testNetworkParameters()
) )
transaction.verify() transaction.verify()

View File

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

View File

@ -1,14 +1,8 @@
package net.corda.nodeapi.internal.persistence package net.corda.nodeapi.internal.persistence
import org.hibernate.stat.*
import javax.management.MXBean 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. * Exposes Hibernate [Statistics] contract as JMX resource.
*/ */
@ -20,6 +14,25 @@ interface StatisticsService : Statistics
* session factory. * session factory.
*/ */
class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService { 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() { override fun clear() {
delegate.clear() delegate.clear()

View File

@ -170,7 +170,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, cacheFactory) 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 // 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, cacheFactory, attributeConverters) return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters)
} }

View File

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

View File

@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
import com.google.common.hash.Hashing import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream import com.google.common.io.CountingInputStream
import net.corda.core.ClientRelevantError
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.AttachmentSort
@ -30,6 +33,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
@ -45,6 +49,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()
@ -94,7 +102,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
@ -212,11 +226,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
} }
@ -290,14 +306,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")
@ -309,6 +330,9 @@ class NodeAttachmentService(
} }
} }
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId { override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try { return try {

View File

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

View File

@ -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. // 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. // So the floor may be necessary anyway.
query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize) 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 // execution
val results = query.resultList val results = query.resultList

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction 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 _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl()
override fun _attachment(contractClassName: ContractClassName) { 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 package net.corda.testing.dsl
import net.corda.core.DoNotImplement 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.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import java.security.PublicKey import java.security.PublicKey
@ -80,6 +89,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
* @param contractClassName The contract class to attach * @param contractClassName The contract class to attach
*/ */
fun _attachment(contractClassName: ContractClassName) 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) = _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) } 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.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash 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.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.services.AttachmentId 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.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths import java.nio.file.Paths
import java.security.PublicKey
import java.util.* import java.util.*
class MockCordappProvider( class MockCordappProvider(
@ -21,7 +23,7 @@ class MockCordappProvider(
private val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>() 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( val cordapp = CordappImpl(
contractClassNames = listOf(contractClassName), contractClassNames = listOf(contractClassName),
initiatedFlows = emptyList(), initiatedFlows = emptyList(),
@ -36,23 +38,23 @@ class MockCordappProvider(
info = CordappImpl.Info.UNKNOWN, info = CordappImpl.Info.UNKNOWN,
allFlows = emptyList(), allFlows = emptyList(),
jarHash = SecureHash.allOnesHash) jarHash = SecureHash.allOnesHash)
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments))) 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? { override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second
return cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) ?: super.getContractAttachmentID(contractClassName)
}
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List<PublicKey>): AttachmentId {
val existingAttachment = attachments.files.filter { val existingAttachment = attachments.files.filter { (attachmentId, content) ->
Arrays.equals(it.value.second, data) contractHash == attachmentId
} }
return if (!existingAttachment.isEmpty()) { return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first() existingAttachment.keys.first()
} else { } 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.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
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
@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.nodeapi.internal.withContractsInJar import net.corda.nodeapi.internal.withContractsInJar
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.jar.JarInputStream 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) } 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. // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
require(jar !is JarInputStream) require(jar !is JarInputStream)
val bytes = jar.readFully() val bytes = jar.readFully()
val sha256 = bytes.sha256() val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) { if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256) 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