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:
Adel El-Beik 2020-04-30 08:57:37 +01:00 committed by GitHub
parent 75d10fe99c
commit 3259b595d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 104 additions and 18 deletions

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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
}
}

Binary file not shown.

Binary file not shown.