Merge pull request #7841 from corda/merge-release/os/4.10-release/os/4.11-2024-10-14-372

ENT-12291: Merging forward updates from release/os/4.10 to release/os/4.11 - 2024-10-14
This commit is contained in:
Adel El-Beik 2024-10-16 23:20:21 +01:00 committed by GitHub
commit 5d0593c20f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1158 additions and 207 deletions

View File

@ -0,0 +1,116 @@
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 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()) {
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

@ -50,6 +50,7 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version" testImplementation "junit:junit:$junit_version"
testImplementation "io.dropwizard.metrics:metrics-jmx:$metrics_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"

View File

@ -3,7 +3,19 @@ package net.corda.coretests.contracts
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.* import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.AutomaticPlaceholderConstraint
import net.corda.core.contracts.BelongsToContract
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
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.allOnesHash import net.corda.core.crypto.SecureHash.Companion.allOnesHash
@ -326,52 +338,53 @@ class ConstraintsPropagationTests {
// propagation check // propagation check
// TODO - enable once the logic to transition has been added. // 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) @Test(timeout=300_000)
fun `Attachment canBeTransitionedFrom behaves as expected`() { fun `Attachment canBeTransitionedFrom behaves as expected`() {
// signed attachment (for signature constraint) // signed attachment (for signature constraint)
val rotatedKeys = CordaRotatedKeys.keys
val attachment = mock<ContractAttachment>() val attachment = mock<ContractAttachment>()
whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey)) whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey))
whenever(attachment.allContracts).thenReturn(setOf(propagatingContractClassName)) whenever(attachment.allContracts).thenReturn(setOf(propagatingContractClassName))
// Exhaustive positive check // Exhaustive positive check
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) 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 // Exhaustive negative check
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) 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(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment, rotatedKeys))
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment, rotatedKeys))
// Fail when encounter a AutomaticPlaceholderConstraint // Fail when encounter a AutomaticPlaceholderConstraint
assertFailsWith<IllegalArgumentException> { assertFailsWith<IllegalArgumentException> {
HashAttachmentConstraint(SecureHash.randomSHA256()) 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) { private fun MockServices.recordTransaction(wireTransaction: WireTransaction) {

View File

@ -0,0 +1,294 @@
package net.corda.coretests.contracts
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.hash
import net.corda.core.internal.retrieveRotatedKeys
import net.corda.core.node.ServiceHub
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.node.MockServices
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RotatedKeysTest {
@Test(timeout = 300_000)
fun `validateDefaultRotatedKeysAreRetrievableFromMockServices`() {
val services: ServiceHub = MockServices(TestIdentity(CordaX500Name("MegaCorp", "London", "GB")))
val rotatedKeys = services.retrieveRotatedKeys()
assertEquals( CordaRotatedKeys.keys.rotatedSigningKeys, rotatedKeys.rotatedSigningKeys)
}
@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 ALICE = TestIdentity(ALICE_NAME, 70).party
val BOB = TestIdentity(BOB_NAME, 80).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 val DUMMY_NOTARY get() = dummyNotary.party
const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract" const val PROGRAM_ID = "net.corda.testing.contracts.MyDummyContract"
} }
@ -348,141 +348,6 @@ class AttachmentsClassLoaderTests {
createClassloader(untrustedAttachment).use {} 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(
InternalMockAttachmentStorage(storage),
cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash)
)
val classJar = fakeAttachment(
"/com/example/something/TrustedClass.class",
"Signed by someone trusted"
).inputStream()
storage.importContractAttachment(
listOf("TrustedClass.class"),
"rpc",
classJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
untrustedClassJar,
signers = listOf(keyPairA.public, keyPairB.public)
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Allow loading a trusted attachment that is signed by a blacklisted key`() { fun `Allow loading a trusted attachment that is signed by a blacklisted key`() {
val keyPairA = Crypto.generateKeyPair() val keyPairA = Crypto.generateKeyPair()

View File

@ -0,0 +1,234 @@
package net.corda.coretests.transactions
import com.codahale.metrics.MetricRegistry
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
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.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
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.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 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 attachmentTrustCalculator2: AttachmentTrustCalculator
private val networkParameters = testNetworkParameters()
private val cacheFactory = TestingNamedCacheFactory(1)
private val cacheFactory2 = TestingNamedCacheFactory()
private val services = rigorousMock<ServicesForResolution>().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.servicesForResolution = services
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

@ -3,6 +3,7 @@ package net.corda.core.internal
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.contracts.RotatedKeys
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
/** /**
@ -57,7 +58,8 @@ val ContractState.requiredContractClassName: String? get() {
* JAR are required to sign in the future. * 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 val output = this
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -83,7 +85,7 @@ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, atta
// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key. // 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. // 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 can be transformed to a SignatureAttachmentConstraint when hash constraint verification checking disabled.
HashAttachmentConstraint.disableHashConstraints && input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> true HashAttachmentConstraint.disableHashConstraints && input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> true

View File

@ -1,6 +1,7 @@
package net.corda.core.internal package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionMetadata
@ -28,6 +29,8 @@ interface ServiceHubCoreInternal : ServiceHub {
val attachmentsClassLoaderCache: AttachmentsClassLoaderCache val attachmentsClassLoaderCache: AttachmentsClassLoaderCache
val rotatedKeys: RotatedKeys
/** /**
* Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage, * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage,
* inclusive of flow recovery metadata. * inclusive of flow recovery metadata.

View File

@ -7,14 +7,32 @@ import net.corda.core.crypto.algorithm
import net.corda.core.crypto.internal.DigestAlgorithmFactory import net.corda.core.crypto.internal.DigestAlgorithmFactory
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.transactions.* import net.corda.core.transactions.*
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.contextLogger
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.PublicKey import java.security.PublicKey
import kotlin.reflect.KClass import kotlin.reflect.KClass
fun ServiceHub.retrieveRotatedKeys(): RotatedKeys {
if (this is ServiceHubCoreInternal) {
return this.rotatedKeys
}
var clazz: Class<*> = javaClass
while (true) {
if (clazz.name == "net.corda.testing.node.MockServices") {
return clazz.getDeclaredMethod("getRotatedKeys").apply { isAccessible = true }.invoke(this) as RotatedKeys
}
clazz = clazz.superclass ?: return CordaRotatedKeys.keys.also {
this.contextLogger().warn("${javaClass.name} is not a MockServices instance - returning default rotated keys")
}
}
}
/** Constructs a [NotaryChangeWireTransaction]. */ /** Constructs a [NotaryChangeWireTransaction]. */
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>, class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
val notary: Party, val notary: Party,

View File

@ -2,11 +2,13 @@ package net.corda.core.internal
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.SignatureAttachmentConstraint import net.corda.core.contracts.SignatureAttachmentConstraint
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
@ -84,9 +86,9 @@ abstract class AbstractVerifier(
/** /**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the * 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. * wrong object instance. This class helps
*/ */
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 inputStates: List<TransactionState<*>> = ltx.inputs.map(StateAndRef<ContractState>::state)
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs
@ -371,7 +373,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
outputConstraints.forEach { outputConstraint -> outputConstraints.forEach { outputConstraint ->
inputConstraints.forEach { inputConstraint -> inputConstraints.forEach { inputConstraint ->
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) { if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment, rotatedKeys))) {
throw ConstraintPropagationRejection( throw ConstraintPropagationRejection(
ltx.id, ltx.id,
contractClassName, contractClassName,
@ -425,8 +427,20 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint) if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint)
logger.warnOnce("Skipping hash constraints verification.") logger.warnOnce("Skipping hash constraints verification.")
else if (!constraint.isSatisfiedBy(constraintAttachment)) else if (!constraint.isSatisfiedBy(constraintAttachment)) {
throw ContractConstraintRejection(ltx.id, contract) 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(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build())
if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract)
}
else {
throw ContractConstraintRejection(ltx.id, contract)
} }
} }
} }
@ -461,7 +475,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun
} }
private fun validateTransaction(ltx: LedgerTransaction) { private fun validateTransaction(ltx: LedgerTransaction) {
Validator(ltx, transactionClassLoader).validate() Validator(ltx, transactionClassLoader, ltx.rotatedKeys).validate()
} }
override fun apply(transactionFactory: Supplier<LedgerTransaction>) { 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 com.github.benmanes.caffeine.cache.Caffeine
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment 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
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
@ -357,7 +359,8 @@ object AttachmentsClassLoaderBuilder {
block: (SerializationContext) -> T): T { block: (SerializationContext) -> T): T {
val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id) 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), Function { key -> val cachedSerializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key ->
// Create classloader and load serializers, whitelisted classes // Create classloader and load serializers, whitelisted classes
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
@ -473,11 +476,11 @@ private class AttachmentsHolderImpl : AttachmentsHolder {
} }
interface AttachmentsClassLoaderCache { interface AttachmentsClassLoaderCache {
val rotatedKeys: RotatedKeys
fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function<in AttachmentsClassLoaderKey, out SerializationContext>): SerializationContext fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function<in AttachmentsClassLoaderKey, out SerializationContext>): SerializationContext
} }
class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory, override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache {
private class ToBeClosed( private class ToBeClosed(
serializationContext: SerializationContext, serializationContext: SerializationContext,
val classLoaderToClose: AutoCloseable, val classLoaderToClose: AutoCloseable,
@ -528,8 +531,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> private val cache: MutableMap<AttachmentsClassLoaderKey, SerializationContext>
= createSimpleCache<AttachmentsClassLoaderKey, SerializationContext>(cacheSize).toSynchronised() = createSimpleCache<AttachmentsClassLoaderKey, SerializationContext>(cacheSize).toSynchronised()
@ -538,6 +540,12 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int) : AttachmentsClassLo
} }
} }
class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache {
override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function<in AttachmentsClassLoaderKey, out 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 // 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 // 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 // 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.CommandWithParties
import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.contracts.ComponentGroupEnum
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionState 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.SerializationFactory
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import java.util.Collections.unmodifiableList import java.util.Collections.unmodifiableList
import java.util.function.Predicate import java.util.function.Predicate
@ -96,6 +99,8 @@ private constructor(
val digestService: DigestService val digestService: DigestService
) : FullTransaction() { ) : FullTransaction() {
val rotatedKeys = attachmentsClassLoaderCache?.rotatedKeys ?: CordaRotatedKeys.keys
/** /**
* Old version of [LedgerTransaction] constructor for ABI compatibility. * Old version of [LedgerTransaction] constructor for ABI compatibility.
*/ */
@ -194,7 +199,8 @@ private constructor(
privacySalt: PrivacySalt, privacySalt: PrivacySalt,
networkParameters: NetworkParameters?, networkParameters: NetworkParameters?,
references: List<StateAndRef<ContractState>>, references: List<StateAndRef<ContractState>>,
digestService: DigestService): LedgerTransaction { digestService: DigestService,
rotatedKeys: RotatedKeys): LedgerTransaction {
return LedgerTransaction( return LedgerTransaction(
inputs = protect(inputs), inputs = protect(inputs),
outputs = protect(outputs), outputs = protect(outputs),
@ -211,7 +217,7 @@ private constructor(
serializedReferences = null, serializedReferences = null,
isAttachmentTrusted = { true }, isAttachmentTrusted = { true },
verifierFactory = ::NoOpVerifier, verifierFactory = ::NoOpVerifier,
attachmentsClassLoaderCache = null, attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl(rotatedKeys),
digestService = digestService digestService = digestService
// This check accesses input states and must run on the LedgerTransaction // This check accesses input states and must run on the LedgerTransaction
// instance that is verified, not on the outer LedgerTransaction shell. // instance that is verified, not on the outer LedgerTransaction shell.
@ -861,7 +867,8 @@ private class BasicVerifier(
privacySalt = ltx.privacySalt, privacySalt = ltx.privacySalt,
networkParameters = ltx.networkParameters, networkParameters = ltx.networkParameters,
references = deserializedReferences, references = deserializedReferences,
digestService = ltx.digestService digestService = ltx.digestService,
rotatedKeys = ltx.rotatedKeys
) )
} }
} }

View File

@ -14,7 +14,9 @@ import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.contracts.RotatedKeys
import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.CustomSerializationScheme
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializationDefaults
@ -521,20 +523,25 @@ open class TransactionBuilder(
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." } require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName,
inputStates, attachmentToUse, services)
// Sanity check that the selected attachment actually passes. // Sanity check that the selected attachment actually passes.
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } "Selected output constraint: $defaultOutputConstraint not satisfying $attachmentToUse"
}
val resolvedOutputStates = outputStates.map { val resolvedOutputStates = outputStates.map {
val outputConstraint = it.constraint val outputConstraint = it.constraint
if (outputConstraint in automaticConstraints) { if (outputConstraint in automaticConstraints) {
it.copy(constraint = defaultOutputConstraint) it.copy(constraint = defaultOutputConstraint)
} else { } 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. // 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 -> inputStates?.forEach { input ->
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse, getRotatedKeys(serviceHub))) {
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
}
} }
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
it it
@ -544,6 +551,35 @@ open class TransactionBuilder(
return Pair(selectedAttachmentId, resolvedOutputStates) return Pair(selectedAttachmentId, resolvedOutputStates)
} }
private fun getRotatedKeys(services: ServiceHub?): RotatedKeys {
return services?.let { services.retrieveRotatedKeys() } ?: CordaRotatedKeys.keys.also {
log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
"state pointers outside of flows. If you are writing a unit test then pass in a " +
"MockServices instance.")
}
}
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 && (getRotatedKeys(serviceHub).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 * Checks whether the current transaction can migrate from a [HashAttachmentConstraint] to a
* [SignatureAttachmentConstraint]. This is only possible in very specific scenarios. Most * [SignatureAttachmentConstraint]. This is only possible in very specific scenarios. Most

View File

@ -4,7 +4,17 @@ import net.corda.core.CordaInternal
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
import net.corda.core.crypto.* import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.keys
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
@ -16,6 +26,7 @@ import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
import net.corda.core.serialization.internal.AttachmentsClassLoaderForRotatedKeysOnlyImpl
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import java.security.PublicKey import java.security.PublicKey
@ -149,7 +160,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ stateRef -> resolveStateRef(stateRef)?.serialize() }, { stateRef -> resolveStateRef(stateRef)?.serialize() },
{ null }, { null },
Attachment::isUploaderTrusted, Attachment::isUploaderTrusted,
null attachmentsClassLoaderCache = AttachmentsClassLoaderForRotatedKeysOnlyImpl()
) )
} }

View File

@ -0,0 +1,134 @@
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 com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
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).resolve("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="alias1")
val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1")
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.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationContext
import net.corda.core.contracts.RotatedKeys
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.newSecureRandom
@ -247,7 +248,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private val notaryLoader = configuration.notary?.let { private val notaryLoader = configuration.notary?.let {
NotaryLoader(it, versionInfo) 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 telemetryService: TelemetryServiceImpl = TelemetryServiceImpl().also {
val openTelemetryComponent = OpenTelemetryComponent(configuration.myLegalName.toString(), configuration.telemetry.spanStartEndEventsEnabled, configuration.telemetry.copyBaggageToTags) val openTelemetryComponent = OpenTelemetryComponent(configuration.myLegalName.toString(), configuration.telemetry.spanStartEndEventsEnabled, configuration.telemetry.copyBaggageToTags)
if (configuration.telemetry.openTelemetryEnabled && openTelemetryComponent.isEnabled()) { if (configuration.telemetry.openTelemetryEnabled && openTelemetryComponent.isEnabled()) {
@ -291,7 +293,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
database, database,
configuration.devMode configuration.devMode
).tokenize() ).tokenize()
val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database) val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database, rotatedKeys)
@Suppress("LeakingThis") @Suppress("LeakingThis")
val networkParametersStorage = makeNetworkParametersStorage() val networkParametersStorage = makeNetworkParametersStorage()
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@ -315,7 +317,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
cordappProvider = cordappProvider, cordappProvider = cordappProvider,
attachments = attachments attachments = attachments
).tokenize() ).tokenize()
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory, rotatedKeys).tokenize()
val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize()
val auditService = DummyAuditService().tokenize() val auditService = DummyAuditService().tokenize()
@Suppress("LeakingThis") @Suppress("LeakingThis")
@ -853,7 +855,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
unfinishedSchedules = busyNodeLatch unfinishedSchedules = busyNodeLatch
).tokenize() ).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)) val generatedCordapps = mutableListOf(VirtualCordapp.generateCore(versionInfo))
notaryLoader?.builtInNotary?.let { notaryImpl -> notaryLoader?.builtInNotary?.let { notaryImpl ->
generatedCordapps += notaryImpl generatedCordapps += notaryImpl
@ -868,7 +870,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
configuration.cordappDirectories, configuration.cordappDirectories,
versionInfo, versionInfo,
extraCordapps = generatedCordapps, extraCordapps = generatedCordapps,
signerKeyFingerprintBlacklist = blacklistedKeys signerKeyFingerprintBlacklist = blacklistedKeys,
rotatedKeys = rotatedKeys
) )
} }
@ -883,9 +886,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( private fun makeAttachmentTrustCalculator(
configuration: NodeConfiguration, configuration: NodeConfiguration,
database: CordaPersistence database: CordaPersistence,
rotatedKeys: RotatedKeys
): AttachmentTrustCalculator { ): AttachmentTrustCalculator {
val blacklistedAttachmentSigningKeys: List<SecureHash> = val blacklistedAttachmentSigningKeys: List<SecureHash> =
parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" } parseSecureHashConfiguration(configuration.blacklistedAttachmentSigningKeys) { "Error while adding signing key $it to blacklistedAttachmentSigningKeys" }
@ -893,7 +903,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
attachmentStorage = attachments, attachmentStorage = attachments,
database = database, database = database,
cacheFactory = cacheFactory, cacheFactory = cacheFactory,
blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys blacklistedAttachmentSigningKeys = blacklistedAttachmentSigningKeys,
rotatedKeys = rotatedKeys
).tokenize() ).tokenize()
} }
@ -1205,6 +1216,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val diagnosticsService: DiagnosticsService get() = this@AbstractNode.diagnosticsService override val diagnosticsService: DiagnosticsService get() = this@AbstractNode.diagnosticsService
override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor
override val notaryService: NotaryService? get() = this@AbstractNode.notaryService override val notaryService: NotaryService? get() = this@AbstractNode.notaryService
override val rotatedKeys: RotatedKeys get() = this@AbstractNode.rotatedKeys
override val telemetryService: TelemetryService get() = this@AbstractNode.telemetryService override val telemetryService: TelemetryService get() = this@AbstractNode.telemetryService
private lateinit var _myInfo: NodeInfo private lateinit var _myInfo: NodeInfo

View File

@ -6,6 +6,8 @@ import io.github.classgraph.ScanResult
import net.corda.common.logging.errorReporting.CordappErrors import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.common.logging.errorReporting.ErrorCode import net.corda.common.logging.errorReporting.ErrorCode
import net.corda.core.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
@ -50,7 +52,8 @@ import kotlin.streams.toList
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>, class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>, extraCordapps: List<CordappImpl>,
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() { private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList(),
private val rotatedKeys: RotatedKeys = RotatedKeys()) : CordappLoaderTemplate() {
init { init {
if (cordappJarPaths.isEmpty()) { if (cordappJarPaths.isEmpty()) {
logger.info("No CorDapp paths provided") logger.info("No CorDapp paths provided")
@ -76,10 +79,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
fun fromDirectories(cordappDirs: Collection<Path>, fun fromDirectories(cordappDirs: Collection<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN, versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(), extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader { signerKeyFingerprintBlacklist: List<SecureHash> = emptyList(),
rotatedKeys: RotatedKeys = RotatedKeys()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist, rotatedKeys)
} }
/** /**
@ -88,9 +92,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
*/ */
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader { cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList(), rotatedKeys: RotatedKeys = CordaRotatedKeys.keys): JarScanningCordappLoader {
val paths = scanJars.map { it.restricted() } val paths = scanJars.map { it.restricted() }
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist) return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist, rotatedKeys)
} }
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)

View File

@ -3,6 +3,8 @@ package net.corda.node.services.attachments
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment 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.crypto.SecureHash
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
@ -24,15 +26,17 @@ class NodeAttachmentTrustCalculator(
private val attachmentStorage: AttachmentStorageInternal, private val attachmentStorage: AttachmentStorageInternal,
private val database: CordaPersistence?, private val database: CordaPersistence?,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
private val blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList() private val blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList(),
private val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys
) : AttachmentTrustCalculator, SingletonSerializeAsToken() { ) : AttachmentTrustCalculator, SingletonSerializeAsToken() {
@VisibleForTesting @VisibleForTesting
constructor( constructor(
attachmentStorage: AttachmentStorageInternal, attachmentStorage: AttachmentStorageInternal,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList() blacklistedAttachmentSigningKeys: List<SecureHash> = emptyList(),
) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys) rotatedKeys: RotatedKeys = CordaRotatedKeys.keys
) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys, rotatedKeys)
// A cache for caching whether a signing key is trusted // A cache for caching whether a signing key is trusted
private val trustedKeysCache = cacheFactory.buildNamed<PublicKey, Boolean>( private val trustedKeysCache = cacheFactory.buildNamed<PublicKey, Boolean>(
@ -52,11 +56,33 @@ class NodeAttachmentTrustCalculator(
signersCondition = Builder.equal(listOf(signer)), signersCondition = Builder.equal(listOf(signer)),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS) 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> { override fun calculateAllTrustInfo(): List<AttachmentTrustInfo> {
val publicKeyToTrustRootMap = mutableMapOf<PublicKey, TrustedAttachment>() val publicKeyToTrustRootMap = mutableMapOf<PublicKey, TrustedAttachment>()

View File

@ -86,6 +86,13 @@ interface NodeConfiguration : ConfigurationWithOptionsContainer {
val cordappSignerKeyFingerprintBlacklist: List<String> 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 networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings?
val networkParametersPath: Path val networkParametersPath: Path
@ -215,6 +222,13 @@ data class FlowTimeoutConfiguration(
val backoffBase: Double val backoffBase: Double
) )
/**
* 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>)
data class TelemetryConfiguration( data class TelemetryConfiguration(
val openTelemetryEnabled: Boolean, val openTelemetryEnabled: Boolean,
val simpleLogTelemetryEnabled: Boolean, val simpleLogTelemetryEnabled: Boolean,

View File

@ -79,6 +79,7 @@ data class NodeConfigurationImpl(
override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType, override val jmxReporterType: JmxReporterType? = Defaults.jmxReporterType,
override val flowOverrides: FlowOverrideConfig?, override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist, override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val rotatedCordappSignerKeys: List<RotatedCorDappSignerKeyConfiguration> = Defaults.rotatedCordappSignerKeys,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? = override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings? =
Defaults.networkParameterAcceptanceSettings, Defaults.networkParameterAcceptanceSettings,
override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys, override val blacklistedAttachmentSigningKeys: List<String> = Defaults.blacklistedAttachmentSigningKeys,
@ -121,6 +122,7 @@ data class NodeConfigurationImpl(
val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS
val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType
val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() } val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() }
val rotatedCordappSignerKeys: List<RotatedCorDappSignerKeyConfiguration> = emptyList()
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings() val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
val blacklistedAttachmentSigningKeys: List<String> = emptyList() val blacklistedAttachmentSigningKeys: List<String> = emptyList()
const val flowExternalOperationThreadPoolSize: Int = 1 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.NodeRpcSettings
import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.PasswordEncryption 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
import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId
import net.corda.node.services.config.TelemetryConfiguration import net.corda.node.services.config.TelemetryConfiguration
@ -213,6 +214,14 @@ internal object FlowTimeoutConfigurationSpec : Configuration.Specification<FlowT
} }
} }
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 TelemetryConfigurationSpec : Configuration.Specification<TelemetryConfiguration>("TelemetryConfiguration") { internal object TelemetryConfigurationSpec : Configuration.Specification<TelemetryConfiguration>("TelemetryConfiguration") {
private val openTelemetryEnabled by boolean() private val openTelemetryEnabled by boolean()
private val simpleLogTelemetryEnabled by boolean() private val simpleLogTelemetryEnabled by boolean()

View File

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

View File

@ -3,6 +3,8 @@ package net.corda.testing.node
import com.google.common.collect.MutableClassToInstanceMap import com.google.common.collect.MutableClassToInstanceMap
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.cordapp.CordappProvider import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -118,8 +120,8 @@ open class MockServices private constructor(
) : ServiceHub { ) : ServiceHub {
companion object { companion object {
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, rotatedKeys: RotatedKeys = RotatedKeys()): CordappLoader {
return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo) return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo, rotatedKeys = rotatedKeys)
} }
/** /**
@ -494,6 +496,7 @@ open class MockServices private constructor(
override val cordappProvider: CordappProvider get() = mockCordappProvider override val cordappProvider: CordappProvider get() = mockCordappProvider
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService()
var rotatedKeys: RotatedKeys = CordaRotatedKeys.keys
protected val servicesForResolution: ServicesForResolution protected val servicesForResolution: ServicesForResolution
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions)

View File

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

View File

@ -17,6 +17,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.contracts.RotatedKeys
import net.corda.core.node.services.TransactionStorage import net.corda.core.node.services.TransactionStorage
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
@ -109,17 +110,20 @@ data class TestTransactionDSLInterpreter private constructor(
ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build() ThreadFactoryBuilder().setNameFormat("flow-external-operation-thread").build()
) )
override val rotatedKeys: RotatedKeys = (ledgerInterpreter.services as? ServiceHubCoreInternal)?.rotatedKeys ?: CordaRotatedKeys.keys
override val attachmentTrustCalculator: AttachmentTrustCalculator = override val attachmentTrustCalculator: AttachmentTrustCalculator =
ledgerInterpreter.services.attachments.let { ledgerInterpreter.services.attachments.let {
// Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api // Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api
// while still allowing the tests to work // while still allowing the tests to work
NodeAttachmentTrustCalculator( NodeAttachmentTrustCalculator(
attachmentStorage = if (it is MockAttachmentStorage) { attachmentStorage = if (it is MockAttachmentStorage) {
InternalMockAttachmentStorage(it) InternalMockAttachmentStorage(it)
} else { } else {
it as AttachmentStorageInternal it as AttachmentStorageInternal
}, },
cacheFactory = TestingNamedCacheFactory() cacheFactory = TestingNamedCacheFactory(),
rotatedKeys = rotatedKeys
) )
} }