diff --git a/core/src/main/kotlin/net/corda/core/internal/ServicesForResolutionInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServicesForResolutionInternal.kt deleted file mode 100644 index 8a2c407b46..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/ServicesForResolutionInternal.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 4c23dd5c57..26c4282773 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -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, val contractClasses: List?) + +// A cache for caching whether a particular attachment ID is trusted +private val attachmentTrustedCache: MutableMap = createSimpleCache(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 + } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index 854d21580d..22ae09458f 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -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, val params: NetworkParameters, private val sampleTxId: SecureHash, - private val whitelistedPublicKeys: Collection, + isAttachmentTrusted: (Attachment) -> Boolean, parent: ClassLoader = ClassLoader.getSystemClassLoader()) : URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) { @@ -120,13 +117,7 @@ class AttachmentsClassLoader(attachments: List, // 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, 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 withAttachmentsClassloaderContext(attachments: List, params: NetworkParameters, txId: SecureHash, - whitelistedPublicKeys: Collection, + 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 } diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index de97abfd15..b9ba6c14fd 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -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) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index a3bd7c06e3..5491c8ffd9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -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? = null private var serializedInputs: List? = null private var serializedReferences: List? = null - private var whitelistedKeysForAttachments: Collection = 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? = null, serializedInputs: List? = null, serializedReferences: List? = null, - whitelistedKeysForAttachments: Collection = 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. diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 01539fc27b..ff871c52bb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -99,7 +99,6 @@ class WireTransaction(componentGroups: List, 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, 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, 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, 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, val privacySalt: Pr resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, resolveParameters: (SecureHash?) -> NetworkParameters?, resolveContractAttachment: (StateRef) -> Attachment, - whitelistedKeys: Collection + isAttachmentTrusted: (Attachment) -> Boolean ): LedgerTransaction { // Look up public keys to authenticated identities. val authenticatedCommands = commands.lazyMapped { cmd, _ -> @@ -210,7 +207,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr componentGroups, serializedResolvedInputs, serializedResolvedReferences, - whitelistedKeysForAttachments = whitelistedKeys + isAttachmentTrusted ) checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences) @@ -354,7 +351,8 @@ class WireTransaction(componentGroups: List, 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 diff --git a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt index 8d6d845fdc..007463d280 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt @@ -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("magicString").value) diff --git a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt index 5dbb26aaf8..d1dd10fe08 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt @@ -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, - params: NetworkParameters = networkParameters, - whitelistedKeys: List = 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) } } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 2adc215584..92889e2217 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -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 { buildTransaction().verify() } diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 2a5ed19f0b..cf7d573574 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -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 ` section of the documentation. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 519257cab5..d12e548355 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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: diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 48c67d4daf..a43a0abc07 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -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``. diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 7cfadb6c89..372b45587f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -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) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index cd74679896..d5432bff8e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -178,7 +178,7 @@ abstract class AbstractNode(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(val configuration: NodeConfiguration, } } - inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolutionInternal by servicesForResolution { + inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val identityService: IdentityService get() = this@AbstractNode.identityService diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt index 8ce3baac4e..b8463ee5ec 100644 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt @@ -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 -) : ServicesForResolutionInternal { + private val validatedTransactions: TransactionStorage +) : ServicesForResolution { override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?: throw IllegalArgumentException("No current parameters in network parameters storage") diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt index 8663473ab2..763bbb0e11 100644 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt +++ b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt @@ -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): List> { 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() diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 9d9b697951..672c75533f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -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 val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings - val whitelistedKeysForAttachments: List companion object { // default to at least 8MB and a bit extra for larger heap sizes diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index cf66f0c61b..474c666026 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -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 = Defaults.cordappSignerKeyFingerprintBlacklist, - override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings, - override val whitelistedKeysForAttachments: List = listOf() + override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings ) : NodeConfiguration { internal object Defaults { val jmxMonitoringHttpPort: Int? = null diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/parsers/StandardConfigValueParsers.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/parsers/StandardConfigValueParsers.kt index 7269eebf90..e2c3b2e367 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/parsers/StandardConfigValueParsers.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/parsers/StandardConfigValueParsers.kt @@ -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 { Paths.get(rawValue) } -internal fun toSecureHash(rawValue: String) = attempt { SecureHash.parse(rawValue)} - private inline fun attempt(action: () -> RESULT, message: (ERROR) -> String): Valid { return try { valid(action.invoke()) diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt index 4badb2ba40..6657096da8 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt @@ -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") { private val myLegalName by string().mapValid(::toCordaX500Name) @@ -66,7 +73,6 @@ internal object V1NodeConfigurationSpec : Configuration.Specification 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() { @Suspendable @@ -782,10 +962,13 @@ class NodeAttachmentServiceTest { } private var counter = 0 - private fun makeTestJar(extraEntries: List> = emptyList()): Pair { + private fun makeTestJar(entries: List> = listOf( + Pair("test1.txt", "This is some useful content"), + Pair("test2.txt", "Some more useful content") + )): Pair { counter++ val file = fs.getPath("$counter.jar") - makeTestJar(file.outputStream(), extraEntries) + makeTestJar(file.outputStream(), entries) return Pair(file, file.readAll().sha256()) } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt index a47b6ffae7..c0be0fa947 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt @@ -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) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 4b6a9f9ad2..bda6cbe7c5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -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() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 7f325dbc10..6c5151a795 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -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()).whenever(it).whitelistedKeysForAttachments } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt index 42982e41a4..cefc596c3a 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt @@ -28,19 +28,17 @@ import javax.tools.ToolProvider object ContractJarTestUtils { @JvmOverloads - fun makeTestJar(output: OutputStream, extraEntries: List> = 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> = 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() } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 909743aebd..466cb33766 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -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, val uploader: String) private val _files = HashMap>() private val _contractClasses = HashMap() @@ -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 criteriaFilter(metadata: C, predicate: ColumnPredicate?): 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 { criteria as AttachmentQueryCriteria.AttachmentsQueryCriteria - val contractClassNames = - if (criteria.contractClassNamesCondition is ColumnPredicate.EqualityComparison) - (criteria.contractClassNamesCondition as ColumnPredicate.EqualityComparison>).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)