mirror of
https://github.com/corda/corda.git
synced 2024-12-23 14:52:29 +00:00
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:
commit
5d0593c20f
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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}"
|
||||||
|
@ -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) {
|
||||||
|
@ -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())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
117
core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt
Normal file
117
core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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>) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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>()
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user