CORDA-2595 - check that all attachments are trusted before loading (#4763)

CORDA-2595 - Fix test and api.

CORDA-2595 add test

CORDA-2595 fix tests

CORDA-2595 fix test and address code review comments

CORDA-2595 address code review comments
This commit is contained in:
Tudor Malene 2019-02-15 17:33:14 +00:00 committed by Tommy Lillehagen
parent 37f9eb31f7
commit 3d362e066c
19 changed files with 105 additions and 52 deletions

View File

@ -1094,7 +1094,7 @@ public static final class net.corda.core.contracts.TransactionVerificationExcept
@NotNull
public final net.corda.core.crypto.SecureHash getAttachmentHash()
@NotNull
public final String getContractClass()
public final String getInvalidClassName()
@NotNull
public final String getPackageName()
##

View File

@ -86,7 +86,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
@Before
fun setup() {
val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }, "test") {
override val id: SecureHash get() = throw UnsupportedOperationException()
}

View File

@ -1,21 +1,19 @@
package net.corda.deterministic.verifier
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.PublicKey
// A valid zip file with 1 entry.
val simpleZip = byteArrayOf(80, 75, 3, 4, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 4, 0, 47, 97, -2, -54, 0, 0, 75, 4, 0, 80, 75, 7, 8, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 80, 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 97, -2, -54, 0, 0, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 52, 0, 0, 0, 55, 0, 0, 0, 0, 0)
@CordaSerializable
class MockContractAttachment(
override val id: SecureHash = SecureHash.zeroHash,
val contract: ContractClassName,
override val signerKeys: List<PublicKey> = emptyList(),
override val signers: List<Party> = emptyList()
) : Attachment {
override fun open(): InputStream = ByteArrayInputStream(id.bytes)
override val size = id.size
}
) : AbstractAttachment({ simpleZip }, "app")

View File

@ -275,11 +275,15 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
* and because attachment classloaders are reused this is independent of any particular transaction.
*/
@CordaSerializable
class PackageOwnershipException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String, val packageName: String) : TransactionVerificationException(txId,
"""The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner of package $packageName specified in the network parameters.
class PackageOwnershipException(txId: SecureHash, val attachmentHash: AttachmentId, val invalidClassName: String, val packageName: String) : TransactionVerificationException(txId,
"""The attachment JAR: $attachmentHash containing the class: $invalidClassName is not signed by the owner of package $packageName specified in the network parameters.
Please check the source of this attachment and if it is malicious contact your zone operator to report this incident.
For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null)
@CordaSerializable
class InvalidAttachmentException(txId: SecureHash, attachmentHash: AttachmentId) : TransactionVerificationException(txId,
"The attachment $attachmentHash is not a valid ZIP or JAR file.".trimIndent(), null)
// 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

@ -19,6 +19,7 @@ import java.util.jar.JarInputStream
const val DEPLOYED_CORDAPP_UPLOADER = "app"
const val RPC_UPLOADER = "rpc"
const val P2P_UPLOADER = "p2p"
const val TESTDSL_UPLOADER = "TestDSL"
const val UNKNOWN_UPLOADER = "unknown"
// We whitelist sources of transaction JARs for now as a temporary state until the DJVM and other security sandboxes
@ -26,12 +27,12 @@ const val UNKNOWN_UPLOADER = "unknown"
// can be removed. Because we ARE downloading attachments over the P2P network in anticipation of this upgrade, we
// track the source of each attachment in our store. TestDSL is used by LedgerDSLInterpreter when custom attachments
// are added in unit test code.
val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, "TestDSL")
val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TESTDSL_UPLOADER)
fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS
@KeepForDJVM
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: String?) : Attachment {
companion object {
/**
* Returns a function that knows how to load an attachment.

View File

@ -148,16 +148,18 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
class FetchAttachmentsFlow(requests: Set<SecureHash>,
otherSide: FlowSession) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
private val uploader = "$P2P_UPLOADER:${otherSideSession.counterparty.name}"
override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid)
override fun convert(wire: ByteArray): Attachment = FetchedAttachment({ wire })
override fun convert(wire: ByteArray): Attachment = FetchedAttachment({ wire }, uploader)
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
for (attachment in downloaded) {
with(serviceHub.attachments) {
if (!hasAttachment(attachment.id)) {
try {
importAttachment(attachment.open(), "$P2P_UPLOADER:${otherSideSession.counterparty.name}", null)
importAttachment(attachment.open(), uploader, null)
} catch (e: FileAlreadyExistsException) {
// This can happen when another transaction will insert the same attachment during this transaction.
// The outcome is the same (the attachment is imported), so we can ignore this exception.
@ -170,14 +172,14 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
}
}
private class FetchedAttachment(dataLoader: () -> ByteArray) : AbstractAttachment(dataLoader), SerializeAsToken {
private class FetchedAttachment(dataLoader: () -> ByteArray, uploader: String?) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
override val id: SecureHash by lazy { attachmentData.sha256() }
private class Token(private val id: SecureHash) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = FetchedAttachment(context.attachmentDataLoader(id))
private class Token(private val id: SecureHash, private val uploader: String?) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = FetchedAttachment(context.attachmentDataLoader(id), uploader)
}
override fun toToken(context: SerializeAsTokenContext) = Token(id)
override fun toToken(context: SerializeAsTokenContext) = Token(id, uploader)
}
}

View File

@ -107,13 +107,47 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
}
init {
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }
.map(ContractAttachment::id)
// Make some preliminary checks to ensure that we're not loading invalid attachments.
// All attachments need to be valid JAR or ZIP files.
for (attachment in attachments) {
if (!isZipOrJar(attachment)) throw TransactionVerificationException.InvalidAttachmentException(sampleTxId, attachment.id)
}
// Until we have a sandbox to run untrusted code we need to make sure that any loaded class file was whitelisted by the node administrator.
val untrusted = attachments
.filter(::containsClasses)
.filterNot { attachment ->
when (attachment) {
is ContractAttachment -> isUploaderTrusted(attachment.uploader)
is AbstractAttachment -> isUploaderTrusted(attachment.uploader)
else -> false // This should not happen on normal code paths.
}
}
.map(Attachment::id)
if (untrusted.isNotEmpty())
throw TransactionVerificationException.UntrustedAttachmentsException(sampleTxId, untrusted)
// Enforce the no-overlap and package ownership rules.
checkAttachments(attachments)
}
private fun isZipOrJar(attachment: Attachment) = attachment.openAsJAR().use { jar ->
jar.nextEntry != null
}
private fun containsClasses(attachment: Attachment): Boolean {
attachment.openAsJAR().use { jar ->
while (true) {
val entry = jar.nextJarEntry ?: return false
if(entry.name.endsWith(".class", ignoreCase = true)) return true
}
}
return false
}
// This function attempts to strike a balance between security and usability when it comes to the no-overlap rule.
// TODO - investigate potential exploits.
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
@ -199,7 +233,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
if (path.endsWith(".class")) {
// Get the package name from the file name. Inner classes separate their names with $ not /
// in file names so they are not a problem.
val pkgName= path
val pkgName = path
.dropLast(".class".length)
.replace('/', '.')
.split('.')
@ -241,7 +275,6 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
}
}
/**
* Required to prevent classes that were excluded from the no-overlap check from being loaded by contract code.
* As it can lead to non-determinism.

View File

@ -115,7 +115,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
// Helper for deprecated toLedgerTransaction
// TODO: revisit once Deterministic JVM code updated
private val missingAttachment: Attachment by lazy {
object : AbstractAttachment({ byteArrayOf() }) {
object : AbstractAttachment({ byteArrayOf() }, DEPLOYED_CORDAPP_UPLOADER ) {
override val id: SecureHash get() = throw UnsupportedOperationException()
}
}

View File

@ -155,7 +155,7 @@ class ResolveTransactionsFlowTest {
}
// TODO: this operation should not require an explicit transaction
val id = megaCorpNode.transaction {
megaCorpNode.services.attachments.importAttachment(makeJar(), "test", null)
megaCorpNode.services.attachments.importAttachment(makeJar(), TESTDSL_UPLOADER, null)
}
val stx2 = makeTransactions(withAttachment = id).second
val p = TestFlow(stx2, megaCorp)

View File

@ -42,7 +42,6 @@ class AttachmentsClassLoaderTests {
private val networkParameters = testNetworkParameters()
private fun make(attachments: List<Attachment>, params: NetworkParameters = networkParameters) = AttachmentsClassLoader(attachments, params, SecureHash.zeroHash)
@Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
assertFailsWith<ClassNotFoundException> {
@ -123,9 +122,9 @@ class AttachmentsClassLoaderTests {
}
@Test
fun `Overlapping rules for META-INF serializationwhitelist files`() {
val att1 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.serializationwhitelist", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.serializationwhitelist", "some other data").inputStream(), "app", "file2.jar")
fun `Overlapping rules for META-INF SerializationWhitelist files`() {
val att1 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some other data").inputStream(), "app", "file2.jar")
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
@ -179,6 +178,20 @@ class AttachmentsClassLoaderTests {
assertArrayEquals("some other data".toByteArray(), data2b)
}
@Test
fun `Allow loading untrusted resource jars but only trusted jars that contain class files`() {
val trustedResourceJar = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file0.jar")
val untrustedResourceJar = importAttachment(fakeAttachment("file2.txt", "some malicious data").inputStream(), "untrusted", "file1.jar")
val untrustedClassJar = importAttachment(fakeAttachment("/com/example/something/MaliciousClass.class", "some malicious data").inputStream(), "untrusted", "file2.jar")
val trustedClassJar = importAttachment(fakeAttachment("/com/example/something/VirtuousClass.class", "some other data").inputStream(), "app", "file3.jar")
make(arrayOf(trustedResourceJar, untrustedResourceJar, trustedClassJar).map { storage.openAttachment(it)!! })
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar).map { storage.openAttachment(it)!! })
}
}
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return jar.use { storage.importAttachment(jar, uploader, filename) }
}

View File

@ -158,13 +158,13 @@ class TransactionBuilderTest {
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
}
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }, "test") {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signerKeys: List<PublicKey> get() = emptyList()
}, DummyContract.PROGRAM_ID)
private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }) {
private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }, "test") {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signerKeys: List<PublicKey> get() = parties.map { it.owningKey }

View File

@ -6,6 +6,8 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.TESTDSL_UPLOADER
import net.corda.core.node.NotaryInfo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
@ -124,7 +126,6 @@ class TransactionTests {
doReturn(SecureHash.zeroHash).whenever(it).id
doReturn(fakeAttachment("nothing", "nada").inputStream()).whenever(it).open()
}, DummyContract.PROGRAM_ID, uploader = "app"))
attachments.first().openAsJAR()
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt = PrivacySalt()
@ -168,8 +169,9 @@ class TransactionTests {
val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0)))
val outputs = listOf(outState)
val commands = emptyList<CommandWithParties<CommandData>>()
val attachments = listOf(object : Attachment {
override fun open(): InputStream = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar").openStream()
val attachments = listOf(object : AbstractAttachment( {
AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar").openStream().readBytes()
}, TESTDSL_UPLOADER) {
@Suppress("OverridingDeprecatedMember")
override val signers: List<Party> = emptyList()
override val signerKeys: List<PublicKey> = emptyList()

View File

@ -231,13 +231,13 @@ object DefaultKryoCustomizer {
val attachment = attachmentStorage.openAttachment(attachmentHash)
?: throw MissingAttachmentsException(listOf(attachmentHash))
attachment.open().readFully()
}) {
}, uploader) {
override val id = attachmentHash
}
return ContractAttachment.create(lazyAttachment, contract, additionalContracts, uploader, signers, version)
} else {
val attachment = GeneratedAttachment(input.readBytesWithLength())
val attachment = GeneratedAttachment(input.readBytesWithLength(), "generated")
val contract = input.readString()
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()

View File

@ -204,18 +204,18 @@ class NodeAttachmentService(
}
}
private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean) : AbstractAttachment(dataLoader), SerializeAsToken {
private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean, uploader: String?) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
override fun open(): InputStream {
val stream = super.open()
// This is just an optional safety check. If it slows things down too much it can be disabled.
return if (checkOnLoad && id is SecureHash.SHA256) HashCheckingStream(id, attachmentData.size, stream) else stream
}
private class Token(private val id: SecureHash, private val checkOnLoad: Boolean) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(id, context.attachmentDataLoader(id), checkOnLoad)
private class Token(private val id: SecureHash, private val checkOnLoad: Boolean, private val uploader: String?) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(id, context.attachmentDataLoader(id), checkOnLoad, uploader)
}
override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad)
override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad, uploader)
}
// slightly complex 2 level approach to attachment caching:
@ -241,7 +241,7 @@ class NodeAttachmentService(
return database.transaction {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
?: return@transaction null
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad, attachment.uploader).let {
val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment.create(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers?.toList()

View File

@ -5,6 +5,6 @@ import net.corda.core.crypto.sha256
import net.corda.core.internal.AbstractAttachment
@KeepForDJVM
class GeneratedAttachment(val bytes: ByteArray) : AbstractAttachment({ bytes }) {
class GeneratedAttachment(val bytes: ByteArray, uploader: String?) : AbstractAttachment({ bytes }, uploader) {
override val id = bytes.sha256()
}

View File

@ -24,7 +24,7 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
} catch (e: Exception) {
throw MissingAttachmentsException(listOf(obj.id))
}
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signerKeys, obj.version)
return ContractAttachmentProxy(GeneratedAttachment(bytes, obj.uploader), obj.contract, obj.additionalContracts, obj.uploader, obj.signerKeys, obj.version)
}
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {

View File

@ -40,7 +40,7 @@ class ContractAttachmentSerializerTest {
@Test
fun `write contract attachment and read it back`() {
val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY), DummyContract.PROGRAM_ID)
val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY, "test"), DummyContract.PROGRAM_ID)
// no token context so will serialize the whole attachment
val serialized = contractAttachment.checkpointSerialize()
val deserialized = serialized.checkpointDeserialize()
@ -53,7 +53,7 @@ class ContractAttachmentSerializerTest {
@Test
fun `write contract attachment and read it back using token context`() {
val attachment = GeneratedAttachment("test".toByteArray())
val attachment = GeneratedAttachment("test".toByteArray(), "test")
mockServices.attachments.importAttachment(attachment.open(), "test", null)
@ -70,7 +70,7 @@ class ContractAttachmentSerializerTest {
@Test
fun `check only serialize attachment id and contract class name when using token context`() {
val largeAttachmentSize = 1024 * 1024
val attachment = GeneratedAttachment(ByteArray(largeAttachmentSize))
val attachment = GeneratedAttachment(ByteArray(largeAttachmentSize), "test")
mockServices.attachments.importAttachment(attachment.open(), "test", null)
@ -82,7 +82,7 @@ class ContractAttachmentSerializerTest {
@Test
fun `throws when missing attachment when using token context`() {
val attachment = GeneratedAttachment("test".toByteArray())
val attachment = GeneratedAttachment("test".toByteArray(), "test")
// don't importAttachment in mockService
@ -95,7 +95,7 @@ class ContractAttachmentSerializerTest {
@Test
fun `check attachment in deserialize is lazy loaded when using token context`() {
val attachment = GeneratedAttachment(EMPTY_BYTE_ARRAY)
val attachment = GeneratedAttachment(EMPTY_BYTE_ARRAY, "test")
// don't importAttachment in mockService
val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID)

View File

@ -1304,7 +1304,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
)
factory2.register(net.corda.serialization.internal.amqp.custom.ContractAttachmentSerializer(factory2))
val obj = ContractAttachment(GeneratedAttachment("test".toByteArray()), DummyContract.PROGRAM_ID)
val obj = ContractAttachment(GeneratedAttachment("test".toByteArray(), "test"), DummyContract.PROGRAM_ID)
val obj2 = serdes(obj, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
assertEquals(obj.id, obj2.attachment.id)
assertEquals(obj.contract, obj2.contract)
@ -1324,7 +1324,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
)
factory2.register(net.corda.serialization.internal.amqp.custom.ContractAttachmentSerializer(factory2))
val obj = ContractAttachment(object : AbstractAttachment({ throw Exception() }) {
val obj = ContractAttachment(object : AbstractAttachment({ throw Exception() }, "test") {
override val id = SecureHash.zeroHash
}, DummyContract.PROGRAM_ID)

View File

@ -87,7 +87,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signerKeys: List<PublicKey>) : AbstractAttachment(dataLoader)
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signerKeys: List<PublicKey>, uploader: String) : AbstractAttachment(dataLoader, uploader)
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
@ -97,7 +97,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
val baseAttachment = MockAttachment({ bytes }, sha256, signers, uploader)
val version = try { Integer.parseInt(baseAttachment.openAsJAR().manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)) } catch (e: Exception) { DEFAULT_CORDAPP_VERSION }
val attachment =
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment