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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
}
}

View File

@ -0,0 +1,117 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.hash
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import java.security.PublicKey
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
object CordaRotatedKeys {
val keys = RotatedKeys()
}
// The current development CorDapp code signing public key hash
const val DEV_CORDAPP_CODE_SIGNING_STR = "AA59D829F2CA8FDDF5ABEA40D815F937E3E54E572B65B93B5C216AE6594E7D6B"
// The non production CorDapp code signing public key hash
const val NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR = "B710A80780A12C52DF8A0B4C2247E08907CCA5D0F19AB1E266FE7BAEA9036790"
// The production CorDapp code signing public key hash
const val PROD_CORDAPP_CODE_SIGNING_STR = "EB4989E7F861FEBEC242E6C24CF0B51C41E108D2C4479D296C5570CB8DAD3EE0"
// The new production CorDapp code signing public key hash
const val NEW_PROD_CORDAPP_CODE_SIGNING_STR = "01EFA14B42700794292382C1EEAC9788A26DAFBCCC98992C01D5BC30EEAACD28"
// Rotations used by Corda
private val CORDA_SIGNING_KEY_ROTATIONS = listOf(
listOf(SecureHash.create(DEV_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_NON_PROD_CORDAPP_CODE_SIGNING_STR).sha256()),
listOf(SecureHash.create(PROD_CORDAPP_CODE_SIGNING_STR).sha256(), SecureHash.create(NEW_PROD_CORDAPP_CODE_SIGNING_STR).sha256())
)
/**
* This class represents the rotated CorDapp signing keys known by this node.
*
* A public key in this class is identified by its SHA-256 hash of the public key encoded bytes (@see PublicKey.getEncoded()).
* A sequence of rotated keys is represented by a list of hashes of those public keys. The list of those lists represents
* each unrelated set of rotated keys. A key should not appear more than once, either in the same list of in multiple lists.
*
* For the purposes of SignatureConstraints this means we treat all entries in a list of key hashes as equivalent.
* For two keys to be equivalent, they must be equal, or they must appear in the same list of hashes.
*
* @param rotatedSigningKeys A List of rotated keys. With a rotated key being represented by a list of hashes. This list comes from
* node.conf.
*
*/
@CordaSerializable
data class RotatedKeys(val rotatedSigningKeys: List<List<SecureHash>> = emptyList()): SingletonSerializeAsToken() {
private val canBeTransitionedMap: ConcurrentMap<Pair<PublicKey, PublicKey>, Boolean> = ConcurrentHashMap()
private val rotateMap: Map<SecureHash, SecureHash> = HashMap<SecureHash, SecureHash>().apply {
(rotatedSigningKeys + CORDA_SIGNING_KEY_ROTATIONS).forEach { rotatedKeyList ->
rotatedKeyList.forEach { key ->
if (this.containsKey(key)) throw IllegalStateException("The key with sha256(hash) $key appears in the rotated keys configuration more than once.")
this[key] = rotatedKeyList.last()
}
}
}
fun canBeTransitioned(inputKey: PublicKey, outputKeys: List<PublicKey>): Boolean {
return canBeTransitioned(inputKey, CompositeKey.Builder().addKeys(outputKeys).build())
}
fun canBeTransitioned(inputKeys: List<PublicKey>, outputKeys: List<PublicKey>): Boolean {
return canBeTransitioned(CompositeKey.Builder().addKeys(inputKeys).build(), CompositeKey.Builder().addKeys(outputKeys).build())
}
fun canBeTransitioned(inputKey: PublicKey, outputKey: PublicKey): Boolean {
// Need to handle if inputKey and outputKey are composite keys. They could be if part of SignatureConstraints
return canBeTransitionedMap.getOrPut(Pair(inputKey, outputKey)) {
when {
(inputKey is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey)
(inputKey is CompositeKey && outputKey !is CompositeKey) -> compareKeys(inputKey, outputKey)
(inputKey !is CompositeKey && outputKey is CompositeKey) -> compareKeys(inputKey, outputKey)
else -> isRotatedEquals(inputKey, outputKey)
}
}
}
private fun rotate(key: SecureHash): SecureHash {
return rotateMap[key] ?: key
}
private fun isRotatedEquals(inputKey: PublicKey, outputKey: PublicKey): Boolean {
return when {
inputKey == outputKey -> true
rotate(inputKey.hash.sha256()) == rotate(outputKey.hash.sha256()) -> true
else -> false
}
}
private fun compareKeys(inputKey: CompositeKey, outputKey: PublicKey): Boolean {
if (inputKey.leafKeys.size == 1) {
return canBeTransitioned(inputKey.leafKeys.first(), outputKey)
}
return false
}
private fun compareKeys(inputKey: PublicKey, outputKey: CompositeKey): Boolean {
if (outputKey.leafKeys.size == 1) {
return canBeTransitioned(inputKey, outputKey.leafKeys.first())
}
return false
}
private fun compareKeys(inputKey: CompositeKey, outputKey: CompositeKey): Boolean {
if (inputKey.leafKeys.size != outputKey.leafKeys.size) {
return false
}
else {
inputKey.leafKeys.forEach { inputLeafKey ->
if (!outputKey.leafKeys.any { outputLeafKey -> canBeTransitioned(inputLeafKey, outputLeafKey) }) {
return false
}
}
return true
}
}
}

View File

@ -57,7 +57,8 @@ val ContractState.requiredContractClassName: String? get() {
* JAR are required to sign in the future.
*
*/
fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
@Suppress("ComplexMethod")
fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment, rotatedKeys: RotatedKeys): Boolean {
val output = this
@Suppress("DEPRECATION")
@ -83,7 +84,7 @@ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, atta
// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key.
// TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key.
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> rotatedKeys.canBeTransitioned(input.key, output.key)
// HashAttachmentConstraint can be transformed to a SignatureAttachmentConstraint when hash constraint verification checking disabled.
HashAttachmentConstraint.disableHashConstraints && input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> true

View File

@ -1,6 +1,7 @@
package net.corda.core.internal.verification
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
@ -24,6 +25,8 @@ interface VerificationSupport {
val attachmentsClassLoaderCache: AttachmentsClassLoaderCache? get() = null
val rotatedKeys: RotatedKeys
// TODO Use SequencedCollection if upgraded to Java 21
fun getParties(keys: Collection<PublicKey>): List<Party?>

View File

@ -1,10 +1,12 @@
package net.corda.core.internal.verification
import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.SignatureAttachmentConstraint
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
@ -89,7 +91,7 @@ abstract class AbstractVerifier(
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
* wrong object instance. This class helps avoid that.
*/
private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) {
private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, private val rotatedKeys: RotatedKeys) {
private val inputStates: List<TransactionState<*>> = ltx.inputs.map(StateAndRef<ContractState>::state)
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs
@ -376,7 +378,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
outputConstraints.forEach { outputConstraint ->
inputConstraints.forEach { inputConstraint ->
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) {
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment, rotatedKeys))) {
throw ConstraintPropagationRejection(
ltx.id,
contractClassName,
@ -430,8 +432,20 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint)
logger.warnOnce("Skipping hash constraints verification.")
else if (!constraint.isSatisfiedBy(constraintAttachment))
throw ContractConstraintRejection(ltx.id, contract)
else if (!constraint.isSatisfiedBy(constraintAttachment)) {
verifyConstraintUsingRotatedKeys(constraint, constraintAttachment, contract)
}
}
}
private fun verifyConstraintUsingRotatedKeys(constraint: AttachmentConstraint, constraintAttachment: AttachmentWithContext, contract: ContractClassName ) {
// constraint could be an input constraint so we manually have to rotate to updated constraint
if (constraint is SignatureAttachmentConstraint && rotatedKeys.canBeTransitioned(constraint.key, constraintAttachment.signerKeys)) {
val constraintWithRotatedKeys = SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build())
if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract)
}
else {
throw ContractConstraintRejection(ltx.id, contract)
}
}
}
@ -465,7 +479,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun
}
private fun validateTransaction(ltx: LedgerTransaction) {
Validator(ltx, transactionClassLoader).validate()
Validator(ltx, transactionClassLoader, ltx.rotatedKeys).validate()
}
override fun apply(transactionFactory: Supplier<LedgerTransaction>) {

View File

@ -4,6 +4,8 @@ import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
@ -337,7 +339,8 @@ object AttachmentsClassLoaderBuilder {
block: (SerializationContext) -> T): T {
val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id)
val cache = attachmentsClassLoaderCache ?: fallBackCache
val cache = if (attachmentsClassLoaderCache is AttachmentsClassLoaderForRotatedKeysOnlyImpl) fallBackCache else
attachmentsClassLoaderCache ?: fallBackCache
val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params)) { key ->
// Create classloader and load serializers, whitelisted classes
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
@ -453,14 +456,14 @@ private class AttachmentsHolderImpl : AttachmentsHolder {
}
interface AttachmentsClassLoaderCache {
val rotatedKeys: RotatedKeys
fun computeIfAbsent(
key: AttachmentsClassLoaderKey,
mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext
): SerializationContext
}
class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache {
class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache {
private class ToBeClosed(
serializationContext: SerializationContext,
val classLoaderToClose: AutoCloseable,
@ -513,7 +516,7 @@ class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : Singlet
}
}
class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLoaderCache {
class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache {
private val cache: MutableMap<AttachmentsClassLoaderKey, SerializationContext>
= createSimpleCache<AttachmentsClassLoaderKey, SerializationContext>(cacheSize).toSynchronised()
@ -525,6 +528,12 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLo
}
}
class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache {
override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext): SerializationContext {
throw NotImplementedError("AttachmentsClassLoaderForRotatedKeysOnlyImpl.computeIfAbsent should never be called. Should be replaced by the fallback cache")
}
}
// We use a set here because the ordering of attachments doesn't affect code execution, due to the no
// overlap rule, and attachments don't have any particular ordering enforced by the builders. So we
// can just do unordered comparisons here. But the same attachments run with different network parameters

View File

@ -7,7 +7,9 @@ import net.corda.core.contracts.CommandData
import net.corda.core.contracts.CommandWithParties
import net.corda.core.contracts.ComponentGroupEnum
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionState
@ -31,6 +33,7 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl
import net.corda.core.utilities.contextLogger
import java.util.Collections.unmodifiableList
import java.util.function.Predicate
@ -96,6 +99,8 @@ private constructor(
val digestService: DigestService
) : FullTransaction() {
val rotatedKeys = attachmentsClassLoaderCache?.rotatedKeys ?: CordaRotatedKeys.keys
/**
* Old version of [LedgerTransaction] constructor for ABI compatibility.
*/
@ -195,7 +200,8 @@ private constructor(
privacySalt: PrivacySalt,
networkParameters: NetworkParameters?,
references: List<StateAndRef<ContractState>>,
digestService: DigestService): LedgerTransaction {
digestService: DigestService,
rotatedKeys: RotatedKeys): LedgerTransaction {
return LedgerTransaction(
inputs = protect(inputs),
outputs = protect(outputs),
@ -212,7 +218,7 @@ private constructor(
serializedReferences = null,
isAttachmentTrusted = { true },
verifierFactory = ::NoOpVerifier,
attachmentsClassLoaderCache = null,
attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl(rotatedKeys),
digestService = digestService
// This check accesses input states and must run on the LedgerTransaction
// instance that is verified, not on the outer LedgerTransaction shell.
@ -872,7 +878,8 @@ private class DefaultVerifier(
privacySalt = ltx.privacySalt,
networkParameters = ltx.networkParameters,
references = deserializedReferences,
digestService = ltx.digestService
digestService = ltx.digestService,
rotatedKeys = ltx.rotatedKeys
)
}
}

View File

@ -532,10 +532,10 @@ open class TransactionBuilder(
}
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment.currentAttachment, serviceHub)
val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName,
inputStates, selectedAttachment.currentAttachment, serviceHub)
// Sanity check that the selected attachment actually passes.
val constraintAttachment = AttachmentWithContext(selectedAttachment.currentAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations)
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
}
@ -547,7 +547,7 @@ open class TransactionBuilder(
} else {
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
inputStates?.forEach { input ->
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment)) {
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment, serviceHub.toVerifyingServiceHub().rotatedKeys)) {
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
}
}
@ -559,6 +559,27 @@ open class TransactionBuilder(
return Pair(selectedAttachment, resolvedOutputStates)
}
private fun selectDefaultOutputConstraintAndConstraintAttachment( contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?,
attachmentToUse: ContractAttachment,
services: ServicesForResolution): Pair<AttachmentConstraint, AttachmentWithContext> {
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
// Sanity check that the selected attachment actually passes.
if (!defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
// The defaultOutputConstraint is the input constraint by the attachment in use currently may have a rotated key
if (defaultOutputConstraint is SignatureAttachmentConstraint && services.toVerifyingServiceHub().rotatedKeys.canBeTransitioned(defaultOutputConstraint.key, constraintAttachment.signerKeys)) {
return Pair(makeSignatureAttachmentConstraint(attachmentToUse.signerKeys), constraintAttachment)
}
}
return Pair(defaultOutputConstraint, constraintAttachment)
}
/**
* Checks whether the current transaction can migrate from a [HashAttachmentConstraint] to a
* [SignatureAttachmentConstraint]. This is only possible in very specific scenarios. Most

View File

@ -11,6 +11,7 @@ import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionResolutionException
@ -181,6 +182,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
override val appClassLoader: ClassLoader get() = throw AbstractMethodError()
override fun getTrustedClassAttachments(className: String) = throw AbstractMethodError()
override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>) = throw AbstractMethodError()
override val rotatedKeys: RotatedKeys get() = throw AbstractMethodError()
})
}

View File

@ -0,0 +1,135 @@
package net.corda.node
import net.corda.core.crypto.sha256
import net.corda.core.internal.hash
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.GBP
import net.corda.finance.POUNDS
import net.corda.finance.USD
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.workflows.getCashBalance
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.node.MockNetworkNotarySpec
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.MockNodeArgs
import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.cordappWithPackages
import net.corda.testing.node.internal.startFlow
import org.apache.commons.io.FileUtils.deleteDirectory
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import kotlin.io.path.div
import kotlin.test.assertEquals
class ContractWithRotatedKeyTest {
private val ref = OpaqueBytes.of(0x01)
private val TestStartedNode.party get() = info.legalIdentities.first()
private lateinit var mockNet: InternalMockNetwork
@Before
fun setup() {
mockNet = InternalMockNetwork(initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = 8), notarySpecs = listOf(MockNetworkNotarySpec(
DUMMY_NOTARY_NAME,
validating = false
)))
}
@After
fun shutdown() {
mockNet.stopNodes()
}
private fun restartNodeAndDeleteOldCorDapps(network: InternalMockNetwork,
node: TestStartedNode,
parameters: InternalMockNodeParameters = InternalMockNodeParameters(),
nodeFactory: (MockNodeArgs) -> InternalMockNetwork.MockNode = network.defaultFactory
): TestStartedNode {
node.internals.disableDBCloseOnStop()
node.dispose()
val cordappsDir = network.baseDirectory(node) / "cordapps"
deleteDirectory(cordappsDir.toFile())
return network.createNode(
parameters.copy(legalName = node.internals.configuration.myLegalName, forcedID = node.internals.id),
nodeFactory
)
}
@Test(timeout = 300_000)
fun `cordapp with rotated key continues to transact`() {
val keyStoreDir1 = SelfCleaningDir()
val keyStoreDir2 = SelfCleaningDir()
val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="1-testcordapp-rsa")
val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="1-testcordapp-rsa")
val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services")
val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2)
val signedFinanceCorDapp1 = unsignedFinanceCorDapp1.signed( keyStoreDir1.path )
val signedFinanceCorDapp2 = unsignedFinanceCorDapp2.signed( keyStoreDir2.path )
val configOverrides = { conf: NodeConfiguration ->
val rotatedKeys = listOf(RotatedCorDappSignerKeyConfiguration(listOf(packageOwnerKey1.hash.sha256().toString(), packageOwnerKey2.hash.sha256().toString())))
doReturn(rotatedKeys).whenever(conf).rotatedCordappSignerKeys
}
val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = listOf(signedFinanceCorDapp1), configOverrides = configOverrides))
val bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = listOf(signedFinanceCorDapp1), configOverrides = configOverrides))
val flow1 = alice.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity))
val flow2 = alice.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity))
val flow3 = bob.services.startFlow(CashIssueAndPaymentFlow(300.POUNDS, ref, bob.party, false, mockNet.defaultNotaryIdentity))
val flow4 = bob.services.startFlow(CashIssueAndPaymentFlow(1000.POUNDS, ref, alice.party, false, mockNet.defaultNotaryIdentity))
mockNet.runNetwork()
flow1.resultFuture.getOrThrow()
flow2.resultFuture.getOrThrow()
flow3.resultFuture.getOrThrow()
flow4.resultFuture.getOrThrow()
val alice2 = restartNodeAndDeleteOldCorDapps(mockNet, alice, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2), configOverrides = configOverrides))
val bob2 = restartNodeAndDeleteOldCorDapps(mockNet, bob, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2), configOverrides = configOverrides))
assertEquals(alice.party, alice2.party)
assertEquals(bob.party, bob2.party)
assertEquals(alice2.party, alice2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
assertEquals(bob2.party, alice2.services.identityService.wellKnownPartyFromX500Name(BOB_NAME))
assertEquals(alice2.party, bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
assertEquals(bob2.party, bob2.services.identityService.wellKnownPartyFromX500Name(BOB_NAME))
val flow5 = alice2.services.startFlow(CashPaymentFlow(300.DOLLARS, bob2.party, false))
val flow6 = bob2.services.startFlow(CashPaymentFlow(300.POUNDS, alice2.party, false))
mockNet.runNetwork()
val flow7 = bob2.services.startFlow(CashPaymentFlow(1300.DOLLARS, alice2.party, false))
val flow8 = alice2.services.startFlow(CashPaymentFlow(1300.POUNDS, bob2.party, false))
mockNet.runNetwork()
flow5.resultFuture.getOrThrow()
flow6.resultFuture.getOrThrow()
flow7.resultFuture.getOrThrow()
flow8.resultFuture.getOrThrow()
assertEquals(1300.DOLLARS, alice2.services.getCashBalance(USD))
assertEquals(0.POUNDS, alice2.services.getCashBalance(GBP))
assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD))
assertEquals(1300.POUNDS, bob2.services.getCashBalance(GBP))
keyStoreDir1.close()
keyStoreDir2.close()
}
}

View File

@ -12,6 +12,7 @@ import net.corda.confidential.SwapIdentitiesFlow
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
@ -246,7 +247,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private val notaryLoader = configuration.notary?.let {
NotaryLoader(it, versionInfo)
}
val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo).closeOnStop(false)
val rotatedKeys = makeRotatedKeysService(configuration).tokenize()
val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo, rotatedKeys).closeOnStop(false)
val telemetryService: TelemetryServiceImpl = TelemetryServiceImpl().also {
val openTelemetryComponent = OpenTelemetryComponent(configuration.myLegalName.toString(), configuration.telemetry.spanStartEndEventsEnabled, configuration.telemetry.copyBaggageToTags)
if (configuration.telemetry.openTelemetryEnabled && openTelemetryComponent.isEnabled()) {
@ -290,7 +292,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
database,
configuration.devMode
).tokenize()
val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database)
val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database, rotatedKeys)
@Suppress("LeakingThis")
val networkParametersStorage = makeNetworkParametersStorage()
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@ -303,7 +305,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
// TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions?
val networkMapUpdater = makeNetworkMapUpdater()
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize()
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory, rotatedKeys).tokenize()
val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize()
val auditService = DummyAuditService().tokenize()
@Suppress("LeakingThis")
@ -842,7 +844,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
unfinishedSchedules = busyNodeLatch
).tokenize()
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo, rotatedKeys: RotatedKeys): CordappLoader {
val generatedCordapps = mutableListOf(VirtualCordapp.generateCore(versionInfo))
notaryLoader?.builtInNotary?.let { notaryImpl ->
generatedCordapps += notaryImpl
@ -858,7 +860,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
(configuration.baseDirectory / LEGACY_CONTRACTS_DIR_NAME).takeIf { it.exists() },
versionInfo,
extraCordapps = generatedCordapps,
signerKeyFingerprintBlacklist = blacklistedKeys
signerKeyFingerprintBlacklist = blacklistedKeys,
rotatedKeys = rotatedKeys
)
}
@ -873,9 +876,16 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
}
private fun makeRotatedKeysService( configuration: NodeConfiguration ): RotatedKeys {
return RotatedKeys(configuration.rotatedCordappSignerKeys.map { rotatedKeysConfiguration ->
parseSecureHashConfiguration(rotatedKeysConfiguration.rotatedKeys) { "Error while parsing rotated keys $it"}
}.toList())
}
private fun makeAttachmentTrustCalculator(
configuration: NodeConfiguration,
database: CordaPersistence
database: CordaPersistence,
rotatedKeys: RotatedKeys
): AttachmentTrustCalculator {
val blacklistedAttachmentSigningKeys: List<SecureHash> =
parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" }
@ -883,7 +893,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
attachmentStorage = attachments,
database = database,
cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys
blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys,
rotatedKeys = rotatedKeys
).tokenize()
}
@ -1192,6 +1203,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor
override val notaryService: NotaryService? get() = this@AbstractNode.notaryService
override val telemetryService: TelemetryService get() = this@AbstractNode.telemetryService
override val rotatedKeys: RotatedKeys get() = this@AbstractNode.rotatedKeys
private lateinit var _myInfo: NodeInfo
override val myInfo: NodeInfo get() = _myInfo

View File

@ -7,6 +7,7 @@ import io.github.classgraph.ScanResult
import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.common.logging.errorReporting.ErrorCode
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.RotatedKeys
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
@ -72,12 +73,13 @@ import kotlin.reflect.KProperty1
* @property cordappJars The classpath of cordapp JARs
* @property legacyContractJars Legacy contract CorDapps (4.11 or earlier) needed for backwards compatibility with 4.11 nodes.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
private val legacyContractJars: Set<Path> = emptySet(),
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
private val extraCordapps: List<CordappImpl> = emptyList(),
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList(),
private val rotatedKeys: RotatedKeys = RotatedKeys()) : CordappLoader {
companion object {
private val logger = contextLogger()
@ -93,14 +95,15 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
legacyContractsDir: Path? = null,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList(),
rotatedKeys: RotatedKeys = RotatedKeys()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.toSet().joinToString(", ", "[", "]")}")
val cordappJars = cordappDirs
.asSequence()
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
.toSet()
val legacyContractJars = legacyContractsDir?.useDirectoryEntries("*.jar") { it.toSet() } ?: emptySet()
return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist, rotatedKeys)
}
}
@ -217,7 +220,7 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
private fun checkSignersMatch(legacyCordapp: CordappImpl, nonLegacyCordapp: CordappImpl) {
val legacySigners = legacyCordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectSigners)
val nonLegacySigners = nonLegacyCordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectSigners)
check(legacySigners == nonLegacySigners) {
check(rotatedKeys.canBeTransitioned(legacySigners, nonLegacySigners)) {
"Newer contract CorDapp '${nonLegacyCordapp.jarFile}' signers do not match legacy contract CorDapp " +
"'${legacyCordapp.jarFile}' signers."
}

View File

@ -2,6 +2,8 @@ package net.corda.node.services.attachments
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.AttachmentTrustCalculator
@ -30,15 +32,17 @@ class NodeAttachmentTrustCalculator(
private val attachmentStorage: AttachmentStorageInternal,
private val database: CordaPersistence?,
cacheFactory: NamedCacheFactory,
private val blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList()
private val blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList(),
private val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys
) : AttachmentTrustCalculator, SingletonSerializeAsToken() {
@VisibleForTesting
constructor(
attachmentStorage: AttachmentStorageInternal,
cacheFactory: NamedCacheFactory,
blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList()
) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys)
attachmentStorage: AttachmentStorageInternal,
cacheFactory: NamedCacheFactory,
blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList(),
rotatedKeys: RotatedKeys = CordaRotatedKeys.keys
) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys, rotatedKeys)
// A cache for caching whether a signing key is trusted
private val trustedKeysCache = cacheFactory.buildNamed<PublicKey, Boolean>("NodeAttachmentTrustCalculator_trustedKeysCache")
@ -55,11 +59,33 @@ class NodeAttachmentTrustCalculator(
signersCondition = Builder.equal(listOf(signer)),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
)
attachmentStorage.queryAttachments(queryCriteria).isNotEmpty()
(attachmentStorage.queryAttachments(queryCriteria).isNotEmpty() ||
calculateTrustUsingRotatedKeys(signer))
}!!
}
}
private fun calculateTrustUsingRotatedKeys(signer: PublicKey): Boolean {
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"
}
return db.transaction {
getTrustedAttachments().use { trustedAttachments ->
for ((_, trustedAttachmentFromDB) in trustedAttachments) {
if (canTrustedAttachmentAndAttachmentSignerBeTransitioned(trustedAttachmentFromDB, signer)) {
return@transaction true
}
}
}
return@transaction false
}
}
private fun canTrustedAttachmentAndAttachmentSignerBeTransitioned(trustedAttachmentFromDB: Attachment, signer: PublicKey): Boolean {
return trustedAttachmentFromDB.signerKeys.any { signerKeyFromDB -> rotatedKeys.canBeTransitioned(signerKeyFromDB, signer) }
}
override fun calculateAllTrustInfo(): List<AttachmentTrustInfo> {
val publicKeyToTrustRootMap = mutableMapOf<PublicKey, TrustedAttachment>()

View File

@ -86,6 +86,13 @@ interface NodeConfiguration : ConfigurationWithOptionsContainer {
val cordappSignerKeyFingerprintBlacklist: List<String>
/**
* Represents a list of rotated CorDapp attachment JAR signing key configurations. Each configuration describes a set of equivalent
* keys. Logically there should be no overlap between configurations, since that would mean they should be one combined list,
* and this is enforced.
*/
val rotatedCordappSignerKeys: List<RotatedCorDappSignerKeyConfiguration>
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings?
val networkParametersPath: Path
@ -222,6 +229,13 @@ data class TelemetryConfiguration(
val copyBaggageToTags: Boolean
)
/**
* Represents a list of rotated CorDapp attachment signing keys.
*
* @param rotatedKeys This is a list of public key hashes (SHA-256) in uppercase hexidecimal, that are all equivalent.
*/
data class RotatedCorDappSignerKeyConfiguration(val rotatedKeys: List<String>)
internal typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Error>
fun Config.parseAsNodeConfiguration(options: Configuration.Options = Configuration.Options(strict = true)): Valid<NodeConfiguration> = V1NodeConfigurationSpec.parse(this, options)

View File

@ -80,6 +80,7 @@ data class NodeConfigurationImpl(
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val rotatedCordappSignerKeys: List<RotatedCorDappSignerKeyConfiguration> = Defaults.rotatedCordappSignerKeys,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? =
Defaults.networkParameterAcceptanceSettings,
override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys,
@ -122,6 +123,7 @@ data class NodeConfigurationImpl(
val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS
val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType
val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() }
val rotatedCordappSignerKeys: List<RotatedCorDappSignerKeyConfiguration> = emptyList()
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
val blacklistedAttachmentSigningKeys: List<String> = emptyList()
const val flowExternalOperationThreadPoolSize: Int = 1

View File

@ -27,6 +27,7 @@ import net.corda.node.services.config.NodeH2Settings
import net.corda.node.services.config.NodeRpcSettings
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.PasswordEncryption
import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId
import net.corda.node.services.config.TelemetryConfiguration
@ -225,6 +226,14 @@ internal object TelemetryConfigurationSpec : Configuration.Specification<Telemet
}
}
internal object RotatedSignerKeySpec : Configuration.Specification<RotatedCorDappSignerKeyConfiguration>("RotatedCorDappSignerKeyConfiguration") {
private val rotatedKeys by string().listOrEmpty()
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<RotatedCorDappSignerKeyConfiguration> {
val config = configuration.withOptions(options)
return valid(RotatedCorDappSignerKeyConfiguration(config[rotatedKeys]))
}
}
internal object NotaryConfigSpec : Configuration.Specification<NotaryConfig>("NotaryConfig") {
private val validating by boolean()
private val serviceLegalName by string().mapValid(::toCordaX500Name).optional()

View File

@ -60,6 +60,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 rotatedCordappSignerKeys by nested(RotatedSignerKeySpec).listOrEmpty()
private val blacklistedAttachmentSigningKeys by string().list().optional().withDefaultValue(Defaults.blacklistedAttachmentSigningKeys)
private val networkParameterAcceptanceSettings by nested(NetworkParameterAcceptanceSettingsSpec)
.optional()
@ -138,7 +139,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
flowExternalOperationThreadPoolSize = config[flowExternalOperationThreadPoolSize],
quasarExcludePackages = config[quasarExcludePackages],
reloadCheckpointAfterSuspend = config[reloadCheckpointAfterSuspend],
networkParametersPath = networkParametersPath
networkParametersPath = networkParametersPath,
rotatedCordappSignerKeys = config[rotatedCordappSignerKeys]
))
} catch (e: Exception) {
return when (e) {

View File

@ -237,7 +237,8 @@ class ExternalVerifierHandleImpl(
customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name },
serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name },
System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization
serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize()
serializedCurrentNetworkParameters = verificationSupport.networkParameters.serialize(),
serializedRotatedKeys = verificationSupport.rotatedKeys.serialize()
)
channel.writeCordaSerializable(initialisation)
}

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.verifier
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toStringShort
@ -23,6 +24,7 @@ import kotlin.math.min
import kotlin.reflect.KClass
typealias SerializedNetworkParameters = SerializedBytes<NetworkParameters>
typealias SerializedRotatedKeys = SerializedBytes<RotatedKeys>
@CordaSerializable
sealed class ExternalVerifierInbound {
@ -30,16 +32,19 @@ sealed class ExternalVerifierInbound {
val customSerializerClassNames: Set<String>,
val serializationWhitelistClassNames: Set<String>,
val customSerializationSchemeClassName: String?,
val serializedCurrentNetworkParameters: SerializedNetworkParameters
val serializedCurrentNetworkParameters: SerializedNetworkParameters,
val serializedRotatedKeys: SerializedRotatedKeys
) : ExternalVerifierInbound() {
val currentNetworkParameters: NetworkParameters by lazy { serializedCurrentNetworkParameters.deserialize() }
val rotatedKeys: RotatedKeys by lazy { serializedRotatedKeys.deserialize() }
override fun toString(): String {
return "Initialisation(" +
"customSerializerClassNames=$customSerializerClassNames, " +
"serializationWhitelistClassNames=$serializationWhitelistClassNames, " +
"customSerializationSchemeClassName=$customSerializationSchemeClassName, " +
"currentNetworkParameters=$currentNetworkParameters)"
"currentNetworkParameters=$currentNetworkParameters, " +
"rotatedKeys=$rotatedKeys)"
}
}

View File

@ -5,6 +5,7 @@ import net.corda.core.CordaInternal
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
@ -128,8 +129,8 @@ open class MockServices private constructor(
)
) : ServiceHub {
companion object {
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
return JarScanningCordappLoader(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo = versionInfo)
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, rotatedKeys: RotatedKeys = RotatedKeys()): CordappLoader {
return JarScanningCordappLoader(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo = versionInfo, rotatedKeys = rotatedKeys)
}
/**
@ -500,6 +501,7 @@ open class MockServices private constructor(
protected val servicesForResolution: ServicesForResolution get() = verifyingView
private val verifyingView: VerifyingServiceHub by lazy { VerifyingView(this) }
val rotatedKeys: RotatedKeys = RotatedKeys()
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
return NodeVaultService(
@ -564,10 +566,10 @@ open class MockServices private constructor(
private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices {
override val attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = mockServices.attachments.toInternal(),
cacheFactory = TestingNamedCacheFactory()
cacheFactory = TestingNamedCacheFactory(), rotatedKeys = mockServices.rotatedKeys
)
override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory())
override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory(), mockServices.rotatedKeys)
override val cordappProvider: CordappProviderInternal get() = mockServices.mockCordappProvider
@ -579,6 +581,8 @@ open class MockServices private constructor(
override val externalVerifierHandle: ExternalVerifierHandle
get() = throw UnsupportedOperationException("`Verification of legacy transactions is not supported by MockServices. Use MockNode instead.")
override val rotatedKeys: RotatedKeys = mockServices.rotatedKeys
}

View File

@ -42,6 +42,7 @@ import net.corda.node.services.config.FlowTimeoutConfiguration
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration
import net.corda.node.services.config.TelemetryConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.node.services.identity.PersistentIdentityService
@ -670,6 +671,7 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
doReturn(rigorousMock<ConfigurationWithOptions>()).whenever(it).configurationWithOptions
doReturn(2).whenever(it).flowExternalOperationThreadPoolSize
doReturn(false).whenever(it).reloadCheckpointAfterSuspend
doReturn(emptyList<RotatedCorDappSignerKeyConfiguration>()).whenever(it).rotatedCordappSignerKeys
}
}

View File

@ -14,6 +14,7 @@ import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappProviderInternal
import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.verification.ExternalVerifierHandle
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.StatesToRecord
@ -108,11 +109,13 @@ data class TestTransactionDSLInterpreter private constructor(
ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build()
)
override val rotatedKeys: RotatedKeys = ledgerInterpreter.services.toVerifyingServiceHub().rotatedKeys
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 = it.toInternal(), cacheFactory = TestingNamedCacheFactory())
NodeAttachmentTrustCalculator(attachmentStorage = it.toInternal(), cacheFactory = TestingNamedCacheFactory(), rotatedKeys = rotatedKeys)
}
override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver =

View File

@ -1,6 +1,7 @@
package net.corda.verifier
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
@ -14,7 +15,8 @@ class ExternalVerificationContext(
override val appClassLoader: ClassLoader,
override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache,
private val externalVerifier: ExternalVerifier,
private val transactionInputsAndReferences: Map<StateRef, SerializedTransactionState>
private val transactionInputsAndReferences: Map<StateRef, SerializedTransactionState>,
override val rotatedKeys: RotatedKeys
) : VerificationSupport {
override val isInProcess: Boolean get() = false

View File

@ -2,6 +2,7 @@ package net.corda.verifier
import com.github.benmanes.caffeine.cache.Cache
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.loadClassOfType
@ -70,6 +71,7 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc
private lateinit var appClassLoader: ClassLoader
private lateinit var currentNetworkParameters: NetworkParameters
private lateinit var rotatedKeys: RotatedKeys
init {
val cacheFactory = ExternalVerifierNamedCacheFactory()
@ -117,7 +119,7 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc
currentNetworkParameters = initialisation.currentNetworkParameters
networkParametersMap.put(initialisation.serializedCurrentNetworkParameters.hash, Optional.of(currentNetworkParameters))
rotatedKeys = initialisation.rotatedKeys
log.info("External verifier initialised")
}
@ -132,7 +134,8 @@ class ExternalVerifier(private val baseDirectory: Path, private val channel: Soc
@Suppress("INVISIBLE_MEMBER")
private fun verifyTransaction(request: VerificationRequest) {
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.ctxInputsAndReferences)
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this,
request.ctxInputsAndReferences, rotatedKeys)
val result: Try<Unit> = try {
val ctx = request.ctx
when (ctx) {