mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
CORDA-3715: Check contract classes hav… (#6155)
* CORDA-3715: When loading cordapps now check that contract classes have class version between 49 and 52 * CORDA-3715: Now check class version when contract verification takes place. * CORDA-3715: Making detekt happy with number of levels in func * CORDA-3715: Make use of new ClassGraph release which provides class file major version number. * CORDA-3715: Changed package name in test jars * CORDA-3715: Use ClassGraph when loading attachments. * CORDA-3715: Reverted file to 4.5 version * CORDA-3715: Updating method to match non deterministic version. * CORDA-3715: Added in default param. * CORDA-3715: Adjusted min JDK version to 1.1 * CORDA-3715: Switching check to JDK 1.2 * CORDA-3715: Now version check SerializationWhitelist classes. * CORDA-3715: Switched default to null for range.
This commit is contained in:
parent
75d10fe99c
commit
3259b595d7
@ -21,7 +21,7 @@ quasarVersion11=0.8.0_r3
|
||||
jdkClassifier11=jdk11
|
||||
proguardVersion=6.1.1
|
||||
bouncycastleVersion=1.60
|
||||
classgraphVersion=4.8.68
|
||||
classgraphVersion=4.8.71
|
||||
disruptorVersion=3.4.2
|
||||
typesafeConfigVersion=1.3.4
|
||||
jsr305Version=3.0.2
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.internal
|
||||
/**
|
||||
* Stubbing out non-deterministic method.
|
||||
*/
|
||||
fun <T: Any> createInstancesOfClassesImplementing(@Suppress("UNUSED_PARAMETER") classloader: ClassLoader, @Suppress("UNUSED_PARAMETER") clazz: Class<T>): Set<T> {
|
||||
fun <T: Any> createInstancesOfClassesImplementing(@Suppress("UNUSED_PARAMETER") classloader: ClassLoader, @Suppress("UNUSED_PARAMETER") clazz: Class<T>,
|
||||
@Suppress("UNUSED_PARAMETER") classVersionRange: IntRange? = null): Set<T> {
|
||||
return emptySet()
|
||||
}
|
@ -324,4 +324,17 @@ class TransactionVerificationExceptionSerialisationTests {
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun unsupportedClassVersionErrorTest() {
|
||||
val cause = UnsupportedClassVersionError("wobble")
|
||||
val exception = TransactionVerificationException.UnsupportedClassVersionError(txid, cause.message!!, cause)
|
||||
val exception2 = DeserializationInput(factory).deserialize(
|
||||
SerializationOutput(factory).serialize(exception, context),
|
||||
context)
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals("java.lang.UnsupportedClassVersionError: ${exception.cause?.message}", exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
}
|
@ -337,6 +337,8 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
class InvalidAttachmentException(txId: SecureHash, @Suppress("unused") val attachmentHash: AttachmentId) : TransactionVerificationException(txId,
|
||||
"The attachment $attachmentHash is not a valid ZIP or JAR file.".trimIndent(), null)
|
||||
|
||||
class UnsupportedClassVersionError(txId: SecureHash, message: String, cause: Throwable) : TransactionVerificationException(txId, message, cause)
|
||||
|
||||
// TODO: Make this descend from TransactionVerificationException so that untrusted attachments cause flows to be hospitalized.
|
||||
/** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */
|
||||
@KeepForDJVM
|
||||
|
@ -9,17 +9,20 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a
|
||||
* Creates instances of all the classes in the classpath of the provided classloader, which implement the interface of the provided class.
|
||||
* @param classloader the classloader, which will be searched for the classes.
|
||||
* @param clazz the class of the interface, which the classes - to be returned - must implement.
|
||||
* @param classVersionRange if specified an exception is raised if class version is not within the passed range.
|
||||
*
|
||||
* @return instances of the identified classes.
|
||||
* @throws IllegalArgumentException if the classes found do not have proper constructors.
|
||||
* @throws UnsupportedClassVersionError if the class version is not within range.
|
||||
*
|
||||
* Note: In order to be instantiated, the associated classes must:
|
||||
* - be non-abstract
|
||||
* - either be a Kotlin object or have a constructor with no parameters (or only optional ones)
|
||||
*/
|
||||
@StubOutForDJVM
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<T> {
|
||||
return getNamesOfClassesImplementing(classloader, clazz)
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<T> {
|
||||
return getNamesOfClassesImplementing(classloader, clazz, classVersionRange)
|
||||
.map { classloader.loadClass(it).asSubclass(clazz) }
|
||||
.mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
@ -28,17 +31,26 @@ fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, claz
|
||||
* Scans for all the non-abstract classes in the classpath of the provided classloader which implement the interface of the provided class.
|
||||
* @param classloader the classloader, which will be searched for the classes.
|
||||
* @param clazz the class of the interface, which the classes - to be returned - must implement.
|
||||
* @param classVersionRange if specified an exception is raised if class version is not within the passed range.
|
||||
*
|
||||
* @return names of the identified classes.
|
||||
* @throws UnsupportedClassVersionError if the class version is not within range.
|
||||
*/
|
||||
@StubOutForDJVM
|
||||
fun <T: Any> getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<String> {
|
||||
fun <T: Any> getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<String> {
|
||||
return ClassGraph().overrideClassLoaders(classloader)
|
||||
.enableURLScheme(attachmentScheme)
|
||||
.ignoreParentClassLoaders()
|
||||
.enableClassInfo()
|
||||
.pooledScan()
|
||||
.use { result ->
|
||||
classVersionRange?.let {
|
||||
result.allClasses.firstOrNull { c -> c.classfileMajorVersion !in classVersionRange }?.also {
|
||||
throw UnsupportedClassVersionError("Class ${it.name} found in ${it.classpathElementURL} " +
|
||||
"has an unsupported class version of ${it.classfileMajorVersion}")
|
||||
}
|
||||
}
|
||||
result.getClassesImplementing(clazz.name)
|
||||
.filterNot(ClassInfo::isAbstract)
|
||||
.mapTo(LinkedHashSet(), ClassInfo::getName)
|
||||
|
@ -636,3 +636,6 @@ fun Logger.warnOnce(warning: String) {
|
||||
this.warn(warning)
|
||||
}
|
||||
}
|
||||
|
||||
const val JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46
|
||||
const val JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION = 52
|
||||
|
@ -323,7 +323,13 @@ object AttachmentsClassLoaderBuilder {
|
||||
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
|
||||
// Create classloader and load serializers, whitelisted classes
|
||||
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent)
|
||||
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
||||
val serializers = try {
|
||||
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
||||
JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
||||
}
|
||||
catch(ex: UnsupportedClassVersionError) {
|
||||
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
||||
}
|
||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||
.flatMap(SerializationWhitelist::whitelist)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import io.github.classgraph.ClassGraph
|
||||
import io.github.classgraph.ClassInfo
|
||||
import io.github.classgraph.ScanResult
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -32,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.jar.Manifest
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.collections.LinkedHashSet
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.streams.toList
|
||||
|
||||
@ -161,8 +163,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
val info = parseCordappInfo(manifest, CordappImpl.jarName(url.url))
|
||||
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
|
||||
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
|
||||
validateContractStateClassVersion(this)
|
||||
validateWhitelistClassVersion(this)
|
||||
return CordappImpl(
|
||||
findContractClassNames(this),
|
||||
findContractClassNamesWithVersionCheck(this),
|
||||
findInitiatedFlows(this),
|
||||
findRPCFlows(this),
|
||||
findServiceFlows(this),
|
||||
@ -283,14 +287,22 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
return scanResult.getAllStandardClasses() + scanResult.getAllInterfaces()
|
||||
}
|
||||
|
||||
private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> {
|
||||
val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
private fun findContractClassNamesWithVersionCheck(scanResult: RestrictedScanResult): List<String> {
|
||||
val contractClasses = coreContractClasses.flatMapTo(LinkedHashSet()) { scanResult.getNamesOfClassesImplementingWithClassVersionCheck(it) }.toList()
|
||||
for (contractClass in contractClasses) {
|
||||
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
|
||||
}
|
||||
return contractClasses
|
||||
}
|
||||
|
||||
private fun validateContractStateClassVersion(scanResult: RestrictedScanResult) {
|
||||
coreContractClasses.forEach { scanResult.versionCheckClassesImplementing(it) }
|
||||
}
|
||||
|
||||
private fun validateWhitelistClassVersion(scanResult: RestrictedScanResult) {
|
||||
scanResult.versionCheckClassesImplementing(SerializationWhitelist::class)
|
||||
}
|
||||
|
||||
private fun findWhitelists(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
|
||||
val whitelists = ServiceLoader.load(SerializationWhitelist::class.java, appClassLoader).toList()
|
||||
return whitelists.filter {
|
||||
@ -299,7 +311,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
|
||||
private fun findSerializers(scanResult: RestrictedScanResult): List<SerializationCustomSerializer<*, *>> {
|
||||
return scanResult.getClassesImplementing(SerializationCustomSerializer::class)
|
||||
return scanResult.getClassesImplementingWithClassVersionCheck(SerializationCustomSerializer::class)
|
||||
}
|
||||
|
||||
private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> {
|
||||
@ -315,7 +327,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
.ignoreParentClassLoaders()
|
||||
.enableAllInfo()
|
||||
.pooledScan()
|
||||
return RestrictedScanResult(scanResult, cordappJarPath.qualifiedNamePrefix)
|
||||
return RestrictedScanResult(scanResult, cordappJarPath.qualifiedNamePrefix, cordappJarPath)
|
||||
}
|
||||
|
||||
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
|
||||
@ -340,9 +352,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
return map { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
|
||||
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) : AutoCloseable {
|
||||
fun getNamesOfClassesImplementing(type: KClass<*>): List<String> {
|
||||
return scanResult.getClassesImplementing(type.java.name).names.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String,
|
||||
private val cordappJarPath: RestrictedURL) : AutoCloseable {
|
||||
|
||||
fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List<String> {
|
||||
return scanResult.getClassesImplementing(type.java.name).filter { it.name.startsWith(qualifiedNamePrefix) }.map {
|
||||
validateClassFileVersion(it)
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
fun versionCheckClassesImplementing(type: KClass<*>) {
|
||||
return scanResult.getClassesImplementing(type.java.name).filter { it.name.startsWith(qualifiedNamePrefix) }.forEach {
|
||||
validateClassFileVersion(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> getClassesWithSuperclass(type: KClass<T>): List<Class<out T>> {
|
||||
@ -354,12 +377,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
.filterNot { it.isAbstractClass }
|
||||
}
|
||||
|
||||
fun <T : Any> getClassesImplementing(type: KClass<T>): List<T> {
|
||||
fun <T : Any> getClassesImplementingWithClassVersionCheck(type: KClass<T>): List<T> {
|
||||
return scanResult
|
||||
.getClassesImplementing(type.java.name)
|
||||
.names
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull { loadClass(it, type) }
|
||||
.filter { it.name.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull {
|
||||
validateClassFileVersion(it)
|
||||
loadClass(it.name, type) }
|
||||
.filterNot { it.isAbstractClass }
|
||||
.map { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
@ -396,6 +420,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
}
|
||||
|
||||
private fun validateClassFileVersion(classInfo: ClassInfo) {
|
||||
if (classInfo.classfileMajorVersion < JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION ||
|
||||
classInfo.classfileMajorVersion > JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
||||
throw IllegalStateException("Class ${classInfo.name} from jar file ${cordappJarPath.url} has an invalid version of " +
|
||||
"${classInfo.classfileMajorVersion}")
|
||||
}
|
||||
|
||||
override fun close() = scanResult.close()
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.internal.JavaVersion
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
@ -9,6 +10,8 @@ import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
import net.corda.core.internal.packageName_
|
||||
import org.junit.Assume
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
@InitiatingFlow
|
||||
class DummyFlow : FlowLogic<Unit>() {
|
||||
@ -175,4 +178,19 @@ class JarScanningCordappLoaderTest {
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader successfully loads app containing only flow classes at java class version 55`() {
|
||||
Assume.assumeTrue(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("/workflowClassAtVersion55.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar))
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class, timeout=300_000)
|
||||
fun `cordapp classloader raises exception when loading contract class at class version 55`() {
|
||||
Assume.assumeTrue(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("/contractClassAtVersion55.jar")!!
|
||||
JarScanningCordappLoader.fromJarUrls(listOf(jar)).cordapps
|
||||
}
|
||||
}
|
||||
|
BIN
node/src/test/resources/contractClassAtVersion55.jar
Normal file
BIN
node/src/test/resources/contractClassAtVersion55.jar
Normal file
Binary file not shown.
BIN
node/src/test/resources/workflowClassAtVersion55.jar
Normal file
BIN
node/src/test/resources/workflowClassAtVersion55.jar
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user