ENT-11975: Contract key rotation (#7806)

ENT-11975: Contract key rotation implementation.
This commit is contained in:
Adel El-Beik
2024-10-02 12:53:11 +01:00
committed by GitHub
parent 56234ecbe9
commit 6f4ec5d9e5
28 changed files with 985 additions and 204 deletions

View File

@ -108,6 +108,7 @@ dependencies {
testImplementation "org.bouncycastle:bcprov-lts8on:${bouncycastle_version}"
testImplementation "io.netty:netty-common:$netty_version"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation "io.dropwizard.metrics:metrics-jmx:$metrics_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"

View File

@ -7,6 +7,7 @@ import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.contracts.NoConstraintPropagation
import net.corda.core.contracts.SignatureAttachmentConstraint
@ -341,52 +342,53 @@ class ConstraintsPropagationTests {
// propagation check
// TODO - enable once the logic to transition has been added.
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentSigned))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentSigned, CordaRotatedKeys.keys))
}
@Test(timeout=300_000)
fun `Attachment canBeTransitionedFrom behaves as expected`() {
// signed attachment (for signature constraint)
val rotatedKeys = CordaRotatedKeys.keys
val attachment = mock<ContractAttachment>()
whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey))
whenever(attachment.allContracts).thenReturn(setOf(propagatingContractClassName))
// Exhaustive positive check
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
// Exhaustive negative check
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
// Fail when encounter a AutomaticPlaceholderConstraint
assertFailsWith<IllegalArgumentException> {
HashAttachmentConstraint(SecureHash.randomSHA256())
.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment)
.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment, rotatedKeys)
}
assertFailsWith<IllegalArgumentException> { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
assertFailsWith<IllegalArgumentException> { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment, rotatedKeys) }
}
private fun MockServices.recordTransaction(wireTransaction: WireTransaction) {

View File

@ -0,0 +1,279 @@
package net.corda.coretests.contracts
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.sha256
import net.corda.core.internal.hash
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.SelfCleaningDir
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RotatedKeysTest {
@Test(timeout = 300_000)
fun `when input and output keys are the same canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKey = file.path.generateKey()
val rotatedKeys = RotatedKeys()
assertTrue(rotatedKeys.canBeTransitioned(publicKey, publicKey))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are the same and output is a list canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKey = file.path.generateKey()
val rotatedKeys = RotatedKeys()
assertTrue(rotatedKeys.canBeTransitioned(publicKey, listOf(publicKey)))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different and output is a list canBeTransitioned returns false`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey("AAAA")
val publicKeyB = file.path.generateKey("BBBB")
val rotatedKeys = RotatedKeys()
assertFalse(rotatedKeys.canBeTransitioned(publicKeyA, listOf(publicKeyB)))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different and rotated and output is a list canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey("AAAA")
val publicKeyB = file.path.generateKey("BBBB")
val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()))))
assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, listOf(publicKeyB)))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are the same and both are lists canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKey = file.path.generateKey()
val rotatedKeys = RotatedKeys()
assertTrue(rotatedKeys.canBeTransitioned(listOf(publicKey), listOf(publicKey)))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different and rotated and both are lists canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()))))
assertTrue(rotatedKeys.canBeTransitioned(listOf(publicKeyA), listOf(publicKeyB)))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different canBeTransitioned returns false`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val rotatedKeys = RotatedKeys()
assertFalse(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different but are rotated canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val rotatedKeysData = listOf((listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256())))
val rotatedKeys = RotatedKeys(rotatedKeysData)
assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB))
}
}
@Test(timeout = 300_000)
fun `when input and output keys are different with multiple rotations canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()),
listOf(publicKeyC.hash.sha256(), publicKeyD.hash.sha256()))
val rotatedKeys = RotatedKeys(rotatedKeysData)
assertTrue(rotatedKeys.canBeTransitioned(publicKeyA, publicKeyB))
}
}
@Test(timeout = 300_000)
fun `when multiple input and output keys are different with multiple rotations canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256()),
listOf(publicKeyB.hash.sha256(), publicKeyD.hash.sha256()))
val rotatedKeys = RotatedKeys(rotatedKeysData)
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build()
assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when multiple input and output keys are diff and diff ordering with multiple rotations canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
val rotatedKeysData = listOf(listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256()),
listOf(publicKeyB.hash.sha256(), publicKeyD.hash.sha256()))
val rotatedKeys = RotatedKeys(rotatedKeysData)
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyD, publicKeyC).build()
assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input and output key are composite and the same canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val compositeKey = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val rotatedKeys = RotatedKeys()
assertTrue(rotatedKeys.canBeTransitioned(compositeKey, compositeKey))
}
}
@Test(timeout = 300_000)
fun `when input and output key are composite and different canBeTransitioned returns false`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build()
val rotatedKeys = RotatedKeys()
assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input and output key are composite and different but key is rotated canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build()
val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyB.hash.sha256(), publicKeyC.hash.sha256()))))
assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input and output key are composite and different and diff key is rotated canBeTransitioned returns false`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyC).build()
val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyA.hash.sha256(), publicKeyC.hash.sha256()))))
assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input is composite (1 key) and output is composite (2 keys) canBeTransitioned returns false`() {
// For composite keys number of input and output leaves must be the same, in canBeTransitioned check.
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val compositeKeyInput = CompositeKey.Builder().addKeys(publicKeyA).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val rotatedKeys = RotatedKeys()
assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input and output key are composite with 2 levels and the same canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build()
val compositeKeyC = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build()
val rotatedKeys = RotatedKeys()
assertTrue(rotatedKeys.canBeTransitioned(compositeKeyC, compositeKeyC))
}
}
@Test(timeout = 300_000)
fun `when input and output key are different & composite & rotated with 2 levels canBeTransitioned returns true`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
// in output DDDD has rotated to EEEE
val publicKeyE = file.path.generateKey(alias = "EEEE")
val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build()
val compositeKeyC = CompositeKey.Builder().addKeys(publicKeyC, publicKeyE).build()
val compositeKeyInput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyC).build()
val rotatedKeys = RotatedKeys(listOf((listOf(publicKeyD.hash.sha256(), publicKeyE.hash.sha256()))))
assertTrue(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000)
fun `when input and output key are different & composite & not rotated with 2 levels canBeTransitioned returns false`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
val publicKeyD = file.path.generateKey(alias = "DDDD")
// in output DDDD has rotated to EEEE
val publicKeyE = file.path.generateKey(alias = "EEEE")
val compositeKeyA = CompositeKey.Builder().addKeys(publicKeyA, publicKeyB).build()
val compositeKeyB = CompositeKey.Builder().addKeys(publicKeyC, publicKeyD).build()
val compositeKeyC = CompositeKey.Builder().addKeys(publicKeyC, publicKeyE).build()
val compositeKeyInput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyB).build()
val compositeKeyOutput = CompositeKey.Builder().addKeys(compositeKeyA, compositeKeyC).build()
val rotatedKeys = RotatedKeys()
assertFalse(rotatedKeys.canBeTransitioned(compositeKeyInput, compositeKeyOutput))
}
}
@Test(timeout = 300_000, expected = IllegalStateException::class)
fun `when key is repeated in rotated list, throws exception`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
RotatedKeys(listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256(), publicKeyA.hash.sha256())))
}
}
@Test(timeout = 300_000, expected = IllegalStateException::class)
fun `when key is repeated across rotated lists, throws exception`() {
SelfCleaningDir().use { file ->
val publicKeyA = file.path.generateKey(alias = "AAAA")
val publicKeyB = file.path.generateKey(alias = "BBBB")
val publicKeyC = file.path.generateKey(alias = "CCCC")
RotatedKeys(listOf(listOf(publicKeyA.hash.sha256(), publicKeyB.hash.sha256()), listOf(publicKeyC.hash.sha256(), publicKeyA.hash.sha256())))
}
}
}

View File

@ -73,7 +73,7 @@ class AttachmentsClassLoaderTests {
}
val ALICE = TestIdentity(ALICE_NAME, 70).party
val BOB = TestIdentity(BOB_NAME, 80).party
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val DUMMY_NOTARY get() = dummyNotary.party
const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract"
}
@ -344,141 +344,6 @@ class AttachmentsClassLoaderTests {
createClassloader(untrustedAttachment).use {}
}
@Test(timeout=300_000)
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
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).use {}
}
}
@Test(timeout=300_000)
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
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"
).inputStream()
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
@Test(timeout=300_000)
fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val keyPairC = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/TrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
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"
).inputStream()
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"
).inputStream()
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).use {
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
}
@Test(timeout=300_000)
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(
storage.toInternal(),
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).use {}
}
}
@Test(timeout=300_000)
fun `Allow loading a trusted attachment that is signed by a blacklisted key`() {
val keyPairA = Crypto.generateKeyPair()

View File

@ -0,0 +1,237 @@
package net.corda.coretests.transactions
import com.codahale.metrics.MetricRegistry
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.AttachmentTrustCalculator
import net.corda.core.internal.hash
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.coretesting.internal.rigorousMock
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.toInternal
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.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.internal.ContractJarTestUtils
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
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.node.MockServices
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import java.net.URL
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderWithStoragePersistenceTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderWithStoragePersistenceTests::class.java.getResource("isolated-4.0.jar")!!
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val DUMMY_NOTARY get() = dummyNotary.party
const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract"
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
private lateinit var attachmentTrustCalculator2: AttachmentTrustCalculator
private val networkParameters = testNetworkParameters()
private val cacheFactory = TestingNamedCacheFactory(1)
private val cacheFactory2 = TestingNamedCacheFactory()
private val nodeVerificationSupport = rigorousMock<NodeVerificationSupport>().also {
doReturn(testNetworkParameters()).whenever(it).networkParameters
}
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,
attachmentTrustCalculator2::calculate
)
}
@Before
fun setUp() {
val dataSourceProperties = MockServices.makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
database.transaction {
it.start()
}
}
storage.nodeVerificationSupport = nodeVerificationSupport
attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory)
attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(storage, database, cacheFactory2)
}
@Test(timeout=300_000)
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() {
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH_V4, copyFirst = true)
val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "untrusted", "isolated-signed.jar" )
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(isolatedSignedId).use {}
}
}
@Test(timeout=300_000)
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() {
SelfCleaningDir().use { file ->
val path = file.path
val alias1 = "AAAA"
val alias2 = "BBBB"
val password = "testPassword"
path.generateKey(alias1, password)
path.generateKey(alias2, password)
val contractName = "net.corda.testing.contracts.MyDummyContract"
val content = createContractString(contractName)
val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, contractName, content = content, version = 2)
path.signJar(contractJarPath.toAbsolutePath().toString(), alias1, password)
path.signJar(contractJarPath.toAbsolutePath().toString(), alias2, password)
val untrustedAttachment = storage.importAttachment(contractJarPath.toUri().toURL().openStream(), "untrusted", "contract.jar")
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
}
@Test(timeout=300_000)
fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() {
SelfCleaningDir().use { file ->
val path = file.path
val alias1 = "AAAA"
val alias2 = "BBBB"
val alias3 = "CCCC"
val password = "testPassword"
path.generateKey(alias1, password)
path.generateKey(alias2, password)
path.generateKey(alias3, password)
val contractName1 = "net.corda.testing.contracts.MyDummyContract1"
val contractName2 = "net.corda.testing.contracts.MyDummyContract2"
val contractName3 = "net.corda.testing.contracts.MyDummyContract3"
val content = createContractString(contractName1)
val contractJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = content)
path.signJar(contractJar.toAbsolutePath().toString(), alias1, password)
storage.privilegedImportAttachment(contractJar.toUri().toURL().openStream(), "app", "contract.jar")
val content2 = createContractString(contractName2)
val contractJarPath2 = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = content2, version = 2)
path.signJar(contractJarPath2.toAbsolutePath().toString(), alias1, password)
path.signJar(contractJarPath2.toAbsolutePath().toString(), alias2, password)
val inheritedTrustAttachment = storage.importAttachment(contractJarPath2.toUri().toURL().openStream(), "untrusted", "dummy-contract.jar")
val content3 = createContractString(contractName3)
val contractJarPath3 = ContractJarTestUtils.makeTestContractJar(path, contractName3, content = content3, version = 3)
path.signJar(contractJarPath3.toAbsolutePath().toString(), alias2, password)
path.signJar(contractJarPath3.toAbsolutePath().toString(), alias3, password)
val untrustedAttachment = storage.importAttachment(contractJarPath3.toUri().toURL()
.openStream(), "untrusted", "contract.jar")
// pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment
createClassloader(inheritedTrustAttachment).use {
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
}
}
@Test(timeout=300_000)
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`() {
SelfCleaningDir().use { file ->
val path = file.path
val aliasA = "AAAA"
val aliasB = "BBBB"
val password = "testPassword"
val publicKeyA = path.generateKey(aliasA, password)
path.generateKey(aliasB, password)
attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(
storage,
cacheFactory,
blacklistedAttachmentSigningKeys = listOf(publicKeyA.hash)
)
val contractName1 = "net.corda.testing.contracts.MyDummyContract1"
val contractName2 = "net.corda.testing.contracts.MyDummyContract2"
val contentTrusted = createContractString(contractName1)
val classJar = ContractJarTestUtils.makeTestContractJar(path, contractName1, content = contentTrusted)
path.signJar(classJar.toAbsolutePath().toString(), aliasA, password)
path.signJar(classJar.toAbsolutePath().toString(), aliasB, password)
storage.privilegedImportAttachment(classJar.toUri().toURL().openStream(), "app", "contract.jar")
val contentUntrusted = createContractString(contractName2)
val untrustedClassJar = ContractJarTestUtils.makeTestContractJar(path, contractName2, content = contentUntrusted)
path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasA, password)
path.signJar(untrustedClassJar.toAbsolutePath().toString(), aliasB, password)
val untrustedAttachment = storage.importAttachment(untrustedClassJar.toUri().toURL()
.openStream(), "untrusted", "untrusted-contract.jar")
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
}
private fun createContractString(contractName: String, versionSeed: Int = 0): String {
val pkgs = contractName.split(".")
val className = pkgs.last()
val packages = pkgs.subList(0, pkgs.size - 1)
val output = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*;
import net.corda.core.transactions.*;
import java.net.URL;
import java.io.InputStream;
public class $className implements Contract {
private int seed = $versionSeed;
@Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
System.gc();
InputStream str = this.getClass().getClassLoader().getResourceAsStream("importantDoc.pdf");
if (str == null) throw new IllegalStateException("Could not find importantDoc.pdf");
}
}
""".trimIndent()
println(output)
return output
}
}