Backport contract key rotation to 4.9.

This commit is contained in:
Adel El-Beik 2024-10-09 14:46:57 +01:00
parent 282ee95188
commit 828066a646
15 changed files with 46 additions and 39 deletions

View File

@ -77,6 +77,9 @@ def patchCore = tasks.register('patchCore', Zip) {
exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class'
exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class'
exclude 'net/corda/core/internal/rules/*.class'
exclude 'net/corda/core/contracts/CordaRotatedKeys.class'
exclude 'net/corda/core/contracts/RotatedKeysKt.class'
exclude 'net/corda/core/contracts/RotatedKeys.class'
}
reproducibleFileOrder = true

View File

@ -1,18 +1,14 @@
package net.corda.core.contracts
import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
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
@DeleteForDJVM
object CordaRotatedKeys {
val keys = RotatedKeys()
}
@ -47,8 +43,7 @@ private val CORDA_SIGNING_KEY_ROTATIONS = listOf(
*
*/
@CordaSerializable
@DeleteForDJVM
data class RotatedKeys(val rotatedSigningKeys: List<List<SecureHash>> = emptyList()): SingletonSerializeAsToken() {
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 ->

View File

@ -1,6 +1,5 @@
package net.corda.coretests.contracts
<
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever

View File

@ -1,18 +1,19 @@
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.internal.verification.NodeVerificationSupport
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.node.services.persistence.toInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
@ -30,8 +31,6 @@ import net.corda.testing.node.MockServices
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import java.net.URL
import kotlin.test.assertFailsWith
@ -49,12 +48,11 @@ class AttachmentsClassLoaderWithStoragePersistenceTests {
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
private lateinit var attachmentTrustCalculator2: AttachmentTrustCalculator
private val networkParameters = testNetworkParameters()
private val cacheFactory = TestingNamedCacheFactory(1)
private val cacheFactory2 = TestingNamedCacheFactory()
private val nodeVerificationSupport = rigorousMock<NodeVerificationSupport>().also {
private val services = rigorousMock<ServicesForResolution>().also {
doReturn(testNetworkParameters()).whenever(it).networkParameters
}
@ -86,8 +84,7 @@ class AttachmentsClassLoaderWithStoragePersistenceTests {
it.start()
}
}
storage.nodeVerificationSupport = nodeVerificationSupport
attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory)
storage.servicesForResolution = services
attachmentTrustCalculator2 = NodeAttachmentTrustCalculator(storage, database, cacheFactory2)
}

View File

@ -1,6 +1,5 @@
package net.corda.core.node.services
package net.corda.core.contracts
import net.corda.core.KeepForDJVM
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
@ -45,7 +44,6 @@ private val CORDA_SIGNING_KEY_ROTATIONS = listOf(
*
*/
@CordaSerializable
@KeepForDJVM
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 {

View File

@ -3,6 +3,7 @@ package net.corda.core.internal
import net.corda.core.contracts.*
import net.corda.core.crypto.keys
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.contracts.RotatedKeys
import net.corda.core.utilities.loggerFor
/**

View File

@ -90,6 +90,7 @@ abstract class AbstractVerifier(
/**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
* wrong object instance. This class helps
*/
private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, private val rotatedKeys: RotatedKeys) {
private val inputStates: List<TransactionState<*>> = ltx.inputs.map(StateAndRef<ContractState>::state)
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs
@ -438,7 +439,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
private fun verifyConstraintUsingRotatedKeys(constraint: AttachmentConstraint, constraintAttachment: AttachmentWithContext, contract: ContractClassName ) {
// constraint could be an input constraint so we manually have to rotate to updated constraint
if (constraint is SignatureAttachmentConstraint && rotatedKeys.canBeTransitioned(constraint.key, constraintAttachment.signerKeys)) {
val constraintWithRotatedKeys = SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build())
val constraintWithRotatedKeys = SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(constraintAttachment.signerKeys).build())
if (!constraintWithRotatedKeys.isSatisfiedBy(constraintAttachment)) throw ContractConstraintRejection(ltx.id, contract)
}
else {

View File

@ -171,6 +171,12 @@ interface ServiceHub : ServicesForResolution {
*/
val transactionVerifierService: TransactionVerifierService
/**
* INTERNAL. DO NOT USE.
* @suppress
*/
val rotatedKeys: RotatedKeys
/**
* A [Clock] representing the node's current time. This should be used in preference to directly accessing the
* clock so the current time can be controlled during unit testing.

View File

@ -2,7 +2,6 @@ package net.corda.core.serialization.internal
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.CordaRotatedKeys
@ -359,7 +358,7 @@ object AttachmentsClassLoaderBuilder {
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
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
val serializers = try {
@ -539,7 +538,7 @@ class AttachmentsClassLoaderSimpleCacheImpl(cacheSize: Int, override val rotated
}
class AttachmentsClassLoaderForRotatedKeysOnlyImpl(override val rotatedKeys: RotatedKeys = CordaRotatedKeys.keys) : AttachmentsClassLoaderCache {
override fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: (AttachmentsClassLoaderKey) -> SerializationContext): SerializationContext {
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")
}
}

View File

@ -15,7 +15,9 @@ import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentId
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.node.services.KeyManagementService
import net.corda.core.contracts.RotatedKeys
import net.corda.core.serialization.CustomSerializationScheme
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
@ -524,11 +526,11 @@ open class TransactionBuilder(
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
val (defaultOutputConstraint, constraintAttachment) = selectDefaultOutputConstraintAndConstraintAttachment(contractClassName,
inputStates, selectedAttachment.currentAttachment, serviceHub)
inputStates, attachmentToUse, services)
// Sanity check that the selected attachment actually passes.
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
"Selected output constraint: $defaultOutputConstraint not satisfying $attachmentToUse"
}
val resolvedOutputStates = outputStates.map {
@ -538,7 +540,7 @@ open class TransactionBuilder(
} else {
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
inputStates?.forEach { input ->
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment, serviceHub.toVerifyingServiceHub().rotatedKeys)) {
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse, getRotatedKeys(serviceHub))) {
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
}
}
@ -550,6 +552,14 @@ open class TransactionBuilder(
return Pair(selectedAttachmentId, resolvedOutputStates)
}
private fun getRotatedKeys(services: ServiceHub?): RotatedKeys {
return services?.rotatedKeys ?: CordaRotatedKeys.keys.also {
log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
"rotated keys defined in configuration. If you are writing a unit test then pass in a " +
"MockServices instance.")
}
}
private fun selectDefaultOutputConstraintAndConstraintAttachment( contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?,
attachmentToUse: ContractAttachment,
@ -564,7 +574,7 @@ open class TransactionBuilder(
if (!defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
// The defaultOutputConstraint is the input constraint by the attachment in use currently may have a rotated key
if (defaultOutputConstraint is SignatureAttachmentConstraint && services.toVerifyingServiceHub().rotatedKeys.canBeTransitioned(defaultOutputConstraint.key, constraintAttachment.signerKeys)) {
if (defaultOutputConstraint is SignatureAttachmentConstraint && (getRotatedKeys(serviceHub).canBeTransitioned(defaultOutputConstraint.key, constraintAttachment.signerKeys))) {
return Pair(makeSignatureAttachmentConstraint(attachmentToUse.signerKeys), constraintAttachment)
}
}

View File

@ -6,15 +6,12 @@ import net.corda.core.KeepForDJVM
import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP
import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.RotatedKeys
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionState
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash

View File

@ -5,6 +5,7 @@ import net.corda.core.contracts.Attachment
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.CommandWithParties
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
@ -54,7 +55,8 @@ class LtxSupplierFactory : Function<Array<out Any?>, Supplier<LedgerTransaction>
privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt,
networkParameters = networkParameters,
references = referencesProvider.get(),
digestService = txArgs[TX_DIGEST_SERVICE] as DigestService
digestService = txArgs[TX_DIGEST_SERVICE] as DigestService,
rotatedKeys = CordaRotatedKeys.keys
)
}
}

View File

@ -30,9 +30,8 @@ import org.apache.commons.io.FileUtils.deleteDirectory
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import kotlin.io.path.div
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import kotlin.test.assertEquals
class ContractWithRotatedKeyTest {
@ -62,7 +61,7 @@ class ContractWithRotatedKeyTest {
): TestStartedNode {
node.internals.disableDBCloseOnStop()
node.dispose()
val cordappsDir = network.baseDirectory(node) / "cordapps"
val cordappsDir = network.baseDirectory(node).resolve("cordapps")
deleteDirectory(cordappsDir.toFile())
return network.createNode(
parameters.copy(legalName = node.internals.configuration.myLegalName, forcedID = node.internals.id),
@ -75,8 +74,8 @@ class ContractWithRotatedKeyTest {
val keyStoreDir1 = SelfCleaningDir()
val keyStoreDir2 = SelfCleaningDir()
val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="1-testcordapp-rsa")
val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="1-testcordapp-rsa")
val 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)

View File

@ -6,6 +6,7 @@ import io.github.classgraph.ScanResult
import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.common.logging.errorReporting.ErrorCode
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.CordaRotatedKeys
import net.corda.core.contracts.RotatedKeys
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash
@ -90,9 +91,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
* @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(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList(), rotatedKeys: RotatedKeys = CordaRotatedKeys.keys): JarScanningCordappLoader {
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)

View File

@ -10,7 +10,6 @@ import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.notary.NotaryService
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId