diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 029ab9bb22..a75bc444bf 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -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() ## diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 7f8db6e26f..17e6a0489a 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -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() } diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt index b9f8c1166e..70b5bc2c10 100644 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt @@ -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 = emptyList(), override val signers: List = emptyList() -) : Attachment { - override fun open(): InputStream = ByteArrayInputStream(id.bytes) - override val size = id.size -} +) : AbstractAttachment({ simpleZip }, "app") \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 7b9026e174..3c80993d8d 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 19ae7fef09..cc538e19c0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -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. diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 87ea20d428..ebb64e4efd 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -148,16 +148,18 @@ sealed class FetchDataFlow( class FetchAttachmentsFlow(requests: Set, otherSide: FlowSession) : FetchDataFlow(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) { 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, } } - 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) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index d8d76ed76d..a741d1c570 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -107,13 +107,47 @@ class AttachmentsClassLoader(attachments: List, } 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, 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, } } - /** * 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. diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index eccb432fae..c6b9bdd59e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -115,7 +115,7 @@ class WireTransaction(componentGroups: List, 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() } } diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index 98861f2399..5319c332e9 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -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) diff --git a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt index b2abf37786..eab5df3740 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt @@ -42,7 +42,6 @@ class AttachmentsClassLoaderTests { private val networkParameters = testNetworkParameters() private fun make(attachments: List, params: NetworkParameters = networkParameters) = AttachmentsClassLoader(attachments, params, SecureHash.zeroHash) - @Test fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() { assertFailsWith { @@ -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) } } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index 25cc6ff18e..d4d181a388 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -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 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 get() = parties.map { it.owningKey } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 8bc3a137c2..994c30e925 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -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>() - 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 = emptyList() override val signerKeys: List = emptyList() diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt index dbbf2a4285..ac1c2ec674 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt @@ -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 val uploader = input.readString() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 542dbb5f48..09b8410312 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -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() diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt index 620544181f..1c0684b49d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt @@ -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() } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt index fbab8df68f..2557bf6d6d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt @@ -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 { diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt index e3426a1fd9..0f33ff49bd 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt @@ -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) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index 3adba6ac32..4927961900 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -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) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index d7e0b07b0d..909743aebd 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -87,7 +87,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { fun getAttachmentIdAndBytes(jar: InputStream): Pair = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) } - private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signerKeys: List) : AbstractAttachment(dataLoader) + private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signerKeys: List, uploader: String) : AbstractAttachment(dataLoader, uploader) private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List? = null, attachmentId: AttachmentId? = null, signers: List = 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