CORDA-3018 Whitelisting attachments by public key - phase two tooling (#5386)

Allow node operators to blacklist signing keys (using blacklistedAttachmentSigningKeys config option). These blacklisted keys prevent attachments that are received over the network from being trusted. The docs have been updated to detail how to generate the key hashes that the config requires.

A new shell command attachments trustRoots has been added to see what attachments exist on the node along with information about their trust and where it comes from.

run dumpCheckpoints has been replaced by checkpoints dump as InternalCordaRPCOps needed to change to prevent a function that is meant to be internal from being visible on the shell.
This commit is contained in:
Dan Newton 2019-09-10 12:16:34 +01:00 committed by Shams Asari
parent 4fb1787f1e
commit 4cbe22949d
41 changed files with 1685 additions and 586 deletions

View File

@ -4,18 +4,20 @@ import net.corda.core.contracts.Contract
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.declaredField
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.isolated.contracts.DummyContractBackdoor
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals
@ -36,7 +38,8 @@ class AttachmentsClassLoaderSerializationTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val storage = MockAttachmentStorage()
private val storage = InternalMockAttachmentStorage(MockAttachmentStorage())
private val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
@Test
fun `Can serialize and deserialize with an attachment classloader`() {
@ -52,7 +55,7 @@ class AttachmentsClassLoaderSerializationTests {
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
testNetworkParameters(),
SecureHash.zeroHash,
{ isAttachmentTrusted(it, storage) }) { classLoader ->
{ attachmentTrustCalculator.calculate(it) }) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)

View File

@ -5,20 +5,25 @@ import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.AttachmentTrustCalculator
import net.corda.core.internal.declaredField
import net.corda.core.internal.hash
import net.corda.core.internal.inputStream
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.InputStream
@ -40,11 +45,36 @@ class AttachmentsClassLoaderTests {
}
}
private val storage = MockAttachmentStorage()
private lateinit var storage: MockAttachmentStorage
private lateinit var internalStorage: InternalMockAttachmentStorage
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
private val networkParameters = testNetworkParameters()
private fun make(attachments: List<Attachment>,
params: NetworkParameters = networkParameters): AttachmentsClassLoader {
return AttachmentsClassLoader(attachments, params, SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
private val cacheFactory = TestingNamedCacheFactory()
private fun createClassloader(
attachment: AttachmentId,
params: NetworkParameters = networkParameters
): AttachmentsClassLoader {
return createClassloader(listOf(attachment), params)
}
private fun createClassloader(
attachments: List<AttachmentId>,
params: NetworkParameters = networkParameters
): AttachmentsClassLoader {
return AttachmentsClassLoader(
attachments.map { storage.openAttachment(it)!! },
params,
SecureHash.zeroHash,
attachmentTrustCalculator::calculate
)
}
@Before
fun setup() {
storage = MockAttachmentStorage()
internalStorage = InternalMockAttachmentStorage(storage)
attachmentTrustCalculator = NodeAttachmentTrustCalculator(internalStorage, cacheFactory)
}
@Test
@ -58,7 +88,7 @@ class AttachmentsClassLoaderTests {
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val classloader = make(listOf(storage.openAttachment(isolatedId)!!))
val classloader = createClassloader(isolatedId)
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
@ -70,7 +100,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
}
@ -81,7 +111,7 @@ class AttachmentsClassLoaderTests {
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
// does not throw OverlappingAttachments exception
make(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
createClassloader(listOf(isolatedId, isolatedSignedId))
}
@Test
@ -90,7 +120,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
// does not throw OverlappingAttachments exception
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
@Test
@ -98,7 +128,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val cl = make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val cl = createClassloader(listOf(att1, att2))
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
@ -111,7 +141,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "same data", "file2.txt", "same other data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file1.txt", "same data", "file3.txt", "same totally different").inputStream(), "app", "file2.jar")
val cl = make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val cl = createClassloader(listOf(att1, att2))
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("same data", txt)
}
@ -122,7 +152,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
}
@ -131,7 +161,7 @@ class AttachmentsClassLoaderTests {
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)!! })
createClassloader(listOf(att1, att2))
}
@Test
@ -140,7 +170,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(fakeAttachment("meta-inf/services/com.example.something", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
}
@ -150,7 +180,7 @@ class AttachmentsClassLoaderTests {
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
}
@ -161,7 +191,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", ISOLATED_CONTRACTS_JAR_PATH.file)
val att2 = importAttachment(fakeAttachment("net/corda/finance/contracts/isolated/AnotherDummyContract\$State.class", "some attackdata").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
createClassloader(listOf(att1, att2))
}
}
@ -190,10 +220,10 @@ class AttachmentsClassLoaderTests {
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)!! })
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar))
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar).map { storage.openAttachment(it)!! })
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar))
}
}
@ -206,32 +236,28 @@ class AttachmentsClassLoaderTests {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone trusted"
"/com/example/something/TrustedClass.class",
"Signed by someone trusted"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"rpc",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
storage.importContractAttachment(
listOf("TrustedClass.class"),
"rpc",
classJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
createClassloader(untrustedAttachment)
}
@Test
@ -240,32 +266,28 @@ class AttachmentsClassLoaderTests {
val keyPairB = Crypto.generateKeyPair()
val keyPairC = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone trusted"
"/com/example/something/TrustedClass.class",
"Signed by someone trusted"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"rpc",
it,
signers = listOf(keyPairA.public, keyPairC.public)
)
}
storage.importContractAttachment(
listOf("TrustedClass.class"),
"rpc",
classJar,
signers = listOf(keyPairA.public, keyPairC.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
createClassloader(untrustedAttachment)
}
@Test
@ -273,20 +295,18 @@ class AttachmentsClassLoaderTests {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
createClassloader(untrustedAttachment)
}
}
@ -295,33 +315,29 @@ class AttachmentsClassLoaderTests {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
classJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
createClassloader(untrustedAttachment)
}
}
@ -331,47 +347,105 @@ class AttachmentsClassLoaderTests {
val keyPairB = Crypto.generateKeyPair()
val keyPairC = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
"/com/example/something/TrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"app",
it,
signers = listOf(keyPairA.public)
)
}
storage.importContractAttachment(
listOf("TrustedClass.class"),
"app",
classJar,
signers = listOf(keyPairA.public)
)
val inheritedTrustClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone who inherits trust"
"/com/example/something/UntrustedClass.class",
"Signed by someone who inherits trust"
).inputStream()
val inheritedTrustAttachment = inheritedTrustClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairB.public, keyPairA.public)
)
}
val inheritedTrustAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
inheritedTrustClassJar,
signers = listOf(keyPairB.public, keyPairA.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairB.public, keyPairC.public)
)
}
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairB.public, keyPairC.public)
)
// pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment
createClassloader(inheritedTrustAttachment)
make(arrayOf(inheritedTrustAttachment).map { storage.openAttachment(it)!! })
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
createClassloader(untrustedAttachment)
}
}
@Test
fun `Cannot load an untrusted contract jar if it is signed by a blacklisted key even if there is another attachment signed by the same keys that is trusted`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
attachmentTrustCalculator = NodeAttachmentTrustCalculator(
InternalMockAttachmentStorage(storage),
cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash)
)
val classJar = fakeAttachment(
"/com/example/something/TrustedClass.class",
"Signed by someone trusted"
).inputStream()
storage.importContractAttachment(
listOf("TrustedClass.class"),
"rpc",
classJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment)
}
}
@Test
fun `Allow loading a trusted attachment that is signed by a blacklisted key`() {
val keyPairA = Crypto.generateKeyPair()
attachmentTrustCalculator = NodeAttachmentTrustCalculator(
InternalMockAttachmentStorage(storage),
cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash)
)
val classJar = fakeAttachment(
"/com/example/something/TrustedClass.class",
"Signed by someone trusted"
).inputStream()
val trustedAttachment = storage.importContractAttachment(
listOf("TrustedClass.class"),
"rpc",
classJar,
signers = listOf(keyPairA.public)
)
createClassloader(trustedAttachment)
}
}

View File

@ -5,6 +5,7 @@ package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.serialization.MissingAttachmentsException
@ -31,6 +32,12 @@ val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TESTDSL_
fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS
fun Attachment.isUploaderTrusted(): Boolean = when (this) {
is ContractAttachment -> isUploaderTrusted(uploader)
is AbstractAttachment -> isUploaderTrusted(uploader)
else -> false
}
@KeepForDJVM
abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: String?) : Attachment {
companion object {

View File

@ -0,0 +1,43 @@
package net.corda.core.internal
import net.corda.core.contracts.Attachment
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
/**
* Calculates the trust of attachments stored in the node.
*/
interface AttachmentTrustCalculator {
/**
* Establishes whether an attachment should be trusted. This logic is required in order to verify transactions, as transaction
* verification should only be carried out using trusted attachments.
*
* Attachments are trusted if one of the following is true:
* - They are uploaded by a trusted uploader
* - There is another attachment in the attachment store, that is trusted and is signed by at least one key that the given
* attachment is also signed with
*/
fun calculate(attachment: Attachment): Boolean
/**
* Calculates the trust of attachments stored within the node. Applies the same logic as
* [calculate] when calculating the trust of an attachment.
*/
fun calculateAllTrustInfo(): List<AttachmentTrustInfo>
}
/**
* Data class containing information about an attachment's trust root.
*/
@CordaSerializable
data class AttachmentTrustInfo(
val attachmentId: AttachmentId,
val fileName: String?,
val uploader: String?,
val trustRootId: AttachmentId?,
val trustRootFileName: String?
) {
val isTrusted: Boolean get() = trustRootId != null
val isTrustRoot: Boolean get() = attachmentId == trustRootId
}

View File

@ -1,11 +1,16 @@
package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.DeleteForDJVM
import net.corda.core.node.ServiceHub
import net.corda.core.node.StatesToRecord
// TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal.
@DeleteForDJVM
interface ServiceHubCoreInternal : ServiceHub {
val attachmentTrustCalculator: AttachmentTrustCalculator
fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver
}

View File

@ -7,9 +7,6 @@ import net.corda.core.crypto.componentHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.Builder
import net.corda.core.serialization.*
import net.corda.core.transactions.*
import net.corda.core.utilities.OpaqueBytes
@ -184,42 +181,6 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
// For now we don't check whether the attached network parameters match the current ones.
}
// A cache for caching whether a particular set of signers are trusted
private val trustedKeysCache: MutableMap<PublicKey, Boolean> =
createSimpleCache<PublicKey, Boolean>(100).toSynchronised()
/**
* Establishes whether an attachment should be trusted. This logic is required in order to verify transactions, as transaction
* verification should only be carried out using trusted attachments.
*
* Attachments are trusted if one of the following is true:
* - They are uploaded by a trusted uploader
* - There is another attachment in the attachment store, that is trusted and is signed by at least one key that the input
* attachment is also signed with
*/
fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Boolean {
val trustedByUploader = when (attachment) {
is ContractAttachment -> isUploaderTrusted(attachment.uploader)
is AbstractAttachment -> isUploaderTrusted(attachment.uploader)
else -> false
}
if (trustedByUploader) return true
return if (service != null && attachment.signerKeys.isNotEmpty()) {
attachment.signerKeys.any { signer ->
trustedKeysCache.computeIfAbsent(signer) {
val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(signer)),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
)
service.queryAttachments(queryCriteria).isNotEmpty()
}
}
} else {
false
}
}
val SignedTransaction.dependencies: Set<SecureHash>
get() = (inputs.asSequence() + references.asSequence()).map { it.txhash }.toSet()

View File

@ -1,5 +1,6 @@
package net.corda.core.internal.messaging
import net.corda.core.internal.AttachmentTrustInfo
import net.corda.core.messaging.CordaRPCOps
/**
@ -9,4 +10,7 @@ interface InternalCordaRPCOps : CordaRPCOps {
/** Dump all the current flow checkpoints as JSON into a zip file in the node's log directory. */
fun dumpCheckpoints()
/** Get all attachment trust information */
val attachmentTrustInfos: List<AttachmentTrustInfo>
}

View File

@ -9,8 +9,8 @@ import net.corda.core.crypto.componentHash
import net.corda.core.crypto.computeNonce
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.ServiceHubCoreInternal
import net.corda.core.internal.combinedHash
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
@ -153,7 +153,7 @@ data class ContractUpgradeWireTransaction(
listOf(legacyAttachment, upgradedAttachment),
params,
id,
{ isAttachmentTrusted(it, services.attachments) }) { transactionClassLoader ->
{ (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) }) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize()
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)

View File

@ -74,7 +74,7 @@ private constructor(
private var componentGroups: List<ComponentGroup>? = null
private var serializedInputs: List<SerializedStateAndRef>? = null
private var serializedReferences: List<SerializedStateAndRef>? = null
private var isAttachmentTrusted: (Attachment) -> Boolean = { isAttachmentTrusted(it, null) }
private var isAttachmentTrusted: (Attachment) -> Boolean = { it.isUploaderTrusted() }
init {
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }

View File

@ -108,7 +108,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
services.networkParametersService.lookup(hashToResolve)
},
resolveContractAttachment = { services.loadContractAttachment(it) },
isAttachmentTrusted = { isAttachmentTrusted(it, services.attachments) }
// `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal]
isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true }
)
}
@ -144,7 +145,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ null },
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
{ resolveAttachment(it.txhash) ?: missingAttachment },
{ isAttachmentTrusted(it, null) }
{ it.isUploaderTrusted() }
)
}

View File

@ -91,6 +91,8 @@ implemented. They make it harder to upgrade applications than when using signatu
Further information into the design of Signature Constraints can be found in its :doc:`design document <design/data-model-upgrades/signature-constraints>`.
.. _signing_cordapps_for_use_with_signature_constraints:
Signing CorDapps for use with Signature Constraints
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -101,13 +103,47 @@ across the nodes that intend to use it.
Each transaction received by a node will then verify that the apps attached to it have the correct signers as specified by its
Signature Constraints. This ensures that the version of each app is acceptable to the transaction's input states.
If a node receives a transaction that uses a contract attachment that it doesn't trust, but there is an attachment present on the node with
the same contract classes and same signatures, then the node will execute that contract's code as if it were trusted. This means that nodes
are no longer required to have every version of a CorDapp uploaded to them in order to verify transactions running older version of a CorDapp.
If a node receives a transaction that uses an attachment that it doesn't trust, but there is another attachment present on the node with
at least one common signature, then the node will trust the received attachment. This means that nodes
are no longer required to have every version of a CorDapp uploaded to them in order to verify transactions running older versions of a CorDapp.
Instead, it is sufficient to have any version of the CorDapp contract installed.
For third party dependencies attached to the transaction, the rule is slightly different. In this case, the attachment will be trusted by the
node provided there is another trusted attachment in the node's attachment store that has been signed with the same keys.
.. note:: An attachment is considered trusted if it was manually installed or uploaded via RPC.
Signers can also be blacklisted to prevent attachments received from a peer from being loaded and used in processing transactions. Only a
single signer of an attachment needs to be blacklisted for an attachment to be considered untrusted. CorDapps
and other attachments installed on a node can still be used without issue, even if they are signed by a blacklisted key. Only attachments
received from a peer are affected.
Below are two examples of possible scenarios around blacklisting signing keys:
- The statements below are true for both examples:
- ``Alice`` has ``Contracts CorDapp`` installed
- ``Bob`` has an upgraded version of ``Contracts CorDapp`` (known as ``Contracts CorDapp V2``) installed
- Both ``Alice`` and ``Bob`` have the ``Workflows CorDapp`` allowing them to transact with each other
- ``Contracts CorDapp`` is signed by both ``Alice`` and ``Bob``
- ``Contracts CorDapp V2`` is signed by both ``Alice`` and ``Bob``
- Example 1:
- ``Alice`` has not blacklisted any attachment signing keys
- ``Bob`` transacts with ``Alice``
- ``Alice`` receives ``Contracts CorDapp V2`` and stores it
- When verifying the attachments loaded into the contract verification code, ``Contracts CorDapp V2`` is accepted and used
- The contract verification code in ``Contracts CorDapp V2`` is run
- Example 2:
- ``Alice`` blacklists ``Bob``'s attachment signing key
- ``Bob`` transacts with ``Alice``
- ``Alice`` receives ``Contracts CorDapp V2`` and stores it
- When verifying the attachments loaded in the contract verification code, ``Contracts CorDapp V2`` is declined because it is signed
by ``Bob``'s blacklisted key
- The contract verification code in ``Contracts CorDapp V2`` is not run and the transaction fails
Information on blacklisting attachment signing keys can be found in the
:ref:`node configuration documentation <corda_configuration_file_blacklisted_attachment_signer_keys>`.
More information on how to sign an app directly from Gradle can be found in the
:ref:`CorDapp Jar signing <cordapp_build_system_signing_cordapp_jar_ref>` section of the documentation.

View File

@ -12,7 +12,7 @@ Unreleased
* Introduced a new API on ``KeyManagementService`` which facilitates lookups of ``PublicKey`` s to ``externalId`` s (Account IDs).
* Introduced a new low level flow diagnostics tool: checkpoint agent (that can be used standalone or in conjunction with the ``dumpCheckpoints`` shell command).
* Introduced a new low level flow diagnostics tool: checkpoint agent (that can be used standalone or in conjunction with the ``checkpoints dump`` shell command).
See :doc:`checkpoint-tooling` for more information.
* The MockNet now supports setting a custom Notary class name, as was already supported by normal node config. See :doc:`tutorial-custom-notary`.
@ -25,7 +25,7 @@ Unreleased
into IRS Demo.
* The introductory and technical white papers have been refreshed. They have new content and a clearer organisation.
* Information about checkpointed flows can be retrieved from the shell. Calling ``dumpCheckpoints`` will create a zip file inside the node's
* Information about checkpointed flows can be retrieved from the shell. Calling ``checkpoints dump`` will create a zip file inside the node's
``log`` directory. This zip will contain a JSON representation of each checkpointed flow. This information can then be used to determine the
state of stuck flows or flows that experienced internal errors and were kept in the node for manual intervention.

View File

@ -28,11 +28,11 @@ Checkpoint dumper
The checkpoint dumper outputs information about flows running on a node. This is useful for diagnosing the causes of stuck flows. Using the generated output,
corrective actions can be taken to resolve the issues flows are facing. One possible solution, is ending a flow using the ``flow kill`` command.
.. warning:: Deleting checkpoints manually or via ``flow kill``/```killFlow`` can lead to an inconsistent ledger among transacting parties. Great care
.. warning:: Deleting checkpoints manually or via ``flow kill``/``killFlow`` can lead to an inconsistent ledger among transacting parties. Great care
and coordination with a flow's counterparties must be taken to ensure that a initiating flow and flows responding to it are correctly
removed. This experience will be improved in the future. Making it easier to kill flows while notifying their counterparties.
To retrieve this information, execute ``run dumpCheckpoints`` in the node's shell. The command creates a zip and generates a JSON file for each flow.
To retrieve this information, execute ``checkpoints dump`` in the node's shell. The command creates a zip and generates a JSON file for each flow.
- Each file follows the naming format ``<flow name>-<flow id>.json`` (for example, ``CashIssueAndPaymentFlow-90613d6f-be78-41bd-98e1-33a756c28808.json``).
- The zip is placed into the ``logs`` directory of the node and is named ``checkpoints_dump-<date and time>.zip`` (for example, ``checkpoints_dump-20190812-153847``).
@ -158,7 +158,7 @@ For a given flow *checkpoint*, the agent outputs:
Diagnostics information is written to standard log files (eg. log4j2 configured logger).
This tool is particularly useful when used in conjunction with the ``dumpCheckpoints`` CRaSH shell command to troubleshoot and identify potential
This tool is particularly useful when used in conjunction with the ``checkpoints dump`` CRaSH shell command to troubleshoot and identify potential
problems associated with checkpoints for flows that appear to not be completing.
The checkpoint agent can be downloaded from `here <https://software.r3.com/artifactory/corda-releases/net/corda/corda-tools-checkpoint-agent/>`_.
@ -199,12 +199,12 @@ These arguments are passed to the JVM along with the agent specification. For ex
Checkpoint Dump support
-----------------------
When used in combination with the ``dumpCheckpoints`` shell command (see :ref:`Checkpoint Dumper <checkpoint_dumper>`),
When used in combination with the ``checkpoints dump`` shell command (see :ref:`Checkpoint Dumper <checkpoint_dumper>`),
the checkpoint agent will automatically output additional diagnostic information for all checkpoints dumped by the aforementioned tool.
You should therefore see two different output files upon invoking the checkpoint dumper command:
* ``<NODE_BASE>\logs\checkpoints_dump-<date>.zip`` contains zipped JSON representation of checkpoints (from ``dumpCheckpoints`` shell command)
* ``<NODE_BASE>\logs\checkpoints_dump-<date>.zip`` contains zipped JSON representation of checkpoints (from ``checkpoints dump`` shell command)
* ``<NODE_BASE>\logs\checkpoints_agent-<date>.log`` contains output from this agent tool (types and sizes of a checkpoint stack)
.. note:: You will only see a separate `checkpoints_agent-<date>.log` file if you configure a separate log4j logger as described below.
@ -216,7 +216,7 @@ If you **only** wish to log checkpoint data for failing flows, start the checkpo
checkpoint-agent.jar=instrumentType=read,instrumentClassname=NONE
and use the ``dumpCheckpoints`` shell command to trigger diagnostics collection.
and use the ``checkpoints dump`` shell command to trigger diagnostics collection.
.. warning:: The checkpoint agent JAR file must be called "checkpoint-agent.jar" as the checkpoint dump support code uses Java reflection to
determine whether the VM has been instrumented or not at runtime.
@ -309,7 +309,7 @@ Note,
running on and its checkpoint id (43c7d5c8-aa66-4a98-beed-dc91354d0353)
* on READ (eg. a checkpoint is being deserialized from disk), we only have information about the stack class hierarchy.
Additionally, if we are using the CRaSH shell ``dumpCheckpoints`` command, we also see a flows checkpoint id.
Additionally, if we are using the CRaSH shell ``checkpoints dump`` command, we also see a flows checkpoint id.
Flow diagnostic process
~~~~~~~~~~~~~~~~~~~~~~~
@ -339,14 +339,14 @@ Note that "In progress" indicates the flows above have not completed (and will h
.. note:: Always search for the flow id, in this case **90613d6f-be78-41bd-98e1-33a756c28808**
2. From the CRaSH shell run the ``dumpCheckpoints`` command to trigger diagnostics information.
2. From the CRaSH shell run the ``checkpoints dump`` command to trigger diagnostics information.
.. sourcecode:: none
Welcome to the Corda interactive shell.
Useful commands include 'help' to see what is available, and 'bye' to shut down the node.
Thu Jul 11 18:56:48 BST 2019>>> run dumpCheckpoints
Thu Jul 11 18:56:48 BST 2019>>> checkpoints dump
You will now see an addition line in the main corda node log file as follows:

View File

@ -69,6 +69,19 @@ attachmentCacheBound
*Default:* 1024
.. _corda_configuration_file_blacklisted_attachment_signer_keys:
blacklistedAttachmentSigningKeys
List of SHA-256 hashes of public keys. Attachments signed by any of these public keys will not be considered as trust roots for any attachments received over the network.
This property is similar to :ref:`cordappSignerKeyFingerprintBlacklist <corda_configuration_file_signer_blacklist>` but only restricts CorDapps that were
included as attachments in a transaction and received over the network from a peer.
See :ref:`Signing CorDapps for use with Signature Constraints <signing_cordapps_for_use_with_signature_constraints>` for more information about signing CorDapps and what
makes an attachment trusted (a trust root).
This property requires retrieving the hashes of public keys that need to be blacklisted. More information on this process can be found in :ref:`Generating a public key hash <generating_a_public_key_hash>`.
*Default:* not defined
compatibilityZoneURL (deprecated)
The root address of the Corda compatibility zone network management services, it is used by the Corda node to register with the network and obtain a Corda node certificate, (See :doc:`permissioning` for more information.) and also is used by the node to obtain network map information.
@ -85,6 +98,8 @@ cordappSignerKeyFingerprintBlacklist
The node will not load Cordapps signed by those keys.
The option takes effect only in production mode and defaults to Corda development keys (``["56CA54E803CB87C8472EBD3FBC6A2F1876E814CEEBF74860BD46997F40729367", "83088052AF16700457AE2C978A7D8AC38DD6A7C713539D00B897CD03A5E5D31D"]``), in development mode any key is allowed to sign Cordpapp JARs.
This property requires retrieving the hashes of public keys that need to be blacklisted. More information on this process can be found in :ref:`Generating a public key hash <generating_a_public_key_hash>`.
*Default:* not defined
crlCheckSoftFail
@ -614,4 +629,23 @@ Configuring a node where the Corda Compatibility Zone's registration and Network
.. literalinclude:: example-code/src/main/resources/example-node-with-networkservices.conf
:language: none
.. _generating_a_public_key_hash:
Generating a public key hash
----------------------
This section details how a public key hash can be extracted and generated from a signed CorDapp. This is required for a select number of
configuration properties.
Below are the steps to generate a hash for a CorDapp signed with a RSA certificate. A similar process should work for other certificate types.
- Extract the contents of the signed CorDapp jar.
- Run the following command (replacing the < > variables):
.. code-block:: none
openssl pkcs7 -in <extract_signed_jar_directory>/META-INF/<signature_to_hash>.RSA -print_certs -inform DER -outform DER \
| openssl x509 -pubkey -noout \
| openssl rsa -pubin -outform der | openssl dgst -sha256
- Copy the public key hash that is generated and place it into the required location (e.g. in ``node.conf``).

View File

@ -286,3 +286,48 @@ Here is a sample output displayed by the ``run nodeDiagnosticInfo`` command exec
vendor: "R3"
licence: "Open Source (Apache 2)"
jarHash: "6EA4E0B36010F1DD27B5677F3686B4713BA40C316804A4188DCA20F477FDB23F"
Managing trusted attachments
----------------------------
The node comes equipped with tools to manage attachments, including tooling to examine installed and uploaded attachments as well as those
that were received over the network.
.. note:: A Contract CorDapp (an attachment) received over the network, is only allowed to be evaluated if there are other Contract
CorDapps installed in the node that have been signed by at least one of the received CorDapp's keys.
See :ref:`Signature Constraints <signature_constraints>` and
:ref:`Signing CorDapps for use with Signature Constraints <signing_cordapps_for_use_with_signature_constraints>` for more information
Shell commands
++++++++++++++
The following shell command can be used to extract information about attachments from the node:
- ``attachments trustInfo``
Outputs to the shell a list of all attachments along with the following information:
- Whether an attachment is installed locally
- ``True`` if the attachment is installed in the CorDapps directory or uploaded via RPC
- ``False`` in all other scenarios, including attachments received from a peer node or uploaded via any means other than RPC
- If an attachment is trusted
- Which other attachment, if any, provided trust to an attachment
Below is an example out the command's output:
.. code-block:: none
Name Attachment ID Installed Trusted Trust Root
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
net.corda.dummy-cordapp-contracts-states 654CDFD0F195269B1C839DD9D539592B4DE7DD09BF29A3762EF600F94AE45E18 true true net.corda.dummy-cordapp-contracts-states
Corda Finance Demo 71154836EBE54C0A60C6C5D9513EE015DB722EED57034B34428C72459CF133D7 true true Corda Finance Demo
Received from: O=PartyA, L=London, C=GB CDDDD9A5C97DBF839445FFD79F604078D9D9766D178F698780EA4F9EA7A02D5F false true net.corda.dummy-cordapp-contracts-states
.. note:: The ``Name`` column will be empty if the attachment has been stored without a name. ``Trust Root`` will also display an attachment
hash if there is no name to display.
The output above shows that two CorDapps have been installed locally and are therefore trusted. The 3rd record is an attachment received
from another node, hence the ``Name`` field containing ``Received from: O=PartyA, L=London, C=GB``. The CorDapp is also trusted as another
CorDapp has been signed by a common key, the ``Trust Root`` field is filled in to highlight this.

View File

@ -281,7 +281,7 @@ a drain is complete there should be no outstanding checkpoints or running flows.
A node can be drained or undrained via RPC using the ``setFlowsDrainingModeEnabled`` method, and via the shell using
the standard ``run`` command to invoke the RPC. See :doc:`shell` to learn more.
To assist in draining a node, the ``dumpCheckpoints`` shell command will output JSON representations of each checkpointed flow.
To assist in draining a node, the ``checkpoints dump`` shell command will output JSON representations of each checkpointed flow.
A zip containing the JSON files is created in the ``logs`` directory of the node. This information can then be used to determine the
state of stuck flows or flows that experienced internal errors and were kept in the node for manual intervention. To drain these flows,
the node will need to be restarted or the flow will need to be removed using ``killFlow``.

View File

@ -46,6 +46,7 @@ import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.api.*
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.config.rpc.NodeRpcOptions
@ -174,7 +175,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
@Suppress("LeakingThis")
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database, configuration.devMode).tokenize()
val attachments = NodeAttachmentService(
metricRegistry,
cacheFactory,
database,
configuration.devMode
).tokenize()
val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database)
val cryptoService = CryptoServiceFactory.makeCryptoService(
SupportedCryptoServices.BC_SIMPLE,
configuration.myLegalName,
@ -568,14 +575,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
generatedCordapps += notaryImpl
}
val blacklistedKeys = if (configuration.devMode) emptyList()
else configuration.cordappSignerKeyFingerprintBlacklist.map {
try {
SecureHash.parse(it)
} catch (e: IllegalArgumentException) {
log.error("Error while adding key fingerprint $it to cordappSignerKeyFingerprintBlacklist due to ${e.message}", e)
throw e
}
val blacklistedKeys = if (configuration.devMode) {
emptyList()
} else {
parseSecureHashConfiguration(configuration.cordappSignerKeyFingerprintBlacklist) { "Error while adding key fingerprint $it to blacklistedAttachmentSigningKeys" }
}
return JarScanningCordappLoader.fromDirectories(
configuration.cordappDirectories,
@ -585,6 +588,31 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
)
}
private fun parseSecureHashConfiguration(unparsedConfig: List<String>, errorMessage: (String) -> String): List<SecureHash.SHA256> {
return unparsedConfig.map {
try {
SecureHash.parse(it)
} catch (e: IllegalArgumentException) {
log.error("${errorMessage(it)} due to - ${e.message}", e)
throw e
}
}
}
private fun makeAttachmentTrustCalculator(
configuration: NodeConfiguration,
database: CordaPersistence
): AttachmentTrustCalculator {
val blacklistedAttachmentSigningKeys: List<SecureHash> =
parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" }
return NodeAttachmentTrustCalculator(
attachmentStorage = attachments,
database = database,
cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys
).tokenize()
}
private fun isRunningSimpleNotaryService(configuration: NodeConfiguration): Boolean {
return configuration.notary != null && configuration.notary?.className == SimpleNotaryService::class.java.name
}
@ -1010,6 +1038,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater
override val cacheFactory: NamedCacheFactory get() = this@AbstractNode.cacheFactory
override val networkParametersService: NetworkParametersStorage get() = this@AbstractNode.networkParametersStorage
override val attachmentTrustCalculator: AttachmentTrustCalculator get() = this@AbstractNode.attachmentTrustCalculator
private lateinit var _myInfo: NodeInfo
override val myInfo: NodeInfo get() = _myInfo

View File

@ -17,11 +17,8 @@ import net.corda.core.flows.StateMachineRunId
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.STRUCTURAL_STEP_PREFIX
import net.corda.core.internal.*
import net.corda.core.internal.messaging.InternalCordaRPCOps
import net.corda.core.internal.sign
import net.corda.core.messaging.*
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeDiagnosticInfo
@ -139,6 +136,11 @@ internal class CordaRPCOpsImpl(
override fun dumpCheckpoints() = checkpointDumper.dump()
override val attachmentTrustInfos: List<AttachmentTrustInfo>
get() {
return services.attachmentTrustCalculator.calculateAllTrustInfo()
}
override fun stateMachinesSnapshot(): List<StateMachineInfo> {
val (snapshot, updates) = stateMachinesFeed()
updates.notUsed()

View File

@ -12,8 +12,8 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.cordapp.CordappLoader
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
@ -70,20 +70,29 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse()[cordapp.jarPath]
private fun loadContractsIntoAttachmentStore(): Map<SecureHash, URL> =
cordapps.filter { !it.contractClassNames.isEmpty() }.map {
it.jarPath.openStream().use { stream ->
cordapps.filter { it.contractClassNames.isNotEmpty() }.map { cordapp ->
cordapp.jarPath.openStream().use { stream ->
try {
// We can't make attachmentStorage a AttachmentStorageInternal as that ends up requiring
// MockAttachmentStorage to implement it.
// This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage]
// [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because
// doing so results in internal functions being exposed in the public API.
if (attachmentStorage is AttachmentStorageInternal) {
attachmentStorage.privilegedImportAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
attachmentStorage.privilegedImportAttachment(
stream,
DEPLOYED_CORDAPP_UPLOADER,
cordapp.info.shortName
)
} else {
attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
attachmentStorage.importAttachment(
stream,
DEPLOYED_CORDAPP_UPLOADER,
cordapp.info.shortName
)
}
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
} to it.jarPath
} to cordapp.jarPath
}.toMap()
/**

View File

@ -35,6 +35,7 @@ class MigrationNamedCacheFactory(private val metricRegistry: MetricRegistry?,
"NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(defaultCacheSize)
"NodeAttachmentService_contractAttachmentVersions" -> caffeine.maximumSize(defaultCacheSize)
"NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize)
"NodeAttachmentTrustCalculator_trustedKeysCache" -> caffeine.maximumSize(defaultCacheSize)
else -> throw IllegalArgumentException("Unexpected cache name $name.")
}
}

View File

@ -6,11 +6,13 @@ import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.deserialiseComponentGroup
import net.corda.core.internal.div
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.internal.readObject
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.*
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersService
import net.corda.core.node.services.TransactionStorage
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
@ -18,6 +20,8 @@ import net.corda.core.transactions.NotaryChangeLedgerTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.contextLogger
import net.corda.node.internal.DBNetworkParametersStorage
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -30,7 +34,7 @@ import java.util.Comparator.comparingInt
class MigrationServicesForResolution(
override val identityService: IdentityService,
override val attachments: AttachmentStorage,
override val attachments: AttachmentStorageInternal,
private val transactions: TransactionStorage,
private val cordaDB: CordaPersistence,
cacheFactory: MigrationNamedCacheFactory
@ -54,6 +58,11 @@ class MigrationServicesForResolution(
}
private val cordappLoader = SchemaMigration.loader.get()
private val attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachments,
cacheFactory
)
private fun defaultNetworkParameters(): NetworkParameters {
logger.warn("Using a dummy set of network parameters for migration.")
val clock = Clock.systemUTC()
@ -115,7 +124,7 @@ class MigrationServicesForResolution(
txAttachments,
networkParameters,
tx.id,
{ isAttachmentTrusted(it, attachments) },
attachmentTrustCalculator::calculate,
cordappLoader.appClassLoader) {
deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
}

View File

@ -0,0 +1,150 @@
package net.corda.node.services.attachments
import com.github.benmanes.caffeine.cache.Caffeine
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.*
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.Builder
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import java.security.PublicKey
import java.util.stream.Stream
/**
* Implementation of [AttachmentTrustCalculator].
*
* @param blacklistedAttachmentSigningKeys Attachments signed by any of these public keys will not be considered as trust roots for any
* attachments received over the network. The list consists of SHA-256 hashes of public keys
*/
class NodeAttachmentTrustCalculator(
private val attachmentStorage: AttachmentStorageInternal,
private val database: CordaPersistence?,
cacheFactory: NamedCacheFactory,
private val blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList()
) : AttachmentTrustCalculator, SingletonSerializeAsToken() {
@VisibleForTesting
constructor(
attachmentStorage: AttachmentStorageInternal,
cacheFactory: NamedCacheFactory,
blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList()
) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys)
// A cache for caching whether a signing key is trusted
private val trustedKeysCache = cacheFactory.buildNamed<PublicKey, Boolean>(
Caffeine.newBuilder(),
"NodeAttachmentTrustCalculator_trustedKeysCache"
)
override fun calculate(attachment: Attachment): Boolean {
if (attachment.isUploaderTrusted()) return true
if (attachment.isSignedByBlacklistedKey()) return false
return attachment.signerKeys.any { signer ->
trustedKeysCache.get(signer) {
val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(signer)),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
)
attachmentStorage.queryAttachments(queryCriteria).isNotEmpty()
}!!
}
}
override fun calculateAllTrustInfo(): List<AttachmentTrustInfo> {
val publicKeyToTrustRootMap = mutableMapOf<PublicKey, TrustedAttachment>()
val attachmentTrustInfos = mutableListOf<AttachmentTrustInfo>()
val db = checkNotNull(database) {
// This should never be hit, except for tests that have not been setup correctly to test internal code
"CordaPersistence has not been set"
}
db.transaction {
getTrustedAttachments().use { trustedAttachments ->
for ((name, attachment) in trustedAttachments) {
attachment.signerKeys.forEach {
// add signers to the cache as this is a fully trusted attachment
trustedKeysCache.put(it, true)
publicKeyToTrustRootMap.putIfAbsent(
it,
TrustedAttachment(attachment.id, name)
)
}
attachmentTrustInfos += AttachmentTrustInfo(
attachmentId = attachment.id,
fileName = name,
uploader = attachment.uploader,
trustRootId = attachment.id,
trustRootFileName = name
)
}
}
getUntrustedAttachments().use { untrustedAttachments ->
for ((name, attachment) in untrustedAttachments) {
val trustRoot = if (attachment.isSignedByBlacklistedKey()) {
null
} else {
attachment.signerKeys
.mapNotNull { publicKeyToTrustRootMap[it] }
.firstOrNull()
}
attachmentTrustInfos += AttachmentTrustInfo(
attachmentId = attachment.id,
fileName = name,
uploader = attachment.uploader,
trustRootId = trustRoot?.id,
trustRootFileName = trustRoot?.name
)
}
}
}
return attachmentTrustInfos
}
private fun getTrustedAttachments(): Stream<Pair<String?, Attachment>> {
return attachmentStorage.getAllAttachmentsByCriteria(
// `isSignedCondition` is not included here as attachments uploaded by trusted uploaders are considered trusted
AttachmentQueryCriteria.AttachmentsQueryCriteria(
uploaderCondition = Builder.`in`(
TRUSTED_UPLOADERS
)
)
)
}
private fun getUntrustedAttachments(): Stream<Pair<String?, Attachment>> {
return attachmentStorage.getAllAttachmentsByCriteria(
// Filter by `isSignedCondition` so normal data attachments are not returned
AttachmentQueryCriteria.AttachmentsQueryCriteria(
uploaderCondition = Builder.notIn(
TRUSTED_UPLOADERS
),
isSignedCondition = Builder.equal(true)
)
)
}
private data class TrustedAttachment(val id: AttachmentId, val name: String?)
private fun Attachment.isSignedByBlacklistedKey() =
signerKeys.any { it.isBlacklisted() }
private fun PublicKey.isBlacklisted() =
blacklistedAttachmentSigningKeys.contains(this.hash)
private val Attachment.uploader: String?
get() = when (this) {
is ContractAttachment -> uploader
is AbstractAttachment -> uploader
else -> null
}
}

View File

@ -83,6 +83,8 @@ interface NodeConfiguration {
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
val blacklistedAttachmentSigningKeys: List<String>
companion object {
// default to at least 8MB and a bit extra for larger heap sizes
val defaultTransactionCacheSize: Long = 8.MB + getAdditionalCacheMemory()

View File

@ -75,7 +75,8 @@ data class NodeConfigurationImpl(
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings,
override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys
) : NodeConfiguration {
internal object Defaults {
val jmxMonitoringHttpPort: Int? = null
@ -108,6 +109,7 @@ data class NodeConfigurationImpl(
val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType
val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() }
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
val blacklistedAttachmentSigningKeys: List<String> = emptyList()
fun cordappsDirectories(baseDirectory: Path) = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT)

View File

@ -57,6 +57,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
private val jarDirs by string().list().optional().withDefaultValue(Defaults.jarDirs)
private val cordappDirectories by string().mapValid(::toPath).list().optional()
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
private val blacklistedAttachmentSigningKeys by string().list().optional().withDefaultValue(Defaults.blacklistedAttachmentSigningKeys)
@Suppress("unused")
private val custom by nestedObject().optional()
@Suppress("unused")
@ -115,7 +116,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
h2port = configuration[h2port],
jarDirs = configuration[jarDirs],
cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) },
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist]
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
blacklistedAttachmentSigningKeys = configuration[blacklistedAttachmentSigningKeys]
))
} catch (e: Exception) {
return when (e) {

View File

@ -1,11 +1,15 @@
package net.corda.node.services.persistence
import net.corda.core.contracts.Attachment
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import java.io.InputStream
import java.util.stream.Stream
interface AttachmentStorageInternal : AttachmentStorage {
/**
* This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal
* and is only for the node.
@ -16,4 +20,12 @@ interface AttachmentStorageInternal : AttachmentStorage {
* Similar to above but returns existing [AttachmentId] instead of throwing [DuplicateAttachmentException]
*/
fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId
}
/**
* Get all attachments as a [Stream], filtered by the input [AttachmentQueryCriteria],
* stored within the node paired to their file names.
*
* The [Stream] must be closed once used.
*/
fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria()): Stream<Pair<String?, Attachment>>
}

View File

@ -33,6 +33,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.nodeapi.internal.withContractsInJar
import org.hibernate.query.Query
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
@ -42,6 +43,7 @@ import java.time.Instant
import java.util.*
import java.util.jar.JarEntry
import java.util.jar.JarInputStream
import java.util.stream.Stream
import javax.annotation.concurrent.ThreadSafe
import javax.persistence.*
@ -49,15 +51,12 @@ import javax.persistence.*
* Stores attachments using Hibernate to database.
*/
@ThreadSafe
class NodeAttachmentService(
metrics: MetricRegistry,
cacheFactory: NamedCacheFactory,
private val database: CordaPersistence,
val devMode: Boolean
class NodeAttachmentService @JvmOverloads constructor(
metrics: MetricRegistry,
cacheFactory: NamedCacheFactory,
private val database: CordaPersistence,
val devMode: Boolean = false
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
constructor(metrics: MetricRegistry,
cacheFactory: NamedCacheFactory,
database: CordaPersistence) : this(metrics, cacheFactory, database, false)
// This is to break the circular dependency.
lateinit var servicesForResolution: ServicesForResolution
@ -227,18 +226,37 @@ class NodeAttachmentService(
}
}
private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean, uploader: String?) : AbstractAttachment(dataLoader, uploader), SerializeAsToken {
private class AttachmentImpl(
override val id: SecureHash,
dataLoader: () -> ByteArray,
private val checkOnLoad: Boolean,
uploader: String?,
override val signerKeys: List<PublicKey>
) : 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, private val uploader: String?) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(id, context.attachmentDataLoader(id), checkOnLoad, uploader)
private class Token(
private val id: SecureHash,
private val checkOnLoad: Boolean,
private val uploader: String?,
private val signerKeys: List<PublicKey>
) : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl(
id,
context.attachmentDataLoader(id),
checkOnLoad,
uploader,
signerKeys
)
}
override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad, uploader)
override fun toToken(context: SerializeAsTokenContext) =
Token(id, checkOnLoad, uploader, signerKeys)
}
// slightly complex 2 level approach to attachment caching:
@ -264,16 +282,30 @@ 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, 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()
?: emptyList(), attachment.version)
} else {
it
}
}
Pair(attachmentImpl, attachment.content)
Pair(createAttachmentFromDatabase(attachment), attachment.content)
}
}
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
val attachmentImpl = AttachmentImpl(
id = SecureHash.parse(attachment.attId),
dataLoader = { attachment.content },
checkOnLoad = checkAttachmentsOnLoad,
uploader = attachment.uploader,
signerKeys = attachment.signers?.toList() ?: emptyList()
)
val contracts = attachment.contractClassNames
return if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment.create(
attachment = attachmentImpl,
contract = contracts.first(),
additionalContracts = contracts.drop(1).toSet(),
uploader = attachment.uploader,
signerKeys = attachment.signers?.toList() ?: emptyList(),
version = attachment.version
)
} else {
attachmentImpl
}
}
@ -426,28 +458,38 @@ class NodeAttachmentService(
}
}
// TODO do not retrieve whole attachments only to return ids - https://r3-cev.atlassian.net/browse/CORDA-3191 raised to address this
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
log.info("Attachment query criteria: $criteria, sorting: $sorting")
return database.transaction {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
val root = criteriaQuery.from(DBAttachment::class.java)
val criteriaParser = HibernateAttachmentQueryCriteriaParser(criteriaBuilder, criteriaQuery, root)
// parse criteria and build where predicates
criteriaParser.parse(criteria, sorting)
// prepare query for execution
val query = session.createQuery(criteriaQuery)
// execution
query.resultList.map { AttachmentId.parse(it.attId) }
createAttachmentsQuery(
criteria,
sorting
).resultList.map { AttachmentId.parse(it.attId) }
}
}
private fun createAttachmentsQuery(
criteria: AttachmentQueryCriteria,
sorting: AttachmentSort?
): Query<DBAttachment> {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
val criteriaParser = HibernateAttachmentQueryCriteriaParser(
criteriaBuilder,
criteriaQuery,
criteriaQuery.from(DBAttachment::class.java)
)
// parse criteria and build where predicates
criteriaParser.parse(criteria, sorting)
// prepare query for execution
return session.createQuery(criteriaQuery)
}
// Holds onto a signed and/or unsigned attachment (at least one or the other).
private data class AttachmentIds(val signed: AttachmentId?, val unsigned: AttachmentId?) {
init {
@ -471,27 +513,28 @@ class NodeAttachmentService(
private val contractsCache = InfrequentlyMutatedCache<ContractClassName, NavigableMap<Version, AttachmentIds>>("NodeAttachmentService_contractAttachmentVersions", cacheFactory)
private fun getContractAttachmentVersions(contractClassName: String): NavigableMap<Version, AttachmentIds> = contractsCache.get(contractClassName) { name ->
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)),
versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS))
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC),
AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.INSERTION_DATE, Sort.Direction.DESC)))
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
contractClassNamesCondition = Builder.equal(listOf(name)),
versionCondition = Builder.greaterThanOrEqual(0),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
)
val attachmentSort = AttachmentSort(
listOf(
AttachmentSort.AttachmentSortColumn(
AttachmentSort.AttachmentSortAttribute.VERSION,
Sort.Direction.DESC
),
AttachmentSort.AttachmentSortColumn(
AttachmentSort.AttachmentSortAttribute.INSERTION_DATE,
Sort.Direction.DESC
)
)
)
database.transaction {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
val root = criteriaQuery.from(DBAttachment::class.java)
val criteriaParser = HibernateAttachmentQueryCriteriaParser(criteriaBuilder, criteriaQuery, root)
// parse criteria and build where predicates
criteriaParser.parse(attachmentQueryCriteria, attachmentSort)
// prepare query for execution
val query = session.createQuery(criteriaQuery)
// execution
TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it, name) }.toMap())
createAttachmentsQuery(
attachmentQueryCriteria,
attachmentSort
).resultList.groupBy { it.version }.map { makeAttachmentIds(it, name) }.toMap(TreeMap())
}
}
@ -517,4 +560,11 @@ class NodeAttachmentService(
else
emptyList()
}
override fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria): Stream<Pair<String?, Attachment>> {
return createAttachmentsQuery(
criteria,
null
).resultStream.map { it.filename to createAttachmentFromDatabase(it) }
}
}

View File

@ -61,6 +61,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi
name == "BasicHSMKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize)
name == "NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize)
name == "PublicKeyToOwningIdentityCache_cache" -> caffeine.maximumSize(defaultCacheSize)
name == "NodeAttachmentTrustCalculator_trustedKeysCache" -> caffeine.maximumSize(defaultCacheSize)
else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?")
}
}

View File

@ -0,0 +1,629 @@
package net.corda.node.services.attachments
import com.codahale.metrics.MetricRegistry
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class AttachmentTrustCalculatorTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
private val services = rigorousMock<ServicesForResolution>().also {
doReturn(testNetworkParameters()).whenever(it).networkParameters
}
private val cacheFactory = TestingNamedCacheFactory()
@Before
fun setUp() {
val dataSourceProperties = MockServices.makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
database.transaction {
it.start()
}
}
storage.servicesForResolution = services
attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, database, cacheFactory)
}
@After
fun tearDown() {
database.close()
}
@Test
fun `Jar uploaded by trusted uploader is trusted`() {
tempFolder.root.toPath().let { path ->
val (jar, _) = ContractJarTestUtils.makeTestSignedContractJar(
path,
"foo.bar.DummyContract"
)
val unsignedJar = ContractJarTestUtils.makeTestContractJar(path, "com.example.MyContract")
val (attachment, _) = makeTestJar()
val signedId = jar.read { storage.privilegedImportAttachment(it, "app", "signed-contract.jar")}
val unsignedId = unsignedJar.read { storage.privilegedImportAttachment(it, "app", "unsigned-contract.jar") }
val attachmentId = attachment.read { storage.privilegedImportAttachment(it, "app", "attachment.jar")}
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(signedId)!!), "Signed contract $signedId should be trusted but isn't")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(unsignedId)!!), "Unsigned contract $unsignedId should be trusted but isn't")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentId)!!), "Attachment $attachmentId should be trusted but isn't")
}
}
@Test
fun `jar trusted if signed by same key and has same contract as existing jar uploaded by a trusted uploader`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DummyContract",
version = 2
)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if same key but different contract`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DifferentContract",
version = 2
)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a subset of an existing trusted jar's signers`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
path.generateKey(alias, password)
path.generateKey(alias2, password)
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DifferentContract",
version = 2
)
path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are an intersection of an existing trusted jar's signers`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
val alias3 = "yetAnotherTestAlias"
path.generateKey(alias, password)
path.generateKey(alias2, password)
path.generateKey(alias3, password)
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DifferentContract",
version = 2
)
path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
path.signJar(jarV2.toAbsolutePath().toString(), alias3, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a superset of an existing trusted jar's signers`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
path.generateKey(alias, password)
path.generateKey(alias2, password)
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DifferentContract",
version = 2
)
path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
path.signJar(jarV2.toAbsolutePath().toString(), alias2, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar with inherited trust does not grant trust to other jars (no chain of trust)`() {
tempFolder.root.toPath().let { path ->
val aliasA = "Daredevil"
val aliasB = "The Punisher"
val aliasC = "Jessica Jones"
val password = "i am a netflix series"
path.generateKey(aliasA, password)
path.generateKey(aliasB, password)
path.generateKey(aliasC, password)
val jarSignedByA =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DifferentContract",
version = 2
)
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
val jarSignedByBC = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.AnotherContract",
version = 2
)
path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password)
path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentA)!!), "Contract $attachmentA should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentB)!!), "Contract $attachmentB should inherit trust")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentC)!!), "Contract $attachmentC should not be trusted (no chain of trust)")
}
}
@Test
fun `jar not trusted if different key but same contract`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
(path / "_shredder").delete()
(path / "_teststore").delete()
path.generateKey(alias, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DummyContract",
version = 2
)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertNotEquals(key1, key2, "Same public keys used to sign jars")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should be trusted")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should not be trusted")
}
}
@Test
fun `neither jar trusted if same contract and signer but not uploaded by a trusted uploader`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = ContractJarTestUtils.makeTestContractJar(
path,
"foo.bar.DummyContract",
version = 2
)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial contract $v1Id should not be trusted")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Upgraded contract $v2Id should not be trusted")
}
}
@Test
fun `non-contract jar trusted if trusted jar with same key present`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
// them.
var counter = 0
val jarV1 = path / "$counter.jar"
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
counter++
val jarV2 = path / "$counter.jar"
// Ensure that the first and second jars do not have the same hash
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial attachment $v1Id should be trusted")
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Other attachment $v2Id should be trusted")
}
}
@Test
fun `non-contract jars not trusted if uploaded by non trusted uploaders`() {
tempFolder.root.toPath().let { path ->
val alias = "testAlias"
val password = "testPassword"
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
// them.
var counter = 0
val jarV1 = path / "$counter.jar"
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
counter++
val jarV2 = path / "$counter.jar"
// Ensure that the first and second jars do not have the same hash
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
path.generateKey(alias, password)
val key1 = path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val key2 = path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial attachment $v1Id should not be trusted")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Other attachment $v2Id should not be trusted")
}
}
@Test
fun `non-contract jars not trusted if unsigned`() {
SelfCleaningDir().use {
val (jarV1, _) = makeTestJar()
val (jarV2, _) = makeTestJar(entries = listOf(Pair("foo", "bar")))
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(v1Id)!!), "Initial attachment $v1Id should not be trusted")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(v2Id)!!), "Other attachment $v2Id should not be trusted")
}
}
@Test
fun `jar not trusted if signed by a blacklisted key and not uploaded by trusted uploader`() {
tempFolder.root.toPath().let { path ->
val aliasA = "Antman"
val aliasB = "The Wasp"
val password = "antman and the wasp"
path.generateKey(aliasA, password)
val keyB = path.generateKey(aliasB, password)
attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = storage,
database = database,
cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyB.hash)
)
val jarA = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarA.toAbsolutePath().toString(), aliasA, password)
path.signJar(jarA.toAbsolutePath().toString(), aliasB, password)
val jarB =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.AnotherDummyContract")
path.signJar(jarB.toAbsolutePath().toString(), aliasA, password)
path.signJar(jarB.toAbsolutePath().toString(), aliasB, password)
val attachmentA = jarA.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val attachmentB = jarB.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentA)!!), "Contract $attachmentA should be trusted")
assertFalse(attachmentTrustCalculator.calculate(storage.openAttachment(attachmentB)!!), "Contract $attachmentB should not be trusted")
}
}
@Test
fun `jar uploaded by trusted uploader is still trusted even if it is signed by a blacklisted key`() {
tempFolder.root.toPath().let { path ->
val aliasA = "Thanos"
val password = "what did it cost? everything"
val key = path.generateKey(aliasA, password)
attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = storage,
database = database,
cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = listOf(key.hash)
)
val jar = ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jar.toAbsolutePath().toString(), aliasA, password)
val attachment = jar.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
assertTrue(attachmentTrustCalculator.calculate(storage.openAttachment(attachment)!!), "Contract $attachment should be trusted")
}
}
@Test
fun `calculateAllTrustInfo returns all attachment trust roots`() {
tempFolder.root.toPath().let { path ->
val aliasA = "dan"
val aliasB = "james"
val aliasC = "tudor"
val aliasD = "shams"
val password = "one day the attachment service will be refactored"
path.generateKey(aliasA, password)
path.generateKey(aliasB, password)
path.generateKey(aliasC, password)
path.generateKey(aliasD, password)
val jarSignedByA =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.AnotherContract")
path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
val jarSignedByC =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract1")
path.signJar(jarSignedByC.toAbsolutePath().toString(), aliasC, password)
val jarSignedByD =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract2")
path.signJar(jarSignedByD.toAbsolutePath().toString(), aliasD, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "B.jar") }
val attachmentC = jarSignedByC.read { storage.privilegedImportAttachment(it, "app", "C.zip") }
val attachmentD = jarSignedByD.read { storage.privilegedImportAttachment(it, "untrusted", null) }
assertThat(attachmentTrustCalculator.calculateAllTrustInfo()).containsOnly(
AttachmentTrustInfo(
attachmentId = attachmentA,
fileName = "A.jar",
uploader = "app",
trustRootId = attachmentA,
trustRootFileName = "A.jar"
),
AttachmentTrustInfo(
attachmentId = attachmentB,
fileName = "B.jar",
uploader = "untrusted",
trustRootId = attachmentA,
trustRootFileName = "A.jar"
),
AttachmentTrustInfo(
attachmentId = attachmentC,
fileName = "C.zip",
uploader = "app",
trustRootId = attachmentC,
trustRootFileName = "C.zip"
),
AttachmentTrustInfo(
attachmentId = attachmentD,
fileName = null,
uploader = "untrusted",
trustRootId = null,
trustRootFileName = null
)
)
}
}
@Test
fun `calculateAllTrustInfo only returns signed attachments or attachments manually installed on the node`() {
tempFolder.root.toPath().let { path ->
val aliasA = "dan"
val aliasB = "james"
val password = "one day the attachment service will be refactored"
path.generateKey(aliasA, password)
path.generateKey(aliasB, password)
val jarSignedByA =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.AnotherContract")
path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
val (zipC, _) = makeTestJar(listOf(Pair("file", "content")))
val (zipD, _) = makeTestJar(listOf(Pair("magic_file", "magic_content_puff")))
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "B.jar") }
val attachmentC = zipC.read { storage.privilegedImportAttachment(it, "app", "C.zip") }
zipD.read { storage.privilegedImportAttachment(it, "untrusted", null) }
assertThat(attachmentTrustCalculator.calculateAllTrustInfo()).containsOnly(
AttachmentTrustInfo(
attachmentId = attachmentA,
fileName = "A.jar",
uploader = "app",
trustRootId = attachmentA,
trustRootFileName = "A.jar"
),
AttachmentTrustInfo(
attachmentId = attachmentB,
fileName = "B.jar",
uploader = "untrusted",
trustRootId = attachmentA,
trustRootFileName = "A.jar"
),
AttachmentTrustInfo(
attachmentId = attachmentC,
fileName = "C.zip",
uploader = "app",
trustRootId = attachmentC,
trustRootFileName = "C.zip"
)
)
}
}
@Test
fun `calculateAllTrustInfo attachments signed by blacklisted keys output without trust root fields filled in`() {
tempFolder.root.toPath().let { path ->
val aliasA = "batman"
val aliasB = "the joker"
val password = "nanananana batman"
path.generateKey(aliasA, password)
val keyB = path.generateKey(aliasB, password)
attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = storage,
database = database,
cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyB.hash)
)
val jarSignedByA =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.AnotherContract")
path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB =
ContractJarTestUtils.makeTestContractJar(path, "foo.bar.DummyContract")
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "B.jar") }
assertThat(attachmentTrustCalculator.calculateAllTrustInfo()).containsOnly(
AttachmentTrustInfo(
attachmentId = attachmentA,
fileName = "A.jar",
uploader = "app",
trustRootId = attachmentA,
trustRootFileName = "A.jar"
),
AttachmentTrustInfo(
attachmentId = attachmentB,
fileName = "B.jar",
uploader = "untrusted",
trustRootId = null,
trustRootFileName = null
)
)
}
}
private var counter = 0
private fun makeTestJar(
entries: List<Pair<String, String>> = listOf(
Pair("test1.txt", "This is some useful content"),
Pair("test2.txt", "Some more useful content")
)
): Pair<Path, SecureHash> {
counter++
val file = Paths.get((tempFolder.root.toPath() / "$counter.jar").toString())
ContractJarTestUtils.makeTestJar(Files.newOutputStream(file), entries)
return Pair(file, file.readAll().sha256())
}
}

View File

@ -52,6 +52,7 @@ import java.nio.file.FileSystem
import java.nio.file.Path
import java.util.*
import java.util.jar.JarInputStream
import kotlin.streams.toList
import kotlin.test.*
class NodeAttachmentServiceTest {
@ -762,291 +763,6 @@ class NodeAttachmentServiceTest {
}
}
@Test
fun `Jar uploaded by trusted uploader is trusted`() {
SelfCleaningDir().use { file ->
val (jar, _) = makeTestSignedContractJar(file.path, "foo.bar.DummyContract")
val unsignedJar = ContractJarTestUtils.makeTestContractJar(file.path, "com.example.MyContract")
val (attachment, _) = makeTestJar()
val signedId = jar.read { storage.privilegedImportAttachment(it, "app", "signed-contract.jar")}
val unsignedId = unsignedJar.read { storage.privilegedImportAttachment(it, "app", "unsigned-contract.jar") }
val attachmentId = attachment.read { storage.privilegedImportAttachment(it, "app", "attachment.jar")}
assertTrue(isAttachmentTrusted(storage.openAttachment(signedId)!!, storage), "Signed contract $signedId should be trusted but isn't")
assertTrue(isAttachmentTrusted(storage.openAttachment(unsignedId)!!, storage), "Unsigned contract $unsignedId should be trusted but isn't")
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentId)!!, storage), "Attachment $attachmentId should be trusted but isn't")
}
}
@Test
fun `jar trusted if signed by same key and has same contract as existing jar`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if same key but different contract`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a subset of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are an intersection of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
val alias3 = "yetAnotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
file.path.generateKey(alias3, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias3, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a superset of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias2, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar with inherited trust does not grant trust to other jars (no chain of trust)`() {
SelfCleaningDir().use { file ->
val aliasA = "Daredevil"
val aliasB = "The Punisher"
val aliasC = "Jessica Jones"
val password = "i am a netflix series"
file.path.generateKey(aliasA, password)
file.path.generateKey(aliasB, password)
file.path.generateKey(aliasC, password)
val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
val jarSignedByBC = makeTestContractJar(file.path, "foo.bar.AnotherContract", version = 2)
file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password)
file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentA)!!, storage), "Contract $attachmentA should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentB)!!, storage), "Contract $attachmentB should inherit trust")
assertFalse(isAttachmentTrusted(storage.openAttachment(attachmentC)!!, storage), "Contract $attachmentC should not be trusted (no chain of trust)")
}
}
@Test
fun `jar not trusted if different key but same contract`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
(file.path / "_shredder").delete()
(file.path / "_teststore").delete()
file.path.generateKey(alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertNotEquals(key1, key2, "Same public keys used to sign jars")
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
}
}
@Test
fun `neither jar trusted if same contract and signer but not uploaded by a trusted uploader`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DummyContract", version = 2)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertFalse(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should not be trusted")
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
}
}
@Test
fun `non contract jar trusted if trusted jar with same key present`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
// them.
var counter = 0
val jarV1 = file.path / "$counter.jar"
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
counter++
val jarV2 = file.path / "$counter.jar"
// Ensure that the first and second jars do not have the same hash
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should be trusted")
}
}
@Test
fun `all non contract jars not trusted if all are uploaded by non trusted uploaders`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
// Directly use the ContractJarTestUtils version of makeTestJar to ensure jars are created in the right place, in order to sign
// them.
var counter = 0
val jarV1 = file.path / "$counter.jar"
ContractJarTestUtils.makeTestJar(jarV1.outputStream())
counter++
val jarV2 = file.path / "$counter.jar"
// Ensure that the first and second jars do not have the same hash
ContractJarTestUtils.makeTestJar(jarV2.outputStream(), entries = listOf(Pair("foo", "bar")))
file.path.generateKey(alias, password)
val key1 = file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val key2 = file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertFalse(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should not be trusted")
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should not be trusted")
}
}
@Test
fun `non contract jars not trusted if unsigned`() {
SelfCleaningDir().use {
val (jarV1, _) = makeTestJar()
val (jarV2, _) = makeTestJar(entries = listOf(Pair("foo", "bar")))
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-attachment.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-attachment-2.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial attachment $v1Id should not be trusted")
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Other attachment $v2Id should not be trusted")
}
}
@Test
fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() {
SelfCleaningDir().use { file ->
@ -1188,6 +904,88 @@ class NodeAttachmentServiceTest {
}
}
@Test
fun `getAllAttachmentsByCriteria returns no attachments if there are no stored attachments`() {
assertTrue(database.transaction {
storage.getAllAttachmentsByCriteria().toList().isEmpty()
})
}
@Test
fun `getAllAttachmentsByCriteria fails if no database transaction is set`() {
assertThatExceptionOfType(IllegalStateException::class.java).isThrownBy {
storage.getAllAttachmentsByCriteria()
}.withMessageContaining("Was expecting to find transaction set on current strand")
}
@Test
fun `getAllAttachmentsByCriteria returns all stored attachments when no filtering is applied`() {
SelfCleaningDir().use { file ->
val aliasA = "Spiderman"
val password = "why is Sony taking me out of the MCU?!?!"
file.path.generateKey(aliasA, password)
val signedJar = makeTestContractJar(file.path, "foo.bar.DummyContract")
val key = file.path.signJar(signedJar.toAbsolutePath().toString(), aliasA, password)
val unsignedJar = makeTestContractJar(file.path, "foo.bar.DifferentContract")
val (zipC, _) = makeTestJar(listOf(Pair("file", "content")))
val (zipD, _) = makeTestJar(listOf(Pair("magic_file", "magic_content_puff")))
val attachmentA = signedJar.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = unsignedJar.read { storage.privilegedImportAttachment(it, "untrusted", "B.jar") }
val attachmentC = zipC.read { storage.privilegedImportAttachment(it, "untrusted", "C.zip") }
val attachmentD = zipD.read { storage.privilegedImportAttachment(it, "untrusted", null) }
val result = database.transaction { storage.getAllAttachmentsByCriteria().toList() }
assertEquals(4, result.size)
assertThat(result.map { (_, attachment) -> attachment.id }).containsOnly(
attachmentA, attachmentB, attachmentC, attachmentD
)
assertThat(result.map { (name, _) -> name }).containsOnly(
"A.jar", "B.jar", "C.zip", null
)
assertThat(result.map { (_, attachment) -> attachment.signerKeys }).containsOnly(
listOf(key), emptyList(), emptyList(), emptyList()
)
}
}
@Test
fun `getAllAttachmentsByCriteria returns attachments filtered by criteria`() {
SelfCleaningDir().use { file ->
val aliasA = "Dan"
val password = "i am so tired with this work"
file.path.generateKey(aliasA, password)
val signedJar = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(signedJar.toAbsolutePath().toString(), aliasA, password)
val unsignedJar = makeTestContractJar(file.path, "foo.bar.DifferentContract")
val (zipC, _) = makeTestJar(listOf(Pair("file", "content")))
val (zipD, _) = makeTestJar(listOf(Pair("magic_file", "magic_content_puff")))
val attachmentA = signedJar.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = unsignedJar.read { storage.privilegedImportAttachment(it, "untrusted", "B.jar") }
zipC.read { storage.privilegedImportAttachment(it, "untrusted", "C.zip") }
zipD.read { storage.privilegedImportAttachment(it, "untrusted", null) }
val result = database.transaction {
storage.getAllAttachmentsByCriteria(
AttachmentsQueryCriteria(
filenameCondition = Builder.`in`(listOf("A.jar", "B.jar"))
)
).toList()
}
assertEquals(2, result.size)
assertThat(result.map { (_, attachment) -> attachment.id }).containsOnly(
attachmentA, attachmentB
)
}
}
// Not the real FetchAttachmentsFlow!
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
@Suspendable

View File

@ -12,7 +12,6 @@ import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.isAttachmentTrusted
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
@ -20,8 +19,11 @@ import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.node.serialization.kryo.CordaClassResolver
import net.corda.node.serialization.kryo.CordaKryo
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Rule
import org.junit.Test
@ -212,18 +214,20 @@ class CordaClassResolverTests {
@Test(expected = KryoException::class)
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = MockAttachmentStorage()
val storage = InternalMockAttachmentStorage(MockAttachmentStorage())
val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { attachmentTrustCalculator.calculate(it) })
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@Test(expected = TransactionVerificationException.UntrustedAttachmentsException::class)
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
val storage = MockAttachmentStorage()
val storage = InternalMockAttachmentStorage(MockAttachmentStorage())
val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { isAttachmentTrusted(it, storage) })
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { attachmentTrustCalculator.calculate(it) })
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}

View File

@ -606,6 +606,7 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
doReturn("").whenever(it).emailAddress
doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(true).whenever(it).devMode
doReturn(emptyList<String>()).whenever(it).blacklistedAttachmentSigningKeys
doReturn(null).whenever(it).compatibilityZoneURL
doReturn(null).whenever(it).networkServices
doReturn(VerifierType.InMemory).whenever(it).verifierType

View File

@ -7,16 +7,21 @@ import net.corda.core.crypto.NullKeys.NULL_SIGNATURE
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.*
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.TransactionStorage
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.node.services.DbTransactionsResolver
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.testing.core.dummyCommand
import net.corda.testing.internal.MockCordappProvider
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage
import java.io.InputStream
import java.security.PublicKey
@ -77,12 +82,43 @@ data class TestTransactionDSLInterpreter private constructor(
transactionBuilder: TransactionBuilder
) : this(ledgerInterpreter, transactionBuilder, HashMap())
val services = object : ServicesForResolution by ledgerInterpreter.services {
override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
// Implementing [ServiceHubCoreInternal] allows better use in internal Corda tests
val services: ServicesForResolution = object : ServiceHubCoreInternal, ServiceHub by ledgerInterpreter.services {
// [validatedTransactions.getTransaction] needs overriding as there are no calls to
// [ServiceHub.recordTransactions] in the test dsl
override val validatedTransactions: TransactionStorage =
object : TransactionStorage by ledgerInterpreter.services.validatedTransactions {
override fun getTransaction(id: SecureHash): SignedTransaction? =
ledgerInterpreter.getTransaction(id)
}
override val attachmentTrustCalculator: AttachmentTrustCalculator =
ledgerInterpreter.services.attachments.let {
// Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api
// while still allowing the tests to work
NodeAttachmentTrustCalculator(
attachmentStorage = if (it is MockAttachmentStorage) {
InternalMockAttachmentStorage(it)
} else {
it as AttachmentStorageInternal
},
cacheFactory = TestingNamedCacheFactory()
)
}
override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver =
DbTransactionsResolver(flow)
override fun loadState(stateRef: StateRef) =
ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> {
return stateRefs.map { StateAndRef(loadState(it), it) }.toSet()
}
override val cordappProvider: CordappProvider = ledgerInterpreter.services.cordappProvider
override val cordappProvider: CordappProvider =
ledgerInterpreter.services.cordappProvider
}
private fun copy(): TestTransactionDSLInterpreter =
@ -205,6 +241,11 @@ data class TestLedgerDSLInterpreter private constructor(
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations)
)
internal fun getTransaction(id: SecureHash): SignedTransaction? {
val tx = transactionWithLocations[id] ?: nonVerifiedTransactionWithLocations[id]
return tx?.let { SignedTransaction(it.transaction, listOf(NULL_SIGNATURE)) }
}
internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
val transactionWithLocation =
transactionWithLocations[stateRef.txhash] ?:

View File

@ -0,0 +1,43 @@
package net.corda.testing.internal.services
import net.corda.core.contracts.Attachment
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.testing.services.MockAttachmentStorage
import java.io.InputStream
import java.util.stream.Stream
/**
* Internal version of [MockAttachmentStorage] that implements [AttachmentStorageInternal] for use
* in internal tests where [AttachmentStorageInternal] functions are needed.
*/
class InternalMockAttachmentStorage(storage: MockAttachmentStorage) : AttachmentStorageInternal,
AttachmentStorage by storage {
override fun privilegedImportAttachment(
jar: InputStream,
uploader: String,
filename: String?
): AttachmentId = importAttachment(jar, uploader, filename)
override fun privilegedImportOrGetAttachment(
jar: InputStream,
uploader: String,
filename: String?
): AttachmentId {
return try {
importAttachment(jar, uploader, filename)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
}
override fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria): Stream<Pair<String?, Attachment>> {
return queryAttachments(criteria)
.map(this::openAttachment)
.map { null as String? to it!! }
.stream()
}
}

View File

@ -25,6 +25,7 @@ import java.util.jar.JarInputStream
* A mock implementation of [AttachmentStorage] for use within tests
*/
class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
private data class ContractAttachmentMetadata(val name: ContractClassName, val version: Int, val isSigned: Boolean, val signers: List<PublicKey>, val uploader: String)
private val _files = HashMap<SecureHash, Pair<Attachment, ByteArray>>()

View File

@ -308,7 +308,7 @@ class InteractiveShellIntegrationTest {
Thread.sleep(5000)
val (output) = mockRenderPrintWriter()
InteractiveShell.runRPCFromString(listOf("dumpCheckpoints"), output, mock(), aliceNode.rpc as InternalCordaRPCOps, inputObjectMapper)
InteractiveShell.runDumpCheckpoints(aliceNode.rpc as InternalCordaRPCOps)
val zipFile = (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
val json = ZipInputStream(zipFile.inputStream()).use { zip ->

View File

@ -0,0 +1,15 @@
package net.corda.tools.shell;
import org.crsh.cli.Command;
import org.crsh.cli.Man;
import static net.corda.tools.shell.InteractiveShell.runAttachmentTrustInfoView;
public class AttachmentShellCommand extends InteractiveShellCommand {
@Command
@Man("Displays the trusted CorDapp attachments that have been manually installed or received over the network")
public void trustInfo() {
runAttachmentTrustInfoView(out, ops());
}
}

View File

@ -0,0 +1,15 @@
package net.corda.tools.shell;
import org.crsh.cli.Command;
import org.crsh.cli.Man;
import static net.corda.tools.shell.InteractiveShell.*;
public class CheckpointShellCommand extends InteractiveShellCommand {
@Command
@Man("Outputs the contents of all checkpoints as json to be manually reviewed")
public void dump() {
runDumpCheckpoints(ops());
}
}

View File

@ -3,7 +3,6 @@ package net.corda.tools.shell;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.corda.client.jackson.StringToMethodCallParser;
import net.corda.core.internal.messaging.InternalCordaRPCOps;
import net.corda.core.messaging.CordaRPCOps;
import org.crsh.cli.Argument;
import org.crsh.cli.Command;
@ -14,7 +13,10 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Comparator.comparing;
@ -45,22 +47,15 @@ public class RunShellCommand extends InteractiveShellCommand {
return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(InteractiveShell.getCordappsClassloader()));
}
private void emitHelp(InvocationContext<Map> context) {
// to handle the lack of working inheritance in [StringToMethodCallParser] two parsers are used
private void emitHelp(InvocationContext<Map> context) {
StringToMethodCallParser<CordaRPCOps> cordaRpcOpsParser =
new StringToMethodCallParser<>(
CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader()));
StringToMethodCallParser<InternalCordaRPCOps> internalCordaRpcOpsParser =
new StringToMethodCallParser<>(
InternalCordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader()));
new StringToMethodCallParser<>(
CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader()));
// Sends data down the pipeline about what commands are available. CRaSH will render it nicely.
// Each element we emit is a map of column -> content.
Set<Map.Entry<String, String>> entries = cordaRpcOpsParser.getAvailableCommands().entrySet();
Set<Map.Entry<String, String>> internalEntries = internalCordaRpcOpsParser.getAvailableCommands().entrySet();
Set<Map.Entry<String, String>> entrySet = new HashSet<>(entries);
entrySet.addAll(internalEntries);
List<Map.Entry<String, String>> entryList = new ArrayList<>(entrySet);
List<Map.Entry<String, String>> entryList = new ArrayList<>(entries);
entryList.sort(comparing(Map.Entry::getKey));
for (Map.Entry<String, String> entry : entryList) {
// Skip these entries as they aren't really interesting for the user.

View File

@ -0,0 +1,60 @@
package net.corda.tools.shell
import net.corda.core.internal.AttachmentTrustInfo
import net.corda.core.internal.P2P_UPLOADER
import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter
import org.crsh.text.ui.LabelElement
import org.crsh.text.ui.Overflow
import org.crsh.text.ui.RowElement
import org.crsh.text.ui.TableElement
class AttachmentTrustTable(
writer: RenderPrintWriter,
private val attachmentTrustInfos: List<AttachmentTrustInfo>
) {
private val content: TableElement
init {
content = createTable()
createRows()
writer.print(content)
}
private fun createTable(): TableElement {
val table = TableElement(2, 3, 1, 1, 3).overflow(Overflow.WRAP).rightCellPadding(3)
val header =
RowElement(true).add("Name", "Attachment ID", "Installed", "Trusted", "Trust Root").style(
Decoration.bold.fg(
Color.black
).bg(Color.white)
)
table.add(header)
return table
}
private fun createRows() {
for (info in attachmentTrustInfos) {
info.run {
val name = when {
fileName != null -> fileName!!
uploader?.startsWith(P2P_UPLOADER) ?: false -> {
"Received from: ${uploader!!.removePrefix("$P2P_UPLOADER:")}"
}
else -> ""
}
content.add(
RowElement().add(
LabelElement(name),
LabelElement(attachmentId),
LabelElement(isTrustRoot),
LabelElement(isTrusted),
LabelElement(trustRootFileName ?: trustRootId ?: "")
)
)
}
}
}
}

View File

@ -131,6 +131,8 @@ object InteractiveShell {
ExternalResolver.INSTANCE.addCommand("flow", "Commands to work with flows. Flows are how you can change the ledger.", FlowShellCommand::class.java)
ExternalResolver.INSTANCE.addCommand("start", "An alias for 'flow start'", StartShellCommand::class.java)
ExternalResolver.INSTANCE.addCommand("hashLookup", "Checks if a transaction with matching Id hash exists.", HashLookupShellCommand::class.java)
ExternalResolver.INSTANCE.addCommand("attachments", "Commands to extract information about attachments stored within the node", AttachmentShellCommand::class.java)
ExternalResolver.INSTANCE.addCommand("checkpoints", "Commands to extract information about checkpoints stored within the node", CheckpointShellCommand::class.java)
shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
}
@ -483,7 +485,20 @@ object InteractiveShell {
}
@JvmStatic
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: InternalCordaRPCOps,
fun runAttachmentTrustInfoView(
out: RenderPrintWriter,
rpcOps: InternalCordaRPCOps
): Any {
return AttachmentTrustTable(out, rpcOps.attachmentTrustInfos)
}
@JvmStatic
fun runDumpCheckpoints(rpcOps: InternalCordaRPCOps) {
rpcOps.dumpCheckpoints()
}
@JvmStatic
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps,
inputObjectMapper: ObjectMapper): Any? {
val cmd = input.joinToString(" ").trim { it <= ' ' }
if (cmd.startsWith("startflow", ignoreCase = true)) {
@ -499,7 +514,7 @@ object InteractiveShell {
var result: Any? = null
try {
InputStreamSerializer.invokeContext = context
val parser = StringToMethodCallParser(InternalCordaRPCOps::class.java, inputObjectMapper)
val parser = StringToMethodCallParser(CordaRPCOps::class.java, inputObjectMapper)
val call = parser.parse(cordaRPCOps, cmd)
result = call.call()
if (result != null && result !== kotlin.Unit && result !is Void) {