[CORDA-941]: Add NetworkParameters contract implementation whitelist. (#2580)

This commit is contained in:
Michele Sollecito
2018-02-23 14:29:02 +00:00
committed by GitHub
parent 977836f4eb
commit 5be0e4b39e
51 changed files with 641 additions and 287 deletions

View File

@ -49,7 +49,7 @@ class AttachmentLoadingTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments)
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments, testNetworkParameters().whitelistedContractImplementations)
private val cordapp get() = provider.cordapps.first()
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
private val appContext get() = provider.getAppContext(cordapp)

View File

@ -48,7 +48,7 @@ class BFTNotaryServiceTests {
@Before
fun before() {
mockNet = InternalMockNetwork(emptyList())
mockNet = InternalMockNetwork(listOf("net.corda.testing.contracts"))
}
@After

View File

@ -556,7 +556,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations)
val keyManagementService = makeKeyManagementService(identityService, keyPairs)
_services = ServiceHubInternalImpl(
identityService,

View File

@ -13,6 +13,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.sign
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
@ -189,7 +190,7 @@ internal class CordaRPCOpsImpl(
override fun uploadAttachment(jar: InputStream): SecureHash {
// TODO: this operation should not require an explicit transaction
return database.transaction {
services.attachments.importAttachment(jar)
services.attachments.importAttachment(jar, RPC_UPLOADER, null)
}
}

View File

@ -1,10 +1,12 @@
package net.corda.node.internal.cordapp
import com.google.common.collect.HashBiMap
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappConfigProvider
import net.corda.core.internal.createCordappContext
import net.corda.core.node.services.AttachmentId
@ -17,7 +19,10 @@ import java.util.concurrent.ConcurrentHashMap
/**
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa.
*/
open class CordappProviderImpl(private val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal {
open class CordappProviderImpl(private val cordappLoader: CordappLoader,
private val cordappConfigProvider: CordappConfigProvider,
attachmentStorage: AttachmentStorage,
private val whitelistedContractImplementations: Map<String, List<AttachmentId>>) : SingletonSerializeAsToken(), CordappProviderInternal {
companion object {
private val log = loggerFor<CordappProviderImpl>()
@ -25,6 +30,34 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private
private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
/**
* Current known CorDapps loaded on this node
*/
override val cordapps get() = cordappLoader.cordapps
private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage))
init {
verifyInstalledCordapps(attachmentStorage)
}
private fun verifyInstalledCordapps(attachmentStorage: AttachmentStorage) {
if (whitelistedContractImplementations.isEmpty()) {
log.warn("The network parameters don't specify any whitelisted contract implementations. Please contact your zone operator. See https://docs.corda.net/network-map.html")
return
}
// Verify that the installed contract classes correspond with the whitelist hash
// And warn if node is not using latest CorDapp
cordappAttachments.keys.map(attachmentStorage::openAttachment).mapNotNull { it as? ContractAttachment }.forEach { attch ->
(attch.allContracts intersect whitelistedContractImplementations.keys).forEach { contractClassName ->
when {
attch.id !in whitelistedContractImplementations[contractClassName]!! -> log.error("Contract $contractClassName found in attachment ${attch.id} is not whitelisted in the network parameters. If this is a production node contact your zone operator. See https://docs.corda.net/network-map.html")
attch.id != whitelistedContractImplementations[contractClassName]!!.last() -> log.warn("You are not using the latest CorDapp version for contract: $contractClassName. Please contact your zone operator.")
}
}
}
}
override fun getAppContext(): CordappContext {
// TODO: Use better supported APIs in Java 9
@ -42,11 +75,6 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private
return getCordappForClass(contractClassName)?.let(this::getCordappAttachmentId)
}
/**
* Current known CorDapps loaded on this node
*/
override val cordapps get() = cordappLoader.cordapps
private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage))
/**
* Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID
*
@ -55,11 +83,16 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private
*/
fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp.jarPath)
private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map<SecureHash, URL> {
val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath }
val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) } }
return attachmentIds.zip(cordappsWithAttachments).toMap()
}
private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map<SecureHash, URL> =
cordapps.filter { !it.contractClassNames.isEmpty() }.map {
it.jarPath.openStream().use { stream ->
try {
attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
} to it.jarPath
}.toMap()
/**
* Get the current cordapp context for the given CorDapp

View File

@ -8,8 +8,11 @@ import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
@ -24,6 +27,7 @@ import net.corda.node.utilities.NonInvalidatingWeightBasedCache
import net.corda.node.utilities.defaultCordaCacheConcurrencyLevel
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.nodeapi.internal.withContractsInJar
import java.io.*
import java.nio.file.Paths
import java.time.Instant
@ -85,7 +89,14 @@ class NodeAttachmentService(
var uploader: String? = null,
@Column(name = "filename", updatable = false)
var filename: String? = null
var filename: String? = null,
@ElementCollection
@Column(name = "contract_class_name")
@CollectionTable(name = "node_attachments_contract_class_name", joinColumns = arrayOf(
JoinColumn(name = "att_id", referencedColumnName = "att_id")),
foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null
) : Serializable
@VisibleForTesting
@ -196,23 +207,31 @@ class NodeAttachmentService(
// If repeatedly looking for non-existing attachments becomes a performance issue, this is either indicating a
// a problem somewhere else or this needs to be revisited.
private val attachmentContentCache = NonInvalidatingWeightBasedCache<SecureHash, Optional<ByteArray>>(
private val attachmentContentCache = NonInvalidatingWeightBasedCache<SecureHash, Optional<Pair<Attachment, ByteArray>>>(
maxWeight = attachmentContentCacheSize,
concurrencyLevel = defaultCordaCacheConcurrencyLevel,
weigher = object : Weigher<SecureHash, Optional<ByteArray>> {
override fun weigh(key: SecureHash, value: Optional<ByteArray>): Int {
return key.size + if (value.isPresent) value.get().size else 0
weigher = object : Weigher<SecureHash, Optional<Pair<Attachment, ByteArray>>> {
override fun weigh(key: SecureHash, value: Optional<Pair<Attachment, ByteArray>>): Int {
return key.size + if (value.isPresent) value.get().second.size else 0
}
},
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
)
private fun loadAttachmentContent(id: SecureHash): ByteArray? {
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
return attachment?.content
?: return null
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader)
} else {
it
}
}
return Pair(attachmentImpl, attachment.content)
}
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(
attachmentCacheBound,
defaultCordaCacheConcurrencyLevel,
@ -222,16 +241,7 @@ class NodeAttachmentService(
private fun createAttachment(key: SecureHash): Attachment? {
val content = attachmentContentCache.get(key)
if (content.isPresent) {
return AttachmentImpl(
key,
{
attachmentContentCache
.get(key)
.orElseThrow {
IllegalArgumentException("No attachement impl should have been created for non existent content")
}
},
checkAttachmentsOnLoad)
return content.get().first
}
// if no attachement has been found, we don't want to cache that - it might arrive later
attachmentContentCache.invalidate(key)
@ -248,10 +258,10 @@ class NodeAttachmentService(
}
override fun importAttachment(jar: InputStream): AttachmentId {
return import(jar, null, null)
return import(jar, UNKNOWN_UPLOADER, null)
}
override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId {
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return import(jar, uploader, filename)
}
@ -263,47 +273,39 @@ class NodeAttachmentService(
return Pair(id, bytes)
}
override fun hasAttachment(attachmentId: AttachmentId): Boolean {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
criteriaQuery.where(criteriaBuilder.equal(attachments.get<String>(DBAttachment::attId.name), attachmentId.toString()))
return (session.createQuery(criteriaQuery).singleResult > 0)
}
override fun hasAttachment(attachmentId: AttachmentId): Boolean =
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
require(jar !is JarInputStream)
return withContractsInJar(jar) { contractClassNames, inputStream ->
require(inputStream !is JarInputStream)
// Read the file into RAM, hashing it to find the ID as we go. The attachment must fit into memory.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
// To do this we must pipe stream into the database without knowing its hash, which we will learn only once
// the insert/upload is complete. We can then query to see if it's a duplicate and if so, erase, and if not
// set the hash field of the new attachment record.
// Read the file into RAM, hashing it to find the ID as we go. The attachment must fit into memory.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
// To do this we must pipe stream into the database without knowing its hash, which we will learn only once
// the insert/upload is complete. We can then query to see if it's a duplicate and if so, erase, and if not
// set the hash field of the new attachment record.
val (id, bytes) = getAttachmentIdAndBytes(jar)
if (!hasAttachment(id)) {
checkIsAValidJAR(ByteArrayInputStream(bytes))
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
return id
} else {
throw java.nio.file.FileAlreadyExistsException(id.toString())
val (id, bytes) = getAttachmentIdAndBytes(inputStream)
if (!hasAttachment(id)) {
checkIsAValidJAR(ByteArrayInputStream(bytes))
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename, contractClassNames = contractClassNames)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
id
} else {
throw java.nio.file.FileAlreadyExistsException(id.toString())
}
}
}
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
try {
return importAttachment(jar)
}
catch (faee: java.nio.file.FileAlreadyExistsException) {
return AttachmentId.parse(faee.message!!)
}
override fun importOrGetAttachment(jar: InputStream): AttachmentId = try {
importAttachment(jar)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
@ -328,5 +330,4 @@ class NodeAttachmentService(
return results.map { AttachmentId.parse(it.attId) }
}
}

View File

@ -2,14 +2,18 @@ package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import junit.framework.Assert.assertNull
import net.corda.core.internal.cordapp.CordappConfigProvider
import net.corda.core.node.services.AttachmentStorage
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import java.net.URL
class CordappProviderImplTests {
private companion object {
@ -25,6 +29,7 @@ class CordappProviderImplTests {
}
private lateinit var attachmentStore: AttachmentStorage
private val whitelistedContractImplementations = testNetworkParameters().whitelistedContractImplementations
@Before
fun setup() {
@ -33,44 +38,40 @@ class CordappProviderImplTests {
@Test
fun `isolated jar is loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
val provider = newCordappProvider(isolatedJAR)
val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first())
Assert.assertNotNull(maybeAttachmentId)
Assert.assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
assertNotNull(maybeAttachmentId)
assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
}
@Test
fun `empty jar is not loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(emptyJAR))
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
val provider = newCordappProvider(emptyJAR)
assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
}
@Test
fun `test that we find a cordapp class that is loaded into the store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className)
Assert.assertNotNull(actual)
Assert.assertEquals(expected, actual)
assertNotNull(actual)
assertEquals(expected, actual)
}
@Test
fun `test that we find an attachment for a cordapp contract class`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore)
fun `test that we find an attachment for a cordapp contrat class`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)
Assert.assertNotNull(actual)
Assert.assertEquals(actual!!, expected)
assertNotNull(actual)
assertEquals(actual!!, expected)
}
@Test
@ -78,10 +79,15 @@ class CordappProviderImplTests {
val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs.put(isolatedCordappName, validConfig)
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, configProvider, attachmentStore)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore, whitelistedContractImplementations)
val expected = provider.getAppContext(provider.cordapps.first()).config
assertThat(expected.getString("key")).isEqualTo("value")
}
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl {
val loader = CordappLoader.createDevMode(urls.toList())
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore, whitelistedContractImplementations)
}
}