mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
[CORDA-2517] Whitelist attachments signed by keys that already sign existing trusted attachments (#5068)
This allows a different signed version of the same CorDapp to be automatically trusted. This reverts "[CORDA-2575] Allow users to whitelist attachments by public key config (#5035)"
This commit is contained in:
parent
c533792f3f
commit
b4e96778bf
@ -1,14 +0,0 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
|
||||
@DeleteForDJVM
|
||||
interface ServicesForResolutionInternal : ServicesForResolution {
|
||||
|
||||
/**
|
||||
* If an attachment is signed with a public key with one of these hashes, it will automatically be trusted.
|
||||
*/
|
||||
val whitelistedKeysForAttachments: Collection<SecureHash>
|
||||
}
|
@ -7,6 +7,9 @@ import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.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
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) }
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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``.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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) {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user