[CORDA-2575] Allow users to whitelist attachments by public key config (#5035)

This commit is contained in:
JamesHR3 2019-04-25 16:55:43 +01:00 committed by Shams Asari
parent 4607b0c151
commit 7a7c471ebf
20 changed files with 180 additions and 33 deletions

View File

@ -0,0 +1,14 @@
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.crypto.SecureHash
import net.corda.core.node.ServicesForResolution
@DeleteForDJVM
interface ServicesForResolutionInternal : ServicesForResolution {
/**
* If an attachment is signed with a public key with one of these hashes, it will automatically be trusted.
*/
val whitelistedKeysForAttachments: Collection<SecureHash>
}

View File

@ -14,6 +14,7 @@ import net.corda.core.serialization.*
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.toSHA256Bytes
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -31,10 +32,13 @@ import java.util.*
* @property sampleTxId The transaction ID that triggered the creation of this classloader. Because classloaders are cached * @property sampleTxId The transaction ID that triggered the creation of this classloader. Because classloaders are cached
* this tx may be stale, that is, classloading might be triggered by the verification of some other transaction * this tx may be stale, that is, classloading might be triggered by the verification of some other transaction
* if not all code is invoked every time, however we want a txid for errors in case of attachment bogusness. * if not all code is invoked every time, however we want a txid for errors in case of attachment bogusness.
* @property whitelistedPublicKeys A collection of public key hashes. An attachment signed by a public key with one of these hashes
* will automatically be trusted.
*/ */
class AttachmentsClassLoader(attachments: List<Attachment>, class AttachmentsClassLoader(attachments: List<Attachment>,
val params: NetworkParameters, val params: NetworkParameters,
private val sampleTxId: SecureHash, private val sampleTxId: SecureHash,
private val whitelistedPublicKeys: Collection<SecureHash>,
parent: ClassLoader = ClassLoader.getSystemClassLoader()) : parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) { URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
@ -118,8 +122,8 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
.filter(::containsClasses) .filter(::containsClasses)
.filterNot { attachment -> .filterNot { attachment ->
when (attachment) { when (attachment) {
is ContractAttachment -> isUploaderTrusted(attachment.uploader) is ContractAttachment -> isUploaderTrusted(attachment.uploader) || attachmentSignedByTrustedKey(attachment)
is AbstractAttachment -> isUploaderTrusted(attachment.uploader) is AbstractAttachment -> isUploaderTrusted(attachment.uploader) || attachmentSignedByTrustedKey(attachment)
else -> false // This should not happen on normal code paths. else -> false // This should not happen on normal code paths.
} }
} }
@ -136,6 +140,10 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
checkAttachments(attachments) checkAttachments(attachments)
} }
private fun attachmentSignedByTrustedKey(attachment: Attachment): Boolean {
return attachment.signerKeys.map { it.hash }.any { whitelistedPublicKeys.contains(it) }
}
private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar -> private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar ->
jar.nextEntry != null jar.nextEntry != null
} }
@ -311,12 +319,17 @@ object AttachmentsClassLoaderBuilder {
* *
* @param txId The transaction ID that triggered this request; it's unused except for error messages and exceptions that can occur during setup. * @param txId The transaction ID that triggered this request; it's unused except for error messages and exceptions that can occur during setup.
*/ */
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, params: NetworkParameters, txId: SecureHash, parent: ClassLoader = ClassLoader.getSystemClassLoader(), block: (ClassLoader) -> T): T { fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>,
params: NetworkParameters,
txId: SecureHash,
whitelistedPublicKeys: Collection<SecureHash>,
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
block: (ClassLoader) -> T): T {
val attachmentIds = attachments.map { it.id }.toSet() val attachmentIds = attachments.map { it.id }.toSet()
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) { val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
// Create classloader and load serializers, whitelisted classes // Create classloader and load serializers, whitelisted classes
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, parent) val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, whitelistedPublicKeys, parent)
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java) val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader) val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
.flatMap { it.whitelist } .flatMap { it.whitelist }

View File

@ -9,6 +9,7 @@ import net.corda.core.crypto.componentHash
import net.corda.core.crypto.computeNonce import net.corda.core.crypto.computeNonce
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.ServicesForResolutionInternal
import net.corda.core.internal.combinedHash import net.corda.core.internal.combinedHash
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
@ -144,8 +145,13 @@ data class ContractUpgradeWireTransaction(
?: throw MissingContractAttachments(emptyList()) ?: throw MissingContractAttachments(emptyList())
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
?: throw MissingContractAttachments(emptyList()) ?: throw MissingContractAttachments(emptyList())
val whitelistedPublicKeys = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(listOf(legacyAttachment, upgradedAttachment), params, id) { transactionClassLoader -> return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
listOf(legacyAttachment, upgradedAttachment),
params,
id,
whitelistedPublicKeys) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize() val resolvedInput = binaryInput.deserialize()
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader) val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment) val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)

View File

@ -75,6 +75,7 @@ private constructor(
private var componentGroups: List<ComponentGroup>? = null private var componentGroups: List<ComponentGroup>? = null
private var serializedInputs: List<SerializedStateAndRef>? = null private var serializedInputs: List<SerializedStateAndRef>? = null
private var serializedReferences: List<SerializedStateAndRef>? = null private var serializedReferences: List<SerializedStateAndRef>? = null
private var whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
init { init {
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" } if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
@ -98,12 +99,14 @@ private constructor(
references: List<StateAndRef<ContractState>>, references: List<StateAndRef<ContractState>>,
componentGroups: List<ComponentGroup>? = null, componentGroups: List<ComponentGroup>? = null,
serializedInputs: List<SerializedStateAndRef>? = null, serializedInputs: List<SerializedStateAndRef>? = null,
serializedReferences: List<SerializedStateAndRef>? = null serializedReferences: List<SerializedStateAndRef>? = null,
whitelistedKeysForAttachments: Collection<SecureHash> = listOf()
): LedgerTransaction { ): LedgerTransaction {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply { return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply {
this.componentGroups = componentGroups this.componentGroups = componentGroups
this.serializedInputs = serializedInputs this.serializedInputs = serializedInputs
this.serializedReferences = serializedReferences this.serializedReferences = serializedReferences
this.whitelistedKeysForAttachments = whitelistedKeysForAttachments
} }
} }
} }
@ -141,7 +144,11 @@ private constructor(
internal fun internalPrepareVerify(extraAttachments: List<Attachment>): Verifier { internal fun internalPrepareVerify(extraAttachments: List<Attachment>): Verifier {
// Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules // Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules
// like no-overlap, package namespace ownership and (in future) deterministic Java. // like no-overlap, package namespace ownership and (in future) deterministic Java.
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments, getParamsWithGoo(), id) { transactionClassLoader -> return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
this.attachments + extraAttachments,
getParamsWithGoo(),
id,
whitelistedPublicKeys = whitelistedKeysForAttachments) { transactionClassLoader ->
// Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader]. // Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader].
// Only the copy will be used for verification, and the outer shell will be discarded. // Only the copy will be used for verification, and the outer shell will be discarded.
// This artifice is required to preserve backwards compatibility. // This artifice is required to preserve backwards compatibility.

View File

@ -99,6 +99,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
@DeleteForDJVM @DeleteForDJVM
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
val whitelistedKeysForAttachments = (services as? ServicesForResolutionInternal)?.whitelistedKeysForAttachments ?: listOf()
return toLedgerTransactionInternal( return toLedgerTransactionInternal(
resolveIdentity = { services.identityService.partyFromKey(it) }, resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.attachments.openAttachment(it) }, resolveAttachment = { services.attachments.openAttachment(it) },
@ -107,7 +108,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val hashToResolve = it ?: services.networkParametersService.defaultHash val hashToResolve = it ?: services.networkParametersService.defaultHash
services.networkParametersService.lookup(hashToResolve) services.networkParametersService.lookup(hashToResolve)
}, },
resolveContractAttachment = { services.loadContractAttachment(it) } resolveContractAttachment = { services.loadContractAttachment(it) },
whitelistedKeys = whitelistedKeysForAttachments
) )
} }
@ -142,11 +144,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ stateRef -> resolveStateRef(stateRef)?.serialize() }, { stateRef -> resolveStateRef(stateRef)?.serialize() },
{ null }, { null },
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1 // Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
{ resolveAttachment(it.txhash) ?: missingAttachment } { resolveAttachment(it.txhash) ?: missingAttachment },
listOf()
) )
} }
// Especially crafted for TransactionVerificationRequest // Especially crafted for TransactionVerificationRequest.
// Note that whitelisted keys do not need to be passed here. The DJVM automatically assumes all attachments are provided by a trusted
// uploader, and so all attachments will be trusted when this is called from the DJVM.
@CordaInternal @CordaInternal
internal fun toLtxDjvmInternalBridge( internal fun toLtxDjvmInternalBridge(
resolveAttachment: (SecureHash) -> Attachment?, resolveAttachment: (SecureHash) -> Attachment?,
@ -158,7 +163,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveAttachment, resolveAttachment,
{ stateRef -> resolveStateRef(stateRef)?.serialize() }, { stateRef -> resolveStateRef(stateRef)?.serialize() },
resolveParameters, resolveParameters,
{ resolveAttachment(it.txhash) ?: missingAttachment } { resolveAttachment(it.txhash) ?: missingAttachment },
listOf()
) )
} }
@ -167,7 +173,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveAttachment: (SecureHash) -> Attachment?, resolveAttachment: (SecureHash) -> Attachment?,
resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?, resolveStateRefAsSerialized: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
resolveParameters: (SecureHash?) -> NetworkParameters?, resolveParameters: (SecureHash?) -> NetworkParameters?,
resolveContractAttachment: (StateRef) -> Attachment resolveContractAttachment: (StateRef) -> Attachment,
whitelistedKeys: Collection<SecureHash>
): LedgerTransaction { ): LedgerTransaction {
// Look up public keys to authenticated identities. // Look up public keys to authenticated identities.
val authenticatedCommands = commands.lazyMapped { cmd, _ -> val authenticatedCommands = commands.lazyMapped { cmd, _ ->
@ -202,7 +209,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolvedReferences, resolvedReferences,
componentGroups, componentGroups,
serializedResolvedInputs, serializedResolvedInputs,
serializedResolvedReferences serializedResolvedReferences,
whitelistedKeysForAttachments = whitelistedKeys
) )
checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences) checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences)

View File

@ -47,7 +47,11 @@ class AttachmentsClassLoaderSerializationTests {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar") val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar") val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash) { classLoader -> val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
testNetworkParameters(),
SecureHash.zeroHash,
listOf()) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader) val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value) assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)

View File

@ -3,12 +3,15 @@ package net.corda.core.transactions
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.declaredField import net.corda.core.internal.declaredField
import net.corda.core.internal.hash
import net.corda.core.internal.inputStream import net.corda.core.internal.inputStream
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.fakeAttachment import net.corda.testing.internal.fakeAttachment
@ -40,7 +43,11 @@ class AttachmentsClassLoaderTests {
private val storage = MockAttachmentStorage() private val storage = MockAttachmentStorage()
private val networkParameters = testNetworkParameters() private val networkParameters = testNetworkParameters()
private fun make(attachments: List<Attachment>, params: NetworkParameters = networkParameters) = AttachmentsClassLoader(attachments, params, SecureHash.zeroHash) private fun make(attachments: List<Attachment>,
params: NetworkParameters = networkParameters,
whitelistedKeys: List<SecureHash> = listOf()): AttachmentsClassLoader {
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, whitelistedPublicKeys = whitelistedKeys)
}
@Test @Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() { fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
@ -192,6 +199,22 @@ class AttachmentsClassLoaderTests {
} }
} }
@Test
fun `Allow loading an untrusted contract jar if signed by a trusted public key`() {
val keyPair = Crypto.generateKeyPair()
val classJar = fakeAttachment("/com/example/something/UntrustedClass.class", "Signed by someone trusted").inputStream()
val attachment = classJar.use { storage.importContractAttachment(listOf("UntrustedClass.class"), "untrusted", classJar, signers = listOf(keyPair.public))}
// Check that without the public key whitelisted, building the AttachmentsClassLoader fails. The AttachmentsClassLoader is responsible
// for checking what attachments are trusted at the point that it is constructed.
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(attachment).map { storage.openAttachment(it)!! })
}
// Check that with the public key whitelisted, the AttachmentsClassLoader can be built (i.e. the attachment trusted check passes)
make(arrayOf(attachment).map { storage.openAttachment(it)!! }, whitelistedKeys = listOf(keyPair.public.hash))
}
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return jar.use { storage.importAttachment(jar, uploader, filename) } return jar.use { storage.importAttachment(jar, uploader, filename) }
} }

View File

@ -4,6 +4,16 @@ Changelog
Here's a summary of what's changed in each Corda release. For guidance on how to upgrade code from the previous Here's a summary of what's changed in each Corda release. For guidance on how to upgrade code from the previous
release, see :doc:`app-upgrade-notes`. release, see :doc:`app-upgrade-notes`.
.. _changelog_v4.2:
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.
.. _changelog_v4.0: .. _changelog_v4.0:
Version 4.0 Version 4.0

View File

@ -545,6 +545,16 @@ verfierType
*Default:* InMemory *Default:* InMemory
whitelistedKeysForAttachments
A list of SHA256 hashes of public keys. Any attachments that are signed by a key that hashes to one of the items in this list will be
treated as trusted by the node, even if it was received by an untrusted source (for example, over the network).
.. note:: In the future, the DJVM will be integrated with Corda and all attachments will be loaded inside a DJVM sandbox. At this point,
all attachments would be considered trusted, and so this configuration option would be ignored.
*Default:* not defined
Reference.conf Reference.conf
-------------- --------------
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``. A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.

View File

@ -14,12 +14,14 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.testing.common.internal.checkNotOnClasspath import net.corda.testing.common.internal.checkNotOnClasspath
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.enclosedCordapp
@ -27,6 +29,7 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test import org.junit.Test
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarInputStream
class AttachmentLoadingTests { class AttachmentLoadingTests {
private companion object { private companion object {
@ -87,6 +90,49 @@ class AttachmentLoadingTests {
} }
} }
@Test
fun `contract is not executed if signing key is not whitelisted and uploader is untrusted`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = listOf(enclosedCordapp()),
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)) {
installIsolatedCordapp(ALICE_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(NodeParameters(providedName = BOB_NAME))
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
.hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name)
}
}
@Test
fun `contract is executed if signing key is whitelisted`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = listOf(enclosedCordapp()),
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)) {
installIsolatedCordapp(ALICE_NAME)
val signingKeys = JarSignatureCollector.collectSigners(JarInputStream(isolatedJar.openStream()))
val bobOverrides = mapOf("whitelistedKeysForAttachments" to signingKeys.map{ it.hash.toString() })
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(NodeParameters(providedName = BOB_NAME).withCustomOverrides(bobOverrides))
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
}
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) { private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories() val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
isolatedJar.toPath().copyToDirectory(cordappsDir) isolatedJar.toPath().copyToDirectory(cordappsDir)

View File

@ -178,7 +178,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@Suppress("LeakingThis") @Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize() val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, configuration.whitelistedKeysForAttachments).also {
attachments.servicesForResolution = it attachments.servicesForResolution = it
} }
@Suppress("LeakingThis") @Suppress("LeakingThis")
@ -959,7 +959,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
} }
inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolutionInternal by servicesForResolution {
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>() override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database)
override val identityService: IdentityService get() = this@AbstractNode.identityService override val identityService: IdentityService get() = this@AbstractNode.identityService

View File

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

View File

@ -107,7 +107,7 @@ class MigrationServicesForResolution(
private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection<Int>): List<TransactionState<ContractState>> { private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection<Int>): List<TransactionState<ContractState>> {
return try { return try {
val attachments = tx.attachments.mapNotNull { attachments.openAttachment(it)} val attachments = tx.attachments.mapNotNull { attachments.openAttachment(it)}
val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(attachments, networkParameters, tx.id, cordappLoader.appClassLoader) { val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(attachments, networkParameters, tx.id, listOf(), cordappLoader.appClassLoader) {
deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true) deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
} }
states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList() states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList()

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import com.typesafe.config.ConfigUtil
import net.corda.common.configuration.parsing.internal.Configuration import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.validation.internal.Validated.Companion.invalid import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.Valid import net.corda.node.services.config.Valid
@ -33,6 +34,8 @@ internal fun toPrincipal(rawValue: String) = attempt<X500Principal, IllegalArgum
internal fun toPath(rawValue: String) = attempt<Path, InvalidPathException> { Paths.get(rawValue) } internal fun toPath(rawValue: String) = attempt<Path, InvalidPathException> { Paths.get(rawValue) }
internal fun toSecureHash(rawValue: String) = attempt<SecureHash, IllegalArgumentException> { SecureHash.parse(rawValue)}
private inline fun <RESULT, reified ERROR : Exception> attempt(action: () -> RESULT, message: (ERROR) -> String): Valid<RESULT> { private inline fun <RESULT, reified ERROR : Exception> attempt(action: () -> RESULT, message: (ERROR) -> String): Valid<RESULT> {
return try { return try {
valid(action.invoke()) valid(action.invoke())

View File

@ -17,14 +17,7 @@ import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.NodeConfigurationImpl.Defaults import net.corda.node.services.config.NodeConfigurationImpl.Defaults
import net.corda.node.services.config.Valid import net.corda.node.services.config.Valid
import net.corda.node.services.config.VerifierType import net.corda.node.services.config.VerifierType
import net.corda.node.services.config.schema.parsers.badValue import net.corda.node.services.config.schema.parsers.*
import net.corda.node.services.config.schema.parsers.toCordaX500Name
import net.corda.node.services.config.schema.parsers.toNetworkHostAndPort
import net.corda.node.services.config.schema.parsers.toPath
import net.corda.node.services.config.schema.parsers.toPrincipal
import net.corda.node.services.config.schema.parsers.toProperties
import net.corda.node.services.config.schema.parsers.toURL
import net.corda.node.services.config.schema.parsers.toUUID
internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfiguration>("NodeConfiguration") { internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfiguration>("NodeConfiguration") {
private val myLegalName by string().mapValid(::toCordaX500Name) private val myLegalName by string().mapValid(::toCordaX500Name)
@ -73,6 +66,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
private val jarDirs by string().list().optional().withDefaultValue(Defaults.jarDirs) private val jarDirs by string().list().optional().withDefaultValue(Defaults.jarDirs)
private val cordappDirectories by string().mapValid(::toPath).list().optional() private val cordappDirectories by string().mapValid(::toPath).list().optional()
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist) private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
private val whitelistedKeysForAttachments by string().mapValid(::toSecureHash).list().optional().withDefaultValue(listOf())
@Suppress("unused") @Suppress("unused")
private val custom by nestedObject().optional() private val custom by nestedObject().optional()
@ -128,7 +122,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
h2port = configuration[h2port], h2port = configuration[h2port],
jarDirs = configuration[jarDirs], jarDirs = configuration[jarDirs],
cordappDirectories = cordappDirectories, cordappDirectories = cordappDirectories,
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist] cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
whitelistedKeysForAttachments = configuration[whitelistedKeysForAttachments]
)) ))
} catch (e: Exception) { } catch (e: Exception) {
return when (e) { return when (e) {

View File

@ -213,7 +213,7 @@ class CordaClassResolverTests {
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() { fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage) val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash) val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, whitelistedPublicKeys = listOf())
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader) val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass) CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
} }
@ -222,7 +222,7 @@ class CordaClassResolverTests {
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() { fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage, "some_uploader") val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash) val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, whitelistedPublicKeys = listOf())
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader) val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass) CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
} }

View File

@ -393,7 +393,7 @@ open class MockServices private constructor(
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
protected val servicesForResolution: ServicesForResolution protected val servicesForResolution: ServicesForResolution
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, listOf())
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() } return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }

View File

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