mirror of
https://github.com/corda/corda.git
synced 2025-02-01 16:58:27 +00:00
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:
parent
ba7727a4e1
commit
391c6bf66f
@ -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()
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ package net.corda.core.contracts
|
|||||||
|
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap an attachment in this if it is to be used as an executable contract attachment
|
* Wrap an attachment in this if it is to be used as an executable contract attachment
|
||||||
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
*/
|
*/
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment {
|
class ContractAttachment @JvmOverloads constructor(
|
||||||
|
val attachment: Attachment,
|
||||||
|
val contract: ContractClassName,
|
||||||
|
val additionalContracts: Set<ContractClassName> = emptySet(),
|
||||||
|
val uploader: String? = null,
|
||||||
|
override val signers: List<PublicKey> = emptyList()) : Attachment by attachment {
|
||||||
|
|
||||||
val allContracts: Set<ContractClassName> get() = additionalContracts + contract
|
val allContracts: Set<ContractClassName> get() = additionalContracts + contract
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}.") }
|
||||||
|
}
|
||||||
|
@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction(
|
|||||||
private fun verifyConstraints() {
|
private fun verifyConstraints() {
|
||||||
val attachmentForConstraintVerification = AttachmentWithContext(
|
val attachmentForConstraintVerification = AttachmentWithContext(
|
||||||
legacyContractAttachment as? ContractAttachment
|
legacyContractAttachment as? ContractAttachment
|
||||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName),
|
?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers),
|
||||||
upgradedContract.legacyContract,
|
upgradedContract.legacyContract,
|
||||||
networkParameters.whitelistedContractImplementations
|
networkParameters.whitelistedContractImplementations
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.core.transactions
|
|||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.AttachmentWithContext
|
import net.corda.core.internal.AttachmentWithContext
|
||||||
import net.corda.core.internal.castIfPossible
|
import net.corda.core.internal.castIfPossible
|
||||||
@ -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.
|
||||||
|
@ -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
|
||||||
|
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal file
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package net.corda.core
|
||||||
|
|
||||||
|
import net.corda.core.internal.JarSignatureCollector
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.util.jar.JarInputStream
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
object JarSignatureTestUtils {
|
||||||
|
val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
||||||
|
|
||||||
|
fun Path.executeProcess(vararg command: String) {
|
||||||
|
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
||||||
|
assertEquals(0, ProcessBuilder()
|
||||||
|
.inheritIO()
|
||||||
|
.redirectOutput(shredder)
|
||||||
|
.redirectError(shredder)
|
||||||
|
.directory(this.toFile())
|
||||||
|
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
||||||
|
.start()
|
||||||
|
.waitFor())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Path.generateKey(alias: String, password: String, name: String) =
|
||||||
|
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name)
|
||||||
|
|
||||||
|
fun Path.createJar(fileName: String, vararg contents: String) =
|
||||||
|
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
||||||
|
|
||||||
|
fun Path.updateJar(fileName: String, vararg contents: String) =
|
||||||
|
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
||||||
|
|
||||||
|
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
|
||||||
|
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
|
||||||
|
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
|
||||||
|
return ks.getCertificate(alias).publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Path.getJarSigners(fileName: String) =
|
||||||
|
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
|
||||||
|
}
|
@ -0,0 +1,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()
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.internal.createWireTransaction
|
import net.corda.testing.internal.createWireTransaction
|
||||||
@ -129,7 +130,8 @@ class TransactionTests {
|
|||||||
id,
|
id,
|
||||||
null,
|
null,
|
||||||
timeWindow,
|
timeWindow,
|
||||||
privacySalt
|
privacySalt,
|
||||||
|
testNetworkParameters()
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction.verify()
|
transaction.verify()
|
||||||
|
@ -125,6 +125,13 @@ The current set of network parameters:
|
|||||||
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
|
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
|
||||||
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
|
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
|
||||||
|
|
||||||
|
:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners.
|
||||||
|
Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
|
||||||
|
This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same.
|
||||||
|
Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported.
|
||||||
|
The transaction verification logic will throw an exception when this happens.
|
||||||
|
Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`.
|
||||||
|
|
||||||
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
|
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
|
||||||
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.
|
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
|
|||||||
output.writeString(obj.contract)
|
output.writeString(obj.contract)
|
||||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||||
output.writeString(obj.uploader)
|
output.writeString(obj.uploader)
|
||||||
|
kryo.writeClassAndObject(output, obj.signers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
||||||
if (kryo.serializationContext() != null) {
|
if (kryo.serializationContext() != null) {
|
||||||
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
||||||
val contract = input.readString()
|
val contract = input.readString()
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||||
val uploader = input.readString()
|
val uploader = input.readString()
|
||||||
|
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||||
val context = kryo.serializationContext()!!
|
val context = kryo.serializationContext()!!
|
||||||
val attachmentStorage = context.serviceHub.attachments
|
val attachmentStorage = context.serviceHub.attachments
|
||||||
|
|
||||||
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
|
|||||||
override val id = attachmentHash
|
override val id = attachmentHash
|
||||||
}
|
}
|
||||||
|
|
||||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
|
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
|
||||||
} else {
|
} else {
|
||||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||||
val contract = input.readString()
|
val contract = input.readString()
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||||
val uploader = input.readString()
|
val uploader = input.readString()
|
||||||
return ContractAttachment(attachment, contract, additionalContracts, uploader)
|
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||||
|
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
|
|||||||
import com.google.common.hash.Hashing
|
import com.google.common.hash.Hashing
|
||||||
import com.google.common.hash.HashingInputStream
|
import com.google.common.hash.HashingInputStream
|
||||||
import com.google.common.io.CountingInputStream
|
import com.google.common.io.CountingInputStream
|
||||||
|
import net.corda.core.ClientRelevantError
|
||||||
import net.corda.core.CordaRuntimeException
|
import net.corda.core.CordaRuntimeException
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.ContractAttachment
|
import net.corda.core.contracts.ContractAttachment
|
||||||
import net.corda.core.contracts.ContractClassName
|
import net.corda.core.contracts.ContractClassName
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.services.AttachmentId
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||||
import net.corda.core.node.services.vault.AttachmentSort
|
import net.corda.core.node.services.vault.AttachmentSort
|
||||||
@ -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 {
|
||||||
|
@ -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()) }
|
||||||
|
}
|
@ -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
|
||||||
|
@ -14,4 +14,17 @@
|
|||||||
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
|
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="R3.Corda" id="add_signers">
|
||||||
|
<createTable tableName="node_attachments_signers">
|
||||||
|
<column name="att_id" type="NVARCHAR(255)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="signer" type="NVARCHAR(1024)"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addForeignKeyConstraint baseColumnNames="att_id" baseTableName="node_attachments_signers"
|
||||||
|
constraintName="FK__signers__attachments"
|
||||||
|
referencedColumnNames="att_id" referencedTableName="node_attachments"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
@ -2,6 +2,8 @@ package net.corda.node.internal
|
|||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
|
import net.corda.core.node.JavaPackageName
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig
|
|||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.core.node.NotaryInfo
|
import net.corda.core.node.NotaryInfo
|
||||||
|
import net.corda.core.utilities.days
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
@ -65,7 +68,8 @@ class NetworkParametersTest {
|
|||||||
fun `choosing notary not specified in network parameters will fail`() {
|
fun `choosing notary not specified in network parameters will fail`() {
|
||||||
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
|
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
|
||||||
val notary = NotaryConfig(false)
|
val notary = NotaryConfig(false)
|
||||||
doReturn(notary).whenever(it).notary}))
|
doReturn(notary).whenever(it).notary
|
||||||
|
}))
|
||||||
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
||||||
val alice = mockNet.createPartyNode(ALICE_NAME)
|
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||||
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
||||||
@ -87,6 +91,62 @@ class NetworkParametersTest {
|
|||||||
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
|
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `package ownership checks are correct`() {
|
||||||
|
val key1 = generateKeyPair().public
|
||||||
|
val key2 = generateKeyPair().public
|
||||||
|
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||||
|
NetworkParameters(1,
|
||||||
|
emptyList(),
|
||||||
|
2001,
|
||||||
|
2000,
|
||||||
|
Instant.now(),
|
||||||
|
1,
|
||||||
|
emptyMap(),
|
||||||
|
Int.MAX_VALUE.days,
|
||||||
|
mapOf(
|
||||||
|
JavaPackageName("com.!example.stuff") to key2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.withMessageContaining("Attempting to whitelist illegal java package")
|
||||||
|
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||||
|
NetworkParameters(1,
|
||||||
|
emptyList(),
|
||||||
|
2001,
|
||||||
|
2000,
|
||||||
|
Instant.now(),
|
||||||
|
1,
|
||||||
|
emptyMap(),
|
||||||
|
Int.MAX_VALUE.days,
|
||||||
|
mapOf(
|
||||||
|
JavaPackageName("com.example") to key1,
|
||||||
|
JavaPackageName("com.example.stuff") to key2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.withMessage("multiple packages added to the packageOwnership overlap.")
|
||||||
|
|
||||||
|
NetworkParameters(1,
|
||||||
|
emptyList(),
|
||||||
|
2001,
|
||||||
|
2000,
|
||||||
|
Instant.now(),
|
||||||
|
1,
|
||||||
|
emptyMap(),
|
||||||
|
Int.MAX_VALUE.days,
|
||||||
|
mapOf(
|
||||||
|
JavaPackageName("com.example") to key1,
|
||||||
|
JavaPackageName("com.examplestuff") to key2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
|
||||||
|
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
|
||||||
|
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
|
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
|
||||||
NetworkParametersCopier(params).install(dir)
|
NetworkParametersCopier(params).install(dir)
|
||||||
|
@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import com.google.common.jimfs.Configuration
|
import com.google.common.jimfs.Configuration
|
||||||
import com.google.common.jimfs.Jimfs
|
import com.google.common.jimfs.Jimfs
|
||||||
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import net.corda.core.JarSignatureTestUtils.createJar
|
||||||
|
import net.corda.core.JarSignatureTestUtils.generateKey
|
||||||
|
import net.corda.core.JarSignatureTestUtils.signJar
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||||
import net.corda.core.node.services.vault.AttachmentSort
|
import net.corda.core.node.services.vault.AttachmentSort
|
||||||
import net.corda.core.node.services.vault.Builder
|
import net.corda.core.node.services.vault.Builder
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user