mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
[CORDA-2517] Whitelist attachments signed by keys that already sign existing trusted attachments (#5068)
This allows a different signed version of the same CorDapp to be automatically trusted. This reverts "[CORDA-2575] Allow users to whitelist attachments by public key config (#5035)"
This commit is contained in:
parent
c533792f3f
commit
b4e96778bf
@ -1,14 +0,0 @@
|
|||||||
package net.corda.core.internal
|
|
||||||
|
|
||||||
import net.corda.core.DeleteForDJVM
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.node.ServicesForResolution
|
|
||||||
|
|
||||||
@DeleteForDJVM
|
|
||||||
interface ServicesForResolutionInternal : ServicesForResolution {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If an attachment is signed with a public key with one of these hashes, it will automatically be trusted.
|
|
||||||
*/
|
|
||||||
val whitelistedKeysForAttachments: Collection<SecureHash>
|
|
||||||
}
|
|
@ -7,6 +7,9 @@ import net.corda.core.crypto.componentHash
|
|||||||
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.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
|
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||||
|
import net.corda.core.node.services.vault.Builder
|
||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.*
|
||||||
import net.corda.core.transactions.*
|
import net.corda.core.transactions.*
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
@ -180,3 +183,49 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
|
|||||||
// lets us predict what is the reasonable time window for changing parameters on most of the nodes.
|
// lets us predict what is the reasonable time window for changing parameters on most of the nodes.
|
||||||
// For now we don't check whether the attached network parameters match the current ones.
|
// For now we don't check whether the attached network parameters match the current ones.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class AttachmentAttributeKey(val signers: List<PublicKey>, val contractClasses: List<ContractClassName>?)
|
||||||
|
|
||||||
|
// A cache for caching whether a particular attachment ID is trusted
|
||||||
|
private val attachmentTrustedCache: MutableMap<AttachmentAttributeKey, Boolean> = createSimpleCache<AttachmentAttributeKey, Boolean>(100).toSynchronised()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes whether an attachment should be trusted. This logic is required in order to verify transactions, as transaction
|
||||||
|
* verification should only be carried out using trusted attachments.
|
||||||
|
*
|
||||||
|
* Attachments are trusted if one of the following is true:
|
||||||
|
* - They are uploaded by a trusted uploader
|
||||||
|
* - There is another attachment in the attachment store signed by the same keys (and with the same contract classes if the attachment is a
|
||||||
|
* contract attachment) that is trusted
|
||||||
|
*/
|
||||||
|
fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Boolean {
|
||||||
|
val trustedByUploader = when (attachment) {
|
||||||
|
is ContractAttachment -> isUploaderTrusted(attachment.uploader)
|
||||||
|
is AbstractAttachment -> isUploaderTrusted(attachment.uploader)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trustedByUploader) return true
|
||||||
|
|
||||||
|
return if (service != null && attachment.signerKeys.isNotEmpty()) {
|
||||||
|
val signers = attachment.signerKeys
|
||||||
|
val contractClasses = if (attachment is ContractAttachment) {
|
||||||
|
attachment.allContracts.toList()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val key = AttachmentAttributeKey(signers, contractClasses)
|
||||||
|
|
||||||
|
attachmentTrustedCache.computeIfAbsent(key) {
|
||||||
|
val contractClassCondition = it.contractClasses?.let { classes -> Builder.equal(classes) }
|
||||||
|
val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
|
||||||
|
contractClassNamesCondition = contractClassCondition,
|
||||||
|
signersCondition = Builder.equal(signers),
|
||||||
|
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
|
||||||
|
)
|
||||||
|
service.queryAttachments(queryCriteria).isNotEmpty()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@ package net.corda.core.serialization.internal
|
|||||||
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.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
|
|
||||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
|
||||||
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.*
|
import net.corda.core.internal.*
|
||||||
@ -14,7 +14,6 @@ import net.corda.core.serialization.*
|
|||||||
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.toSHA256Bytes
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -32,13 +31,11 @@ import java.util.*
|
|||||||
* @property sampleTxId The transaction ID that triggered the creation of this classloader. Because classloaders are cached
|
* @property sampleTxId The transaction ID that triggered the creation of this classloader. Because classloaders are cached
|
||||||
* this tx may be stale, that is, classloading might be triggered by the verification of some other transaction
|
* this tx may be stale, that is, classloading might be triggered by the verification of some other transaction
|
||||||
* if not all code is invoked every time, however we want a txid for errors in case of attachment bogusness.
|
* if not all code is invoked every time, however we want a txid for errors in case of attachment bogusness.
|
||||||
* @property whitelistedPublicKeys A collection of public key hashes. An attachment signed by a public key with one of these hashes
|
|
||||||
* will automatically be trusted.
|
|
||||||
*/
|
*/
|
||||||
class AttachmentsClassLoader(attachments: List<Attachment>,
|
class AttachmentsClassLoader(attachments: List<Attachment>,
|
||||||
val params: NetworkParameters,
|
val params: NetworkParameters,
|
||||||
private val sampleTxId: SecureHash,
|
private val sampleTxId: SecureHash,
|
||||||
private val whitelistedPublicKeys: Collection<SecureHash>,
|
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||||
parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
|
parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
|
||||||
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
|
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
|
||||||
|
|
||||||
@ -120,13 +117,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
// Until we have a sandbox to run untrusted code we need to make sure that any loaded class file was whitelisted by the node administrator.
|
// Until we have a sandbox to run untrusted code we need to make sure that any loaded class file was whitelisted by the node administrator.
|
||||||
val untrusted = attachments
|
val untrusted = attachments
|
||||||
.filter(::containsClasses)
|
.filter(::containsClasses)
|
||||||
.filterNot { attachment ->
|
.filterNot(isAttachmentTrusted)
|
||||||
when (attachment) {
|
|
||||||
is ContractAttachment -> isUploaderTrusted(attachment.uploader) || attachmentSignedByTrustedKey(attachment)
|
|
||||||
is AbstractAttachment -> isUploaderTrusted(attachment.uploader) || attachmentSignedByTrustedKey(attachment)
|
|
||||||
else -> false // This should not happen on normal code paths.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map(Attachment::id)
|
.map(Attachment::id)
|
||||||
|
|
||||||
if (untrusted.isNotEmpty()) {
|
if (untrusted.isNotEmpty()) {
|
||||||
@ -140,10 +131,6 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
checkAttachments(attachments)
|
checkAttachments(attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachmentSignedByTrustedKey(attachment: Attachment): Boolean {
|
|
||||||
return attachment.signerKeys.map { it.hash }.any { whitelistedPublicKeys.contains(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar ->
|
private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar ->
|
||||||
jar.nextEntry != null
|
jar.nextEntry != null
|
||||||
}
|
}
|
||||||
@ -322,14 +309,14 @@ object AttachmentsClassLoaderBuilder {
|
|||||||
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>,
|
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>,
|
||||||
params: NetworkParameters,
|
params: NetworkParameters,
|
||||||
txId: SecureHash,
|
txId: SecureHash,
|
||||||
whitelistedPublicKeys: Collection<SecureHash>,
|
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||||
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
|
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
|
||||||
block: (ClassLoader) -> T): T {
|
block: (ClassLoader) -> T): T {
|
||||||
val attachmentIds = attachments.map { it.id }.toSet()
|
val attachmentIds = attachments.map { it.id }.toSet()
|
||||||
|
|
||||||
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
|
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
|
||||||
// Create classloader and load serializers, whitelisted classes
|
// Create classloader and load serializers, whitelisted classes
|
||||||
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, whitelistedPublicKeys, parent)
|
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent)
|
||||||
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
||||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||||
.flatMap { it.whitelist }
|
.flatMap { it.whitelist }
|
||||||
|
@ -9,12 +9,15 @@ import net.corda.core.crypto.componentHash
|
|||||||
import net.corda.core.crypto.computeNonce
|
import net.corda.core.crypto.computeNonce
|
||||||
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.ServicesForResolutionInternal
|
|
||||||
import net.corda.core.internal.combinedHash
|
import net.corda.core.internal.combinedHash
|
||||||
|
import net.corda.core.internal.isAttachmentTrusted
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.SerializedBytes
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
|
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
|
||||||
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract
|
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract
|
||||||
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.retrieveAppClassLoader
|
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.retrieveAppClassLoader
|
||||||
@ -145,13 +148,12 @@ data class ContractUpgradeWireTransaction(
|
|||||||
?: throw MissingContractAttachments(emptyList())
|
?: throw MissingContractAttachments(emptyList())
|
||||||
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
||||||
?: throw MissingContractAttachments(emptyList())
|
?: throw MissingContractAttachments(emptyList())
|
||||||
val whitelistedPublicKeys = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
|
|
||||||
|
|
||||||
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
|
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
|
||||||
listOf(legacyAttachment, upgradedAttachment),
|
listOf(legacyAttachment, upgradedAttachment),
|
||||||
params,
|
params,
|
||||||
id,
|
id,
|
||||||
whitelistedPublicKeys) { transactionClassLoader ->
|
{ isAttachmentTrusted(it, services.attachments) }) { transactionClassLoader ->
|
||||||
val resolvedInput = binaryInput.deserialize()
|
val resolvedInput = binaryInput.deserialize()
|
||||||
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
|
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
|
||||||
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
||||||
|
@ -14,7 +14,6 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import java.lang.UnsupportedOperationException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
|
||||||
@ -75,7 +74,7 @@ private constructor(
|
|||||||
private var componentGroups: List<ComponentGroup>? = null
|
private var componentGroups: List<ComponentGroup>? = null
|
||||||
private var serializedInputs: List<SerializedStateAndRef>? = null
|
private var serializedInputs: List<SerializedStateAndRef>? = null
|
||||||
private var serializedReferences: List<SerializedStateAndRef>? = null
|
private var serializedReferences: List<SerializedStateAndRef>? = null
|
||||||
private var whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
|
private var isAttachmentTrusted: (Attachment) -> Boolean = { isAttachmentTrusted(it, null) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||||
@ -100,13 +99,13 @@ private constructor(
|
|||||||
componentGroups: List<ComponentGroup>? = null,
|
componentGroups: List<ComponentGroup>? = null,
|
||||||
serializedInputs: List<SerializedStateAndRef>? = null,
|
serializedInputs: List<SerializedStateAndRef>? = null,
|
||||||
serializedReferences: List<SerializedStateAndRef>? = null,
|
serializedReferences: List<SerializedStateAndRef>? = null,
|
||||||
whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
|
isAttachmentTrusted: (Attachment) -> Boolean
|
||||||
): LedgerTransaction {
|
): LedgerTransaction {
|
||||||
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply {
|
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply {
|
||||||
this.componentGroups = componentGroups
|
this.componentGroups = componentGroups
|
||||||
this.serializedInputs = serializedInputs
|
this.serializedInputs = serializedInputs
|
||||||
this.serializedReferences = serializedReferences
|
this.serializedReferences = serializedReferences
|
||||||
this.whitelistedKeysForAttachments = whitelistedKeysForAttachments
|
this.isAttachmentTrusted = isAttachmentTrusted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +147,7 @@ private constructor(
|
|||||||
this.attachments + extraAttachments,
|
this.attachments + extraAttachments,
|
||||||
getParamsWithGoo(),
|
getParamsWithGoo(),
|
||||||
id,
|
id,
|
||||||
whitelistedPublicKeys = whitelistedKeysForAttachments) { transactionClassLoader ->
|
isAttachmentTrusted = isAttachmentTrusted) { transactionClassLoader ->
|
||||||
// Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader].
|
// Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader].
|
||||||
// Only the copy will be used for verification, and the outer shell will be discarded.
|
// Only the copy will be used for verification, and the outer shell will be discarded.
|
||||||
// This artifice is required to preserve backwards compatibility.
|
// This artifice is required to preserve backwards compatibility.
|
||||||
|
@ -99,7 +99,6 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
|
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
|
||||||
val whitelistedKeysForAttachments = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
|
|
||||||
return toLedgerTransactionInternal(
|
return toLedgerTransactionInternal(
|
||||||
resolveIdentity = { services.identityService.partyFromKey(it) },
|
resolveIdentity = { services.identityService.partyFromKey(it) },
|
||||||
resolveAttachment = { services.attachments.openAttachment(it) },
|
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||||
@ -109,7 +108,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
services.networkParametersService.lookup(hashToResolve)
|
services.networkParametersService.lookup(hashToResolve)
|
||||||
},
|
},
|
||||||
resolveContractAttachment = { services.loadContractAttachment(it) },
|
resolveContractAttachment = { services.loadContractAttachment(it) },
|
||||||
whitelistedKeys = whitelistedKeysForAttachments
|
isAttachmentTrusted = { isAttachmentTrusted(it, services.attachments) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,13 +144,11 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
{ null },
|
{ null },
|
||||||
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
|
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
|
||||||
{ resolveAttachment(it.txhash) ?: missingAttachment },
|
{ resolveAttachment(it.txhash) ?: missingAttachment },
|
||||||
listOf()
|
{ isAttachmentTrusted(it, null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Especially crafted for TransactionVerificationRequest.
|
// Especially crafted for TransactionVerificationRequest
|
||||||
// Note that whitelisted keys do not need to be passed here. The DJVM automatically assumes all attachments are provided by a trusted
|
|
||||||
// uploader, and so all attachments will be trusted when this is called from the DJVM.
|
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
internal fun toLtxDjvmInternalBridge(
|
internal fun toLtxDjvmInternalBridge(
|
||||||
resolveAttachment: (SecureHash) -> Attachment?,
|
resolveAttachment: (SecureHash) -> Attachment?,
|
||||||
@ -164,7 +161,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
||||||
resolveParameters,
|
resolveParameters,
|
||||||
{ resolveAttachment(it.txhash) ?: missingAttachment },
|
{ resolveAttachment(it.txhash) ?: missingAttachment },
|
||||||
listOf()
|
{ true } // Any attachment loaded through the DJVM should be trusted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +171,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
|
resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
|
||||||
resolveParameters: (SecureHash?) -> NetworkParameters?,
|
resolveParameters: (SecureHash?) -> NetworkParameters?,
|
||||||
resolveContractAttachment: (StateRef) -> Attachment,
|
resolveContractAttachment: (StateRef) -> Attachment,
|
||||||
whitelistedKeys: Collection<SecureHash>
|
isAttachmentTrusted: (Attachment) -> Boolean
|
||||||
): LedgerTransaction {
|
): LedgerTransaction {
|
||||||
// Look up public keys to authenticated identities.
|
// Look up public keys to authenticated identities.
|
||||||
val authenticatedCommands = commands.lazyMapped { cmd, _ ->
|
val authenticatedCommands = commands.lazyMapped { cmd, _ ->
|
||||||
@ -210,7 +207,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
componentGroups,
|
componentGroups,
|
||||||
serializedResolvedInputs,
|
serializedResolvedInputs,
|
||||||
serializedResolvedReferences,
|
serializedResolvedReferences,
|
||||||
whitelistedKeysForAttachments = whitelistedKeys
|
isAttachmentTrusted
|
||||||
)
|
)
|
||||||
|
|
||||||
checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences)
|
checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences)
|
||||||
@ -354,7 +351,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
?: throw TransactionResolutionException(stateRef.txhash)
|
?: throw TransactionResolutionException(stateRef.txhash)
|
||||||
// Get the network parameters from the tx or whatever the default params are.
|
// Get the network parameters from the tx or whatever the default params are.
|
||||||
val paramsHash = coreTransaction.networkParametersHash ?: services.networkParametersService.defaultHash
|
val paramsHash = coreTransaction.networkParametersHash ?: services.networkParametersService.defaultHash
|
||||||
val params = services.networkParametersService.lookup(paramsHash) ?: throw IllegalStateException("Should have been able to fetch parameters by this point: $paramsHash")
|
val params = services.networkParametersService.lookup(paramsHash)
|
||||||
|
?: throw IllegalStateException("Should have been able to fetch parameters by this point: $paramsHash")
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
when (coreTransaction) {
|
when (coreTransaction) {
|
||||||
is WireTransaction -> coreTransaction.componentGroups
|
is WireTransaction -> coreTransaction.componentGroups
|
||||||
|
@ -4,6 +4,7 @@ import net.corda.core.contracts.Contract
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.declaredField
|
import net.corda.core.internal.declaredField
|
||||||
|
import net.corda.core.internal.isAttachmentTrusted
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -51,7 +52,7 @@ class AttachmentsClassLoaderSerializationTests {
|
|||||||
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
|
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
|
||||||
testNetworkParameters(),
|
testNetworkParameters(),
|
||||||
SecureHash.zeroHash,
|
SecureHash.zeroHash,
|
||||||
listOf()) { classLoader ->
|
{ isAttachmentTrusted(it, storage) }) { classLoader ->
|
||||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
||||||
val contract = contractClass.newInstance() as Contract
|
val contract = contractClass.newInstance() as Contract
|
||||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||||
|
@ -3,15 +3,13 @@ package net.corda.core.transactions
|
|||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.Contract
|
import net.corda.core.contracts.Contract
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.crypto.Crypto
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.declaredField
|
import net.corda.core.internal.declaredField
|
||||||
import net.corda.core.internal.hash
|
|
||||||
import net.corda.core.internal.inputStream
|
import net.corda.core.internal.inputStream
|
||||||
|
import net.corda.core.internal.isAttachmentTrusted
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.services.AttachmentId
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||||
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
||||||
import net.corda.testing.internal.fakeAttachment
|
import net.corda.testing.internal.fakeAttachment
|
||||||
@ -44,9 +42,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
private val storage = MockAttachmentStorage()
|
private val storage = MockAttachmentStorage()
|
||||||
private val networkParameters = testNetworkParameters()
|
private val networkParameters = testNetworkParameters()
|
||||||
private fun make(attachments: List<Attachment>,
|
private fun make(attachments: List<Attachment>,
|
||||||
params: NetworkParameters = networkParameters,
|
params: NetworkParameters = networkParameters): AttachmentsClassLoader {
|
||||||
whitelistedKeys: List<SecureHash> = listOf()): AttachmentsClassLoader {
|
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
|
||||||
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, whitelistedPublicKeys = whitelistedKeys)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -199,22 +196,6 @@ class AttachmentsClassLoaderTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Allow loading an untrusted contract jar if signed by a trusted public key`() {
|
|
||||||
val keyPair = Crypto.generateKeyPair()
|
|
||||||
val classJar = fakeAttachment("/com/example/something/UntrustedClass.class", "Signed by someone trusted").inputStream()
|
|
||||||
val attachment = classJar.use { storage.importContractAttachment(listOf("UntrustedClass.class"), "untrusted", classJar, signers = listOf(keyPair.public))}
|
|
||||||
|
|
||||||
// Check that without the public key whitelisted, building the AttachmentsClassLoader fails. The AttachmentsClassLoader is responsible
|
|
||||||
// for checking what attachments are trusted at the point that it is constructed.
|
|
||||||
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
|
|
||||||
make(arrayOf(attachment).map { storage.openAttachment(it)!! })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that with the public key whitelisted, the AttachmentsClassLoader can be built (i.e. the attachment trusted check passes)
|
|
||||||
make(arrayOf(attachment).map { storage.openAttachment(it)!! }, whitelistedKeys = listOf(keyPair.public.hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||||
return jar.use { storage.importAttachment(jar, uploader, filename) }
|
return jar.use { storage.importAttachment(jar, uploader, filename) }
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,8 @@ class TransactionTests {
|
|||||||
timeWindow,
|
timeWindow,
|
||||||
privacySalt,
|
privacySalt,
|
||||||
testNetworkParameters(),
|
testNetworkParameters(),
|
||||||
emptyList()
|
emptyList(),
|
||||||
|
isAttachmentTrusted = { true }
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction.verify()
|
transaction.verify()
|
||||||
@ -191,7 +192,8 @@ class TransactionTests {
|
|||||||
timeWindow,
|
timeWindow,
|
||||||
privacySalt,
|
privacySalt,
|
||||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
||||||
emptyList()
|
emptyList(),
|
||||||
|
isAttachmentTrusted = {true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||||
|
@ -101,6 +101,14 @@ across the nodes that intend to use it.
|
|||||||
Each transaction received by a node will then verify that the apps attached to it have the correct signers as specified by its
|
Each transaction received by a node will then verify that the apps attached to it have the correct signers as specified by its
|
||||||
Signature Constraints. This ensures that the version of each app is acceptable to the transaction's input states.
|
Signature Constraints. This ensures that the version of each app is acceptable to the transaction's input states.
|
||||||
|
|
||||||
|
If a node receives a transaction that uses a contract attachment that it doesn't trust, but there is an attachment present on the node with
|
||||||
|
the same contract classes and same signatures, then the node will execute that contract's code as if it were trusted. This means that nodes
|
||||||
|
are no longer required to have every version of a CorDapp uploaded to them in order to verify transactions running older version of a CorDapp.
|
||||||
|
Instead, it is sufficient to have any version of the CorDapp contract installed.
|
||||||
|
|
||||||
|
For third party dependencies attached to the transaction, the rule is slightly different. In this case, the attachment will be trusted by the
|
||||||
|
node provided there is another trusted attachment in the node's attachment store that has been signed with the same keys.
|
||||||
|
|
||||||
More information on how to sign an app directly from Gradle can be found in the
|
More information on how to sign an app directly from Gradle can be found in the
|
||||||
:ref:`CorDapp Jar signing <cordapp_build_system_signing_cordapp_jar_ref>` section of the documentation.
|
:ref:`CorDapp Jar signing <cordapp_build_system_signing_cordapp_jar_ref>` section of the documentation.
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ release, see :doc:`app-upgrade-notes`.
|
|||||||
Version 4.2
|
Version 4.2
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
* Added the ``whitelistedKeysForAttachments`` configuration option. This is a list of SHA-256 hashes of public keys. Attachments signed by
|
* Contract attachments are now automatically whitelisted by the node if another contract attachment is present with the same contract classes,
|
||||||
any keys in this list will automatically be trusted by the node. This change removes the requirement to have every version of a CorDapp
|
signed by the same public keys, and uploaded by a trusted uploader. This allows the node to resolve transactions that use earlier versions
|
||||||
present in the node in order to verify a chain of transactions using different versions of the same CorDapp - instead the signing key can
|
of a contract without having to manually install that version, provided a newer version is installed. Similarly, non-contract attachments
|
||||||
be whitelisted.
|
are whitelisted if another attachment is present on the node that is signed by the same public key.
|
||||||
|
|
||||||
.. _changelog_v4.0:
|
.. _changelog_v4.0:
|
||||||
|
|
||||||
|
@ -545,16 +545,6 @@ verfierType
|
|||||||
|
|
||||||
*Default:* InMemory
|
*Default:* InMemory
|
||||||
|
|
||||||
|
|
||||||
whitelistedKeysForAttachments
|
|
||||||
A list of SHA256 hashes of public keys. Any attachments that are signed by a key that hashes to one of the items in this list will be
|
|
||||||
treated as trusted by the node, even if it was received by an untrusted source (for example, over the network).
|
|
||||||
|
|
||||||
.. note:: In the future, the DJVM will be integrated with Corda and all attachments will be loaded inside a DJVM sandbox. At this point,
|
|
||||||
all attachments would be considered trusted, and so this configuration option would be ignored.
|
|
||||||
|
|
||||||
*Default:* not defined
|
|
||||||
|
|
||||||
Reference.conf
|
Reference.conf
|
||||||
--------------
|
--------------
|
||||||
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.
|
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.
|
||||||
|
@ -14,14 +14,12 @@ import net.corda.core.transactions.TransactionBuilder
|
|||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.testing.common.internal.checkNotOnClasspath
|
import net.corda.testing.common.internal.checkNotOnClasspath
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
|
||||||
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
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.driver.DriverDSL
|
import net.corda.testing.driver.DriverDSL
|
||||||
import net.corda.testing.driver.DriverParameters
|
import net.corda.testing.driver.DriverParameters
|
||||||
import net.corda.testing.driver.NodeParameters
|
|
||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
import net.corda.testing.node.NotarySpec
|
import net.corda.testing.node.NotarySpec
|
||||||
import net.corda.testing.node.internal.enclosedCordapp
|
import net.corda.testing.node.internal.enclosedCordapp
|
||||||
@ -29,7 +27,6 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.jar.JarInputStream
|
|
||||||
|
|
||||||
class AttachmentLoadingTests {
|
class AttachmentLoadingTests {
|
||||||
private companion object {
|
private companion object {
|
||||||
@ -90,49 +87,6 @@ class AttachmentLoadingTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `contract is not executed if signing key is not whitelisted and uploader is untrusted`() {
|
|
||||||
driver(DriverParameters(
|
|
||||||
startNodesInProcess = false,
|
|
||||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
|
||||||
cordappsForAllNodes = listOf(enclosedCordapp()),
|
|
||||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
|
||||||
)) {
|
|
||||||
installIsolatedCordapp(ALICE_NAME)
|
|
||||||
|
|
||||||
val (alice, bob) = listOf(
|
|
||||||
startNode(providedName = ALICE_NAME),
|
|
||||||
startNode(NodeParameters(providedName = BOB_NAME))
|
|
||||||
).transpose().getOrThrow()
|
|
||||||
|
|
||||||
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
|
||||||
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
|
|
||||||
.hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `contract is executed if signing key is whitelisted`() {
|
|
||||||
driver(DriverParameters(
|
|
||||||
startNodesInProcess = false,
|
|
||||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
|
||||||
cordappsForAllNodes = listOf(enclosedCordapp()),
|
|
||||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
|
||||||
)) {
|
|
||||||
installIsolatedCordapp(ALICE_NAME)
|
|
||||||
|
|
||||||
val signingKeys = JarSignatureCollector.collectSigners(JarInputStream(isolatedJar.openStream()))
|
|
||||||
val bobOverrides = mapOf("whitelistedKeysForAttachments" to signingKeys.map{ it.hash.toString() })
|
|
||||||
val (alice, bob) = listOf(
|
|
||||||
startNode(providedName = ALICE_NAME),
|
|
||||||
startNode(NodeParameters(providedName = BOB_NAME).withCustomOverrides(bobOverrides))
|
|
||||||
).transpose().getOrThrow()
|
|
||||||
|
|
||||||
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
|
||||||
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
|
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
|
||||||
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
|
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
|
||||||
isolatedJar.toPath().copyToDirectory(cordappsDir)
|
isolatedJar.toPath().copyToDirectory(cordappsDir)
|
||||||
|
@ -178,7 +178,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
|
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
||||||
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, configuration.whitelistedKeysForAttachments).also {
|
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also {
|
||||||
attachments.servicesForResolution = it
|
attachments.servicesForResolution = it
|
||||||
}
|
}
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
@ -969,7 +969,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolutionInternal by servicesForResolution {
|
inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution {
|
||||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
|
||||||
override val identityService: IdentityService get() = this@AbstractNode.identityService
|
override val identityService: IdentityService get() = this@AbstractNode.identityService
|
||||||
|
@ -2,9 +2,7 @@ package net.corda.node.internal
|
|||||||
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.cordapp.CordappProvider
|
import net.corda.core.cordapp.CordappProvider
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.internal.SerializedStateAndRef
|
import net.corda.core.internal.SerializedStateAndRef
|
||||||
import net.corda.core.internal.ServicesForResolutionInternal
|
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
@ -21,9 +19,8 @@ data class ServicesForResolutionImpl(
|
|||||||
override val attachments: AttachmentStorage,
|
override val attachments: AttachmentStorage,
|
||||||
override val cordappProvider: CordappProvider,
|
override val cordappProvider: CordappProvider,
|
||||||
override val networkParametersService: NetworkParametersService,
|
override val networkParametersService: NetworkParametersService,
|
||||||
private val validatedTransactions: TransactionStorage,
|
private val validatedTransactions: TransactionStorage
|
||||||
override val whitelistedKeysForAttachments: Collection<SecureHash>
|
) : ServicesForResolution {
|
||||||
) : ServicesForResolutionInternal {
|
|
||||||
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
|
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
|
||||||
throw IllegalArgumentException("No current parameters in network parameters storage")
|
throw IllegalArgumentException("No current parameters in network parameters storage")
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.cordapp.CordappProvider
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.deserialiseComponentGroup
|
import net.corda.core.internal.deserialiseComponentGroup
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.isAttachmentTrusted
|
||||||
import net.corda.core.internal.readObject
|
import net.corda.core.internal.readObject
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
@ -106,8 +107,13 @@ class MigrationServicesForResolution(
|
|||||||
|
|
||||||
private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection<Int>): List<TransactionState<ContractState>> {
|
private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection<Int>): List<TransactionState<ContractState>> {
|
||||||
return try {
|
return try {
|
||||||
val attachments = tx.attachments.mapNotNull { attachments.openAttachment(it)}
|
val txAttachments = tx.attachments.mapNotNull { attachments.openAttachment(it)}
|
||||||
val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(attachments, networkParameters, tx.id, listOf(), cordappLoader.appClassLoader) {
|
val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
|
||||||
|
txAttachments,
|
||||||
|
networkParameters,
|
||||||
|
tx.id,
|
||||||
|
{ isAttachmentTrusted(it, attachments) },
|
||||||
|
cordappLoader.appClassLoader) {
|
||||||
deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||||
}
|
}
|
||||||
states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList()
|
states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList()
|
||||||
|
@ -4,7 +4,6 @@ import com.typesafe.config.Config
|
|||||||
import net.corda.common.configuration.parsing.internal.Configuration
|
import net.corda.common.configuration.parsing.internal.Configuration
|
||||||
import net.corda.common.validation.internal.Validated
|
import net.corda.common.validation.internal.Validated
|
||||||
import net.corda.core.context.AuthServiceId
|
import net.corda.core.context.AuthServiceId
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.TimedFlow
|
import net.corda.core.internal.TimedFlow
|
||||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||||
@ -85,7 +84,6 @@ interface NodeConfiguration {
|
|||||||
val cordappSignerKeyFingerprintBlacklist: List<String>
|
val cordappSignerKeyFingerprintBlacklist: List<String>
|
||||||
|
|
||||||
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
|
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
|
||||||
val whitelistedKeysForAttachments: List<SecureHash>
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// default to at least 8MB and a bit extra for larger heap sizes
|
// default to at least 8MB and a bit extra for larger heap sizes
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.node.services.config
|
package net.corda.node.services.config
|
||||||
|
|
||||||
import com.typesafe.config.ConfigException
|
import com.typesafe.config.ConfigException
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
@ -76,8 +75,7 @@ data class NodeConfigurationImpl(
|
|||||||
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
|
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
|
||||||
override val flowOverrides: FlowOverrideConfig?,
|
override val flowOverrides: FlowOverrideConfig?,
|
||||||
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
|
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
|
||||||
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings,
|
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
|
||||||
override val whitelistedKeysForAttachments: List<SecureHash> = listOf()
|
|
||||||
) : NodeConfiguration {
|
) : NodeConfiguration {
|
||||||
internal object Defaults {
|
internal object Defaults {
|
||||||
val jmxMonitoringHttpPort: Int? = null
|
val jmxMonitoringHttpPort: Int? = null
|
||||||
|
@ -6,7 +6,6 @@ import com.typesafe.config.ConfigUtil
|
|||||||
import net.corda.common.configuration.parsing.internal.Configuration
|
import net.corda.common.configuration.parsing.internal.Configuration
|
||||||
import net.corda.common.validation.internal.Validated.Companion.invalid
|
import net.corda.common.validation.internal.Validated.Companion.invalid
|
||||||
import net.corda.common.validation.internal.Validated.Companion.valid
|
import net.corda.common.validation.internal.Validated.Companion.valid
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.node.services.config.Valid
|
import net.corda.node.services.config.Valid
|
||||||
@ -34,8 +33,6 @@ internal fun toPrincipal(rawValue: String) = attempt<X500Principal, IllegalArgum
|
|||||||
|
|
||||||
internal fun toPath(rawValue: String) = attempt<Path, InvalidPathException> { Paths.get(rawValue) }
|
internal fun toPath(rawValue: String) = attempt<Path, InvalidPathException> { Paths.get(rawValue) }
|
||||||
|
|
||||||
internal fun toSecureHash(rawValue: String) = attempt<SecureHash, IllegalArgumentException> { SecureHash.parse(rawValue)}
|
|
||||||
|
|
||||||
private inline fun <RESULT, reified ERROR : Exception> attempt(action: () -> RESULT, message: (ERROR) -> String): Valid<RESULT> {
|
private inline fun <RESULT, reified ERROR : Exception> attempt(action: () -> RESULT, message: (ERROR) -> String): Valid<RESULT> {
|
||||||
return try {
|
return try {
|
||||||
valid(action.invoke())
|
valid(action.invoke())
|
||||||
|
@ -17,7 +17,14 @@ import net.corda.node.services.config.NodeConfigurationImpl
|
|||||||
import net.corda.node.services.config.NodeConfigurationImpl.Defaults
|
import net.corda.node.services.config.NodeConfigurationImpl.Defaults
|
||||||
import net.corda.node.services.config.Valid
|
import net.corda.node.services.config.Valid
|
||||||
import net.corda.node.services.config.VerifierType
|
import net.corda.node.services.config.VerifierType
|
||||||
import net.corda.node.services.config.schema.parsers.*
|
import net.corda.node.services.config.schema.parsers.badValue
|
||||||
|
import net.corda.node.services.config.schema.parsers.toCordaX500Name
|
||||||
|
import net.corda.node.services.config.schema.parsers.toNetworkHostAndPort
|
||||||
|
import net.corda.node.services.config.schema.parsers.toPath
|
||||||
|
import net.corda.node.services.config.schema.parsers.toPrincipal
|
||||||
|
import net.corda.node.services.config.schema.parsers.toProperties
|
||||||
|
import net.corda.node.services.config.schema.parsers.toURL
|
||||||
|
import net.corda.node.services.config.schema.parsers.toUUID
|
||||||
|
|
||||||
internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfiguration>("NodeConfiguration") {
|
internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfiguration>("NodeConfiguration") {
|
||||||
private val myLegalName by string().mapValid(::toCordaX500Name)
|
private val myLegalName by string().mapValid(::toCordaX500Name)
|
||||||
@ -66,7 +73,6 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
|||||||
private val jarDirs by string().list().optional().withDefaultValue(Defaults.jarDirs)
|
private val jarDirs by string().list().optional().withDefaultValue(Defaults.jarDirs)
|
||||||
private val cordappDirectories by string().mapValid(::toPath).list().optional()
|
private val cordappDirectories by string().mapValid(::toPath).list().optional()
|
||||||
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
|
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
|
||||||
private val whitelistedKeysForAttachments by string().mapValid(::toSecureHash).list().optional().withDefaultValue(listOf())
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
private val custom by nestedObject().optional()
|
private val custom by nestedObject().optional()
|
||||||
|
|
||||||
@ -122,8 +128,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
|||||||
h2port = configuration[h2port],
|
h2port = configuration[h2port],
|
||||||
jarDirs = configuration[jarDirs],
|
jarDirs = configuration[jarDirs],
|
||||||
cordappDirectories = cordappDirectories,
|
cordappDirectories = cordappDirectories,
|
||||||
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
|
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist]
|
||||||
whitelistedKeysForAttachments = configuration[whitelistedKeysForAttachments]
|
|
||||||
))
|
))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return when (e) {
|
return when (e) {
|
||||||
|
@ -25,11 +25,17 @@ import net.corda.nodeapi.exceptions.DuplicateContractClassException
|
|||||||
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.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.core.internal.ContractJarTestUtils
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestJar
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestSignedContractJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestSignedContractJar
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||||
|
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||||
import net.corda.testing.core.internal.SelfCleaningDir
|
import net.corda.testing.core.internal.SelfCleaningDir
|
||||||
import net.corda.testing.internal.*
|
import net.corda.testing.internal.LogHelper
|
||||||
|
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||||
|
import net.corda.testing.internal.configureDatabase
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.startFlow
|
import net.corda.testing.node.internal.startFlow
|
||||||
@ -40,7 +46,6 @@ import org.junit.Before
|
|||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.FileAlreadyExistsException
|
import java.nio.file.FileAlreadyExistsException
|
||||||
@ -48,10 +53,7 @@ import java.nio.file.FileSystem
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.*
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertNotEquals
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class NodeAttachmentServiceTest {
|
class NodeAttachmentServiceTest {
|
||||||
|
|
||||||
@ -601,7 +603,7 @@ class NodeAttachmentServiceTest {
|
|||||||
fun `retrieve latest versions of unsigned and signed contracts - both exist at same version`() {
|
fun `retrieve latest versions of unsigned and signed contracts - both exist at same version`() {
|
||||||
SelfCleaningDir().use { file ->
|
SelfCleaningDir().use { file ->
|
||||||
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
||||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
val (signedContractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||||
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
||||||
val (signedContractJarV2, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", version = 2)
|
val (signedContractJarV2, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", version = 2)
|
||||||
|
|
||||||
@ -623,7 +625,7 @@ class NodeAttachmentServiceTest {
|
|||||||
fun `retrieve latest versions of unsigned and signed contracts - signed is later version than unsigned`() {
|
fun `retrieve latest versions of unsigned and signed contracts - signed is later version than unsigned`() {
|
||||||
SelfCleaningDir().use { file ->
|
SelfCleaningDir().use { file ->
|
||||||
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
||||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
val (signedContractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||||
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
||||||
|
|
||||||
contractJar.read { storage.privilegedImportAttachment(it, "app", "contract.jar") }
|
contractJar.read { storage.privilegedImportAttachment(it, "app", "contract.jar") }
|
||||||
@ -643,7 +645,7 @@ class NodeAttachmentServiceTest {
|
|||||||
fun `retrieve latest versions of unsigned and signed contracts - unsigned is later version than signed`() {
|
fun `retrieve latest versions of unsigned and signed contracts - unsigned is later version than signed`() {
|
||||||
SelfCleaningDir().use { file ->
|
SelfCleaningDir().use { file ->
|
||||||
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
||||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
val (signedContractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||||
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = 2)
|
||||||
|
|
||||||
contractJar.read { storage.privilegedImportAttachment(it, "app", "contract.jar") }
|
contractJar.read { storage.privilegedImportAttachment(it, "app", "contract.jar") }
|
||||||
@ -662,7 +664,7 @@ class NodeAttachmentServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `retrieve latest versions of unsigned and signed contracts - only signed contracts exist in store`() {
|
fun `retrieve latest versions of unsigned and signed contracts - only signed contracts exist in store`() {
|
||||||
SelfCleaningDir().use { file ->
|
SelfCleaningDir().use { file ->
|
||||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
val (signedContractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||||
val (signedContractJarV2, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", version = 2)
|
val (signedContractJarV2, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", version = 2)
|
||||||
|
|
||||||
signedContractJar.read { storage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
|
signedContractJar.read { storage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
|
||||||
@ -702,7 +704,7 @@ class NodeAttachmentServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `development mode - retrieve latest versions of signed contracts - multiple versions of same version id exist in store`() {
|
fun `development mode - retrieve latest versions of signed contracts - multiple versions of same version id exist in store`() {
|
||||||
SelfCleaningDir().use { file ->
|
SelfCleaningDir().use { file ->
|
||||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
val (signedContractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||||
val (signedContractJarSameVersion, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", versionSeed = Random().nextInt())
|
val (signedContractJarSameVersion, _) = makeTestSignedContractJar(file.path,"com.example.MyContract", versionSeed = Random().nextInt())
|
||||||
|
|
||||||
signedContractJar.read { devModeStorage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
|
signedContractJar.read { devModeStorage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
|
||||||
@ -771,6 +773,184 @@ class NodeAttachmentServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Jar uploaded by trusted uploader is trusted`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val (jar, _) = makeTestSignedContractJar(file.path, "foo.bar.DummyContract")
|
||||||
|
val unsignedJar = ContractJarTestUtils.makeTestContractJar(file.path, "com.example.MyContract")
|
||||||
|
val (attachment, _) = makeTestJar()
|
||||||
|
|
||||||
|
val signedId = jar.read { storage.privilegedImportAttachment(it, "app", "signed-contract.jar")}
|
||||||
|
val unsignedId = unsignedJar.read { storage.privilegedImportAttachment(it, "app", "unsigned-contract.jar") }
|
||||||
|
val attachmentId = attachment.read { storage.privilegedImportAttachment(it, "app", "attachment.jar")}
|
||||||
|
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(signedId)!!, storage), "Signed contract $signedId should be trusted but isn't")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(unsignedId)!!, storage), "Unsigned contract $unsignedId should be trusted but isn't")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentId)!!, storage), "Attachment $attachmentId should be trusted but isn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `jar trusted if signed by same key and has same contract as existing jar`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertEquals(key1, key2, "Different public keys used to sign jars")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `jar not trusted if same key but different contract`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertEquals(key1, key2, "Different public keys used to sign jars")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `jar not trusted if different key but same contract`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
(file.path / "_shredder").delete()
|
||||||
|
(file.path / "_teststore").delete()
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertNotEquals(key1, key2, "Same public keys used to sign jars")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `neither jar trusted if same contract and signer but not uploaded by a trusted uploader`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertEquals(key1, key2, "Different public keys used to sign jars")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should not be trusted")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non contract jar trusted if trusted jar with same key present`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
|
||||||
|
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
|
||||||
|
// them.
|
||||||
|
var counter = 0
|
||||||
|
val jarV1 = file.path / "$counter.jar"
|
||||||
|
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
|
||||||
|
counter++
|
||||||
|
val jarV2 = file.path / "$counter.jar"
|
||||||
|
// Ensure that the first and second jars do not have the same hash
|
||||||
|
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
|
||||||
|
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertEquals(key1, key2, "Different public keys used to sign jars")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should be trusted")
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all non contract jars not trusted if all are uploaded by non trusted uploaders`() {
|
||||||
|
SelfCleaningDir().use { file ->
|
||||||
|
val alias = "testAlias"
|
||||||
|
val password = "testPassword"
|
||||||
|
|
||||||
|
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
|
||||||
|
// them.
|
||||||
|
var counter = 0
|
||||||
|
val jarV1 = file.path / "$counter.jar"
|
||||||
|
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
|
||||||
|
counter++
|
||||||
|
val jarV2 = file.path / "$counter.jar"
|
||||||
|
// Ensure that the first and second jars do not have the same hash
|
||||||
|
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
|
||||||
|
|
||||||
|
file.path.generateKey(alias, password)
|
||||||
|
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
|
||||||
|
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
assertEquals(key1, key2, "Different public keys used to sign jars")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should not be trusted")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should not be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non contract jars not trusted if unsigned`() {
|
||||||
|
SelfCleaningDir().use {
|
||||||
|
val (jarV1, _) = makeTestJar()
|
||||||
|
val (jarV2, _) = makeTestJar(entries = listOf(Pair("foo", "bar")))
|
||||||
|
|
||||||
|
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
|
||||||
|
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
|
||||||
|
|
||||||
|
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should not be trusted")
|
||||||
|
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should not be trusted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Not the real FetchAttachmentsFlow!
|
// Not the real FetchAttachmentsFlow!
|
||||||
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
|
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -782,10 +962,13 @@ class NodeAttachmentServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var counter = 0
|
private var counter = 0
|
||||||
private fun makeTestJar(extraEntries: List<Pair<String, String>> = emptyList()): Pair<Path, SecureHash> {
|
private fun makeTestJar(entries: List<Pair<String, String>> = listOf(
|
||||||
|
Pair("test1.txt", "This is some useful content"),
|
||||||
|
Pair("test2.txt", "Some more useful content")
|
||||||
|
)): Pair<Path, SecureHash> {
|
||||||
counter++
|
counter++
|
||||||
val file = fs.getPath("$counter.jar")
|
val file = fs.getPath("$counter.jar")
|
||||||
makeTestJar(file.outputStream(), extraEntries)
|
makeTestJar(file.outputStream(), entries)
|
||||||
return Pair(file, file.readAll().sha256())
|
return Pair(file, file.readAll().sha256())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import com.nhaarman.mockito_kotlin.whenever
|
|||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||||
|
import net.corda.core.internal.isAttachmentTrusted
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.core.serialization.ClassWhitelist
|
import net.corda.core.serialization.ClassWhitelist
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -213,7 +214,7 @@ class CordaClassResolverTests {
|
|||||||
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
|
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
|
||||||
val storage = MockAttachmentStorage()
|
val storage = MockAttachmentStorage()
|
||||||
val attachmentHash = importJar(storage)
|
val attachmentHash = importJar(storage)
|
||||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, whitelistedPublicKeys = listOf())
|
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
|
||||||
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||||
}
|
}
|
||||||
@ -222,7 +223,7 @@ class CordaClassResolverTests {
|
|||||||
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
|
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
|
||||||
val storage = MockAttachmentStorage()
|
val storage = MockAttachmentStorage()
|
||||||
val attachmentHash = importJar(storage, "some_uploader")
|
val attachmentHash = importJar(storage, "some_uploader")
|
||||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, whitelistedPublicKeys = listOf())
|
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
|
||||||
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,7 @@ open class MockServices private constructor(
|
|||||||
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
|
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
|
||||||
|
|
||||||
protected val servicesForResolution: ServicesForResolution
|
protected val servicesForResolution: ServicesForResolution
|
||||||
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, listOf())
|
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions)
|
||||||
|
|
||||||
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
|
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
|
||||||
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }
|
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }
|
||||||
|
@ -614,7 +614,6 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
|
|||||||
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec
|
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec
|
||||||
doReturn(null).whenever(it).devModeOptions
|
doReturn(null).whenever(it).devModeOptions
|
||||||
doReturn(NetworkParameterAcceptanceSettings()).whenever(it).networkParameterAcceptanceSettings
|
doReturn(NetworkParameterAcceptanceSettings()).whenever(it).networkParameterAcceptanceSettings
|
||||||
doReturn(emptyList<SecureHash>()).whenever(it).whitelistedKeysForAttachments
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,19 +28,17 @@ import javax.tools.ToolProvider
|
|||||||
object ContractJarTestUtils {
|
object ContractJarTestUtils {
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
|
fun makeTestJar(output: OutputStream,
|
||||||
output.use {
|
entries: List<Pair<String, String>> = listOf(
|
||||||
val jar = JarOutputStream(it)
|
Pair("test1.txt", "This is some useful content"),
|
||||||
jar.putNextEntry(JarEntry("test1.txt"))
|
Pair("test2.txt", "Some more useful content")
|
||||||
jar.write("This is some useful content".toByteArray())
|
)) {
|
||||||
jar.closeEntry()
|
JarOutputStream(output).use {
|
||||||
jar.putNextEntry(JarEntry("test2.txt"))
|
entries.forEach { entry ->
|
||||||
jar.write("Some more useful content".toByteArray())
|
it.putNextEntry(JarEntry(entry.first))
|
||||||
extraEntries.forEach {
|
it.write(entry.second.toByteArray())
|
||||||
jar.putNextEntry(JarEntry(it.first))
|
it.closeEntry()
|
||||||
jar.write(it.second.toByteArray())
|
|
||||||
}
|
}
|
||||||
jar.closeEntry()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,20 +5,19 @@ 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.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.AbstractAttachment
|
||||||
|
import net.corda.core.internal.TRUSTED_UPLOADERS
|
||||||
|
import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||||
|
import net.corda.core.internal.readFully
|
||||||
import net.corda.core.node.services.AttachmentId
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
import net.corda.core.node.services.vault.*
|
||||||
import net.corda.core.node.services.vault.AttachmentSort
|
|
||||||
import net.corda.core.node.services.vault.Builder
|
|
||||||
import net.corda.core.node.services.vault.ColumnPredicate
|
|
||||||
import net.corda.core.node.services.vault.Sort
|
|
||||||
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.security.PublicKey
|
||||||
import java.util.HashMap
|
import java.util.*
|
||||||
import java.util.jar.Attributes
|
import java.util.jar.Attributes
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ import java.util.jar.JarInputStream
|
|||||||
* A mock implementation of [AttachmentStorage] for use within tests
|
* A mock implementation of [AttachmentStorage] for use within tests
|
||||||
*/
|
*/
|
||||||
class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||||
private data class ContractAttachmentMetadata(val name: ContractClassName, val version: Int, val isSigned: Boolean)
|
private data class ContractAttachmentMetadata(val name: ContractClassName, val version: Int, val isSigned: Boolean, val signers: List<PublicKey>, val uploader: String)
|
||||||
|
|
||||||
private val _files = HashMap<SecureHash, Pair<Attachment, ByteArray>>()
|
private val _files = HashMap<SecureHash, Pair<Attachment, ByteArray>>()
|
||||||
private val _contractClasses = HashMap<ContractAttachmentMetadata, SecureHash>()
|
private val _contractClasses = HashMap<ContractAttachmentMetadata, SecureHash>()
|
||||||
@ -44,27 +43,27 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
|||||||
|
|
||||||
override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first
|
override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first
|
||||||
|
|
||||||
|
// This function only covers those possibilities currently used within tests. Each ColumnPredicate type can have multiple operators,
|
||||||
|
// and not all predicate types are covered here.
|
||||||
|
private fun <C> criteriaFilter(metadata: C, predicate: ColumnPredicate<C>?): Boolean {
|
||||||
|
return when (predicate) {
|
||||||
|
is ColumnPredicate.EqualityComparison -> predicate.rightLiteral == metadata
|
||||||
|
is ColumnPredicate.CollectionExpression -> predicate.rightLiteral.contains(metadata)
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
||||||
criteria as AttachmentQueryCriteria.AttachmentsQueryCriteria
|
criteria as AttachmentQueryCriteria.AttachmentsQueryCriteria
|
||||||
val contractClassNames =
|
val metadataFilter = { metadata: ContractAttachmentMetadata ->
|
||||||
if (criteria.contractClassNamesCondition is ColumnPredicate.EqualityComparison)
|
criteriaFilter(listOf(metadata.name), criteria.contractClassNamesCondition) &&
|
||||||
(criteria.contractClassNamesCondition as ColumnPredicate.EqualityComparison<List<ContractClassName>>).rightLiteral
|
criteriaFilter(metadata.signers, criteria.signersCondition) &&
|
||||||
else emptyList()
|
criteriaFilter(metadata.isSigned, criteria.isSignedCondition) &&
|
||||||
val contractMetadataList =
|
criteriaFilter(metadata.version, criteria.versionCondition) &&
|
||||||
if (criteria.isSignedCondition != null) {
|
criteriaFilter(metadata.uploader, criteria.uploaderCondition)
|
||||||
val isSigned = criteria.isSignedCondition == Builder.equal(true)
|
}
|
||||||
contractClassNames.map {contractClassName ->
|
|
||||||
ContractAttachmentMetadata(contractClassName, 1, isSigned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
contractClassNames.flatMap { contractClassName ->
|
|
||||||
listOf(ContractAttachmentMetadata(contractClassName, 1, false),
|
|
||||||
ContractAttachmentMetadata(contractClassName, 1, true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _contractClasses.filterKeys { contractMetadataList.contains(it) }.values.toList()
|
return _contractClasses.filterKeys { metadataFilter(it) }.values.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
|
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
|
||||||
@ -103,7 +102,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
|||||||
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
|
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
|
||||||
else {
|
else {
|
||||||
contractClassNames.map {contractClassName ->
|
contractClassNames.map {contractClassName ->
|
||||||
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty())
|
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty(), signers, uploader)
|
||||||
_contractClasses[contractClassMetadata] = sha256
|
_contractClasses[contractClassMetadata] = sha256
|
||||||
}
|
}
|
||||||
ContractAttachment.create(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers, version)
|
ContractAttachment.create(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers, version)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user