mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
4fb1787f1e
commit
4cbe22949d
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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>
|
||||
}
|
@ -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)
|
||||
|
@ -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" }
|
||||
|
@ -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() }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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``).
|
||||
|
@ -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.
|
@ -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``.
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
/**
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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>>
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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?")
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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] ?:
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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>>()
|
||||
|
@ -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 ->
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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 ?: "")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user