[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:
JamesHR3 2019-05-07 12:55:27 +01:00 committed by Shams Asari
parent c533792f3f
commit b4e96778bf
26 changed files with 351 additions and 213 deletions

View File

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

View File

@ -7,6 +7,9 @@ import net.corda.core.crypto.componentHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
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.transactions.*
import net.corda.core.utilities.OpaqueBytes
@ -179,4 +182,50 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
// We will never end up in perfect synchronization with all the nodes. However, network parameters update process
// 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.
}
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
}
}

View File

@ -3,8 +3,8 @@ package net.corda.core.serialization.internal
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
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.PackageOwnershipException
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
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.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.toSHA256Bytes
import java.io.ByteArrayOutputStream
import java.io.IOException
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
* 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.
* @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>,
val params: NetworkParameters,
private val sampleTxId: SecureHash,
private val whitelistedPublicKeys: Collection<SecureHash>,
isAttachmentTrusted: (Attachment) -> Boolean,
parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
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.
val untrusted = attachments
.filter(::containsClasses)
.filterNot { attachment ->
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.
}
}
.filterNot(isAttachmentTrusted)
.map(Attachment::id)
if (untrusted.isNotEmpty()) {
@ -140,10 +131,6 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
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 ->
jar.nextEntry != null
}
@ -322,14 +309,14 @@ object AttachmentsClassLoaderBuilder {
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>,
params: NetworkParameters,
txId: SecureHash,
whitelistedPublicKeys: Collection<SecureHash>,
isAttachmentTrusted: (Attachment) -> Boolean,
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
block: (ClassLoader) -> T): T {
val attachmentIds = attachments.map { it.id }.toSet()
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
// 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 whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
.flatMap { it.whitelist }

View File

@ -9,12 +9,15 @@ import net.corda.core.crypto.componentHash
import net.corda.core.crypto.computeNonce
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.ServicesForResolutionInternal
import net.corda.core.internal.combinedHash
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.node.NetworkParameters
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.serialize
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract
import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.retrieveAppClassLoader
@ -145,13 +148,12 @@ data class ContractUpgradeWireTransaction(
?: throw MissingContractAttachments(emptyList())
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
?: throw MissingContractAttachments(emptyList())
val whitelistedPublicKeys = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
listOf(legacyAttachment, upgradedAttachment),
params,
id,
whitelistedPublicKeys) { transactionClassLoader ->
{ isAttachmentTrusted(it, services.attachments) }) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize()
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)

View File

@ -14,7 +14,6 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.utilities.contextLogger
import java.lang.UnsupportedOperationException
import java.util.*
import java.util.function.Predicate
@ -75,7 +74,7 @@ private constructor(
private var componentGroups: List<ComponentGroup>? = null
private var serializedInputs: List<SerializedStateAndRef>? = null
private var serializedReferences: List<SerializedStateAndRef>? = null
private var whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
private var isAttachmentTrusted: (Attachment) -> Boolean = { isAttachmentTrusted(it, null) }
init {
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
@ -100,13 +99,13 @@ private constructor(
componentGroups: List<ComponentGroup>? = null,
serializedInputs: List<SerializedStateAndRef>? = null,
serializedReferences: List<SerializedStateAndRef>? = null,
whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
isAttachmentTrusted: (Attachment) -> Boolean
): LedgerTransaction {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply {
this.componentGroups = componentGroups
this.serializedInputs = serializedInputs
this.serializedReferences = serializedReferences
this.whitelistedKeysForAttachments = whitelistedKeysForAttachments
this.isAttachmentTrusted = isAttachmentTrusted
}
}
}
@ -148,7 +147,7 @@ private constructor(
this.attachments + extraAttachments,
getParamsWithGoo(),
id,
whitelistedPublicKeys = whitelistedKeysForAttachments) { transactionClassLoader ->
isAttachmentTrusted = isAttachmentTrusted) { 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.
// This artifice is required to preserve backwards compatibility.

View File

@ -99,7 +99,6 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
@DeleteForDJVM
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
val whitelistedKeysForAttachments = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
return toLedgerTransactionInternal(
resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.attachments.openAttachment(it) },
@ -109,7 +108,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
services.networkParametersService.lookup(hashToResolve)
},
resolveContractAttachment = { services.loadContractAttachment(it) },
whitelistedKeys = whitelistedKeysForAttachments
isAttachmentTrusted = { isAttachmentTrusted(it, services.attachments) }
)
}
@ -145,13 +144,11 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ 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
{ resolveAttachment(it.txhash) ?: missingAttachment },
listOf()
{ isAttachmentTrusted(it, null) }
)
}
// 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.
// Especially crafted for TransactionVerificationRequest
@CordaInternal
internal fun toLtxDjvmInternalBridge(
resolveAttachment: (SecureHash) -> Attachment?,
@ -164,7 +161,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
resolveParameters,
{ 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>>?,
resolveParameters: (SecureHash?) -> NetworkParameters?,
resolveContractAttachment: (StateRef) -> Attachment,
whitelistedKeys: Collection<SecureHash>
isAttachmentTrusted: (Attachment) -> Boolean
): LedgerTransaction {
// Look up public keys to authenticated identities.
val authenticatedCommands = commands.lazyMapped { cmd, _ ->
@ -210,7 +207,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
componentGroups,
serializedResolvedInputs,
serializedResolvedReferences,
whitelistedKeysForAttachments = whitelistedKeys
isAttachmentTrusted
)
checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences)
@ -354,7 +351,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
?: throw TransactionResolutionException(stateRef.txhash)
// Get the network parameters from the tx or whatever the default params are.
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")
when (coreTransaction) {
is WireTransaction -> coreTransaction.componentGroups

View File

@ -4,6 +4,7 @@ import net.corda.core.contracts.Contract
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.declaredField
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.serialize
@ -51,7 +52,7 @@ class AttachmentsClassLoaderSerializationTests {
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
testNetworkParameters(),
SecureHash.zeroHash,
listOf()) { classLoader ->
{ isAttachmentTrusted(it, storage) }) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)

View File

@ -3,15 +3,13 @@ package net.corda.core.transactions
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.declaredField
import net.corda.core.internal.hash
import net.corda.core.internal.inputStream
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId
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.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.fakeAttachment
@ -44,9 +42,8 @@ class AttachmentsClassLoaderTests {
private val storage = MockAttachmentStorage()
private val networkParameters = testNetworkParameters()
private fun make(attachments: List<Attachment>,
params: NetworkParameters = networkParameters,
whitelistedKeys: List<SecureHash> = listOf()): AttachmentsClassLoader {
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, whitelistedPublicKeys = whitelistedKeys)
params: NetworkParameters = networkParameters): AttachmentsClassLoader {
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
}
@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 {
return jar.use { storage.importAttachment(jar, uploader, filename) }
}

View File

@ -139,7 +139,8 @@ class TransactionTests {
timeWindow,
privacySalt,
testNetworkParameters(),
emptyList()
emptyList(),
isAttachmentTrusted = { true }
)
transaction.verify()
@ -191,7 +192,8 @@ class TransactionTests {
timeWindow,
privacySalt,
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
emptyList()
emptyList(),
isAttachmentTrusted = {true}
)
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }

View File

@ -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
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
:ref:`CorDapp Jar signing <cordapp_build_system_signing_cordapp_jar_ref>` section of the documentation.

View File

@ -9,10 +9,10 @@ release, see :doc:`app-upgrade-notes`.
Version 4.2
-----------
* Added the ``whitelistedKeysForAttachments`` configuration option. This is a list of SHA-256 hashes of public keys. Attachments signed by
any keys in this list will automatically be trusted by the node. This change removes the requirement to have every version of a CorDapp
present in the node in order to verify a chain of transactions using different versions of the same CorDapp - instead the signing key can
be whitelisted.
* Contract attachments are now automatically whitelisted by the node if another contract attachment is present with the same contract classes,
signed by the same public keys, and uploaded by a trusted uploader. This allows the node to resolve transactions that use earlier versions
of a contract without having to manually install that version, provided a newer version is installed. Similarly, non-contract attachments
are whitelisted if another attachment is present on the node that is signed by the same public key.
.. _changelog_v4.0:

View File

@ -545,16 +545,6 @@ verfierType
*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
--------------
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.

View File

@ -14,14 +14,12 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
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.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.enclosedCordapp
@ -29,7 +27,6 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.net.URL
import java.net.URLClassLoader
import java.util.jar.JarInputStream
class AttachmentLoadingTests {
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) {
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
isolatedJar.toPath().copyToDirectory(cordappsDir)

View File

@ -178,7 +178,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@Suppress("LeakingThis")
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
}
@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 stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
override val identityService: IdentityService get() = this@AbstractNode.identityService

View File

@ -2,9 +2,7 @@ package net.corda.node.internal
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.internal.ServicesForResolutionInternal
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
@ -21,9 +19,8 @@ data class ServicesForResolutionImpl(
override val attachments: AttachmentStorage,
override val cordappProvider: CordappProvider,
override val networkParametersService: NetworkParametersService,
private val validatedTransactions: TransactionStorage,
override val whitelistedKeysForAttachments: Collection<SecureHash>
) : ServicesForResolutionInternal {
private val validatedTransactions: TransactionStorage
) : ServicesForResolution {
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
throw IllegalArgumentException("No current parameters in network parameters storage")

View File

@ -6,6 +6,7 @@ import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.deserialiseComponentGroup
import net.corda.core.internal.div
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.internal.readObject
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
@ -106,8 +107,13 @@ class MigrationServicesForResolution(
private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection<Int>): List<TransactionState<ContractState>> {
return try {
val attachments = tx.attachments.mapNotNull { attachments.openAttachment(it)}
val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(attachments, networkParameters, tx.id, listOf(), cordappLoader.appClassLoader) {
val txAttachments = tx.attachments.mapNotNull { attachments.openAttachment(it)}
val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
txAttachments,
networkParameters,
tx.id,
{ isAttachmentTrusted(it, attachments) },
cordappLoader.appClassLoader) {
deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
}
states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList()

View File

@ -4,7 +4,6 @@ import com.typesafe.config.Config
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.validation.internal.Validated
import net.corda.core.context.AuthServiceId
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.TimedFlow
import net.corda.core.internal.notary.NotaryServiceFlow
@ -85,7 +84,6 @@ interface NodeConfiguration {
val cordappSignerKeyFingerprintBlacklist: List<String>
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
val whitelistedKeysForAttachments: List<SecureHash>
companion object {
// default to at least 8MB and a bit extra for larger heap sizes

View File

@ -1,7 +1,6 @@
package net.corda.node.services.config
import com.typesafe.config.ConfigException
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
@ -76,8 +75,7 @@ data class NodeConfigurationImpl(
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings,
override val whitelistedKeysForAttachments: List<SecureHash> = listOf()
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
) : NodeConfiguration {
internal object Defaults {
val jmxMonitoringHttpPort: Int? = null

View File

@ -6,7 +6,6 @@ import com.typesafe.config.ConfigUtil
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.valid
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
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 toSecureHash(rawValue: String) = attempt<SecureHash, IllegalArgumentException> { SecureHash.parse(rawValue)}
private inline fun <RESULT, reified ERROR : Exception> attempt(action: () -> RESULT, message: (ERROR) -> String): Valid<RESULT> {
return try {
valid(action.invoke())

View File

@ -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.Valid
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") {
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 cordappDirectories by string().mapValid(::toPath).list().optional()
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
private val whitelistedKeysForAttachments by string().mapValid(::toSecureHash).list().optional().withDefaultValue(listOf())
@Suppress("unused")
private val custom by nestedObject().optional()
@ -122,8 +128,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
h2port = configuration[h2port],
jarDirs = configuration[jarDirs],
cordappDirectories = cordappDirectories,
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
whitelistedKeysForAttachments = configuration[whitelistedKeysForAttachments]
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist]
))
} catch (e: Exception) {
return when (e) {

View File

@ -25,11 +25,17 @@ import net.corda.nodeapi.exceptions.DuplicateContractClassException
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestJar
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.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.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
@ -40,7 +46,6 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URL
import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException
@ -48,10 +53,7 @@ import java.nio.file.FileSystem
import java.nio.file.Path
import java.util.*
import java.util.jar.JarInputStream
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.*
class NodeAttachmentServiceTest {
@ -601,7 +603,7 @@ class NodeAttachmentServiceTest {
fun `retrieve latest versions of unsigned and signed contracts - both exist at same version`() {
SelfCleaningDir().use { file ->
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 (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`() {
SelfCleaningDir().use { file ->
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)
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`() {
SelfCleaningDir().use { file ->
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)
contractJar.read { storage.privilegedImportAttachment(it, "app", "contract.jar") }
@ -662,7 +664,7 @@ class NodeAttachmentServiceTest {
@Test
fun `retrieve latest versions of unsigned and signed contracts - only signed contracts exist in store`() {
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)
signedContractJar.read { storage.privilegedImportAttachment(it, "app", "contract-signed.jar") }
@ -702,7 +704,7 @@ class NodeAttachmentServiceTest {
@Test
fun `development mode - retrieve latest versions of signed contracts - multiple versions of same version id exist in store`() {
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())
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!
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
@Suspendable
@ -782,10 +962,13 @@ class NodeAttachmentServiceTest {
}
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++
val file = fs.getPath("$counter.jar")
makeTestJar(file.outputStream(), extraEntries)
makeTestJar(file.outputStream(), entries)
return Pair(file, file.readAll().sha256())
}
}

View File

@ -12,6 +12,7 @@ import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
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.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
@ -213,7 +214,7 @@ class CordaClassResolverTests {
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = MockAttachmentStorage()
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)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@ -222,7 +223,7 @@ class CordaClassResolverTests {
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
val storage = MockAttachmentStorage()
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)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}

View File

@ -393,7 +393,7 @@ open class MockServices private constructor(
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
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 {
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }

View File

@ -614,7 +614,6 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec
doReturn(null).whenever(it).devModeOptions
doReturn(NetworkParameterAcceptanceSettings()).whenever(it).networkParameterAcceptanceSettings
doReturn(emptyList<SecureHash>()).whenever(it).whitelistedKeysForAttachments
}
}

View File

@ -28,19 +28,17 @@ import javax.tools.ToolProvider
object ContractJarTestUtils {
@JvmOverloads
fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
output.use {
val jar = JarOutputStream(it)
jar.putNextEntry(JarEntry("test1.txt"))
jar.write("This is some useful content".toByteArray())
jar.closeEntry()
jar.putNextEntry(JarEntry("test2.txt"))
jar.write("Some more useful content".toByteArray())
extraEntries.forEach {
jar.putNextEntry(JarEntry(it.first))
jar.write(it.second.toByteArray())
fun makeTestJar(output: OutputStream,
entries: List<Pair<String, String>> = listOf(
Pair("test1.txt", "This is some useful content"),
Pair("test2.txt", "Some more useful content")
)) {
JarOutputStream(output).use {
entries.forEach { entry ->
it.putNextEntry(JarEntry(entry.first))
it.write(entry.second.toByteArray())
it.closeEntry()
}
jar.closeEntry()
}
}

View File

@ -5,20 +5,19 @@ import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
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.readFully
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
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.node.services.vault.*
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.nodeapi.internal.withContractsInJar
import java.io.InputStream
import java.security.PublicKey
import java.util.HashMap
import java.util.*
import java.util.jar.Attributes
import java.util.jar.JarInputStream
@ -26,7 +25,7 @@ import java.util.jar.JarInputStream
* A mock implementation of [AttachmentStorage] for use within tests
*/
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 _contractClasses = HashMap<ContractAttachmentMetadata, SecureHash>()
@ -44,27 +43,27 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
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> {
criteria as AttachmentQueryCriteria.AttachmentsQueryCriteria
val contractClassNames =
if (criteria.contractClassNamesCondition is ColumnPredicate.EqualityComparison)
(criteria.contractClassNamesCondition as ColumnPredicate.EqualityComparison<List<ContractClassName>>).rightLiteral
else emptyList()
val contractMetadataList =
if (criteria.isSignedCondition != null) {
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))
}
}
val metadataFilter = { metadata: ContractAttachmentMetadata ->
criteriaFilter(listOf(metadata.name), criteria.contractClassNamesCondition) &&
criteriaFilter(metadata.signers, criteria.signersCondition) &&
criteriaFilter(metadata.isSigned, criteria.isSignedCondition) &&
criteriaFilter(metadata.version, criteria.versionCondition) &&
criteriaFilter(metadata.uploader, criteria.uploaderCondition)
}
return _contractClasses.filterKeys { contractMetadataList.contains(it) }.values.toList()
return _contractClasses.filterKeys { metadataFilter(it) }.values.toList()
}
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
@ -103,7 +102,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
else {
contractClassNames.map {contractClassName ->
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty())
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, version, signers.isNotEmpty(), signers, uploader)
_contractClasses[contractClassMetadata] = sha256
}
ContractAttachment.create(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers, version)