mirror of
https://github.com/corda/corda.git
synced 2025-06-24 02:04:12 +00:00
ENT-11975: Contract key rotation (#7806)
ENT-11975: Contract key rotation implementation.
This commit is contained in:
@ -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}"
|
||||
|
@ -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) {
|
||||
|
@ -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())))
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user