From 72ac7224511b81f10770cbf8b3a016b32d7c9b5d Mon Sep 17 00:00:00 2001 From: James Higgs <45565019+JamesHR3@users.noreply.github.com> Date: Wed, 14 Aug 2019 13:24:56 +0100 Subject: [PATCH] [CORDA-3130] Add a cache for looking up external UUIDs from public keys (#5357) --- .../nodeapi/internal/KeyOwningIdentity.kt | 44 +++++++ .../PublicKeyToOwningIdentityCache.kt | 17 +++ .../net/corda/node/internal/AbstractNode.kt | 3 +- .../keys/BasicHSMKeyManagementService.kt | 33 ++--- .../keys/E2ETestKeyManagementService.kt | 26 ++-- .../keys/KeyManagementServiceInternal.kt | 49 ++++--- .../keys/PersistentKeyManagementService.kt | 30 ++--- .../persistence/PublicKeyHashToExternalId.kt | 26 ++++ .../PublicKeyToOwningIdentityCacheImpl.kt | 89 +++++++++++++ .../WritablePublicKeyToOwningIdentityCache.kt | 16 +++ .../node/services/schema/NodeSchemaService.kt | 2 +- .../corda/node/utilities/NodeNamedCache.kt | 1 + .../services/network/NetworkMapUpdaterTest.kt | 3 +- .../services/network/NodeInfoWatcherTest.kt | 5 +- .../PublicKeyToOwningIdentityCacheImplTest.kt | 121 ++++++++++++++++++ .../net/corda/testing/node/MockServices.kt | 18 ++- .../node/internal/InternalMockNetwork.kt | 4 +- .../node/internal/MockKeyManagementService.kt | 41 ++---- .../MockPublicKeyToOwningIdentityCache.kt | 23 ++++ 19 files changed, 428 insertions(+), 123 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyOwningIdentity.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/PublicKeyToOwningIdentityCache.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyHashToExternalId.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/WritablePublicKeyToOwningIdentityCache.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockPublicKeyToOwningIdentityCache.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyOwningIdentity.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyOwningIdentity.kt new file mode 100644 index 0000000000..990f3c9f04 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyOwningIdentity.kt @@ -0,0 +1,44 @@ +package net.corda.nodeapi.internal + +import java.util.* + +/** + * A [KeyOwningIdentity] represents an entity that owns a public key. In this case, the "owner" refers to either an identifier provided + * when the key was generated, or the node itself if no identifier was supplied. + */ +sealed class KeyOwningIdentity { + + abstract val uuid: UUID? + + /** + * [UnmappedIdentity] is used for keys that are not assigned a UUID. This is any key created on this node that did not have a UUID + * assigned to it on generation. These keys are the node identity key, or confidential identity keys. + */ + object UnmappedIdentity : KeyOwningIdentity() { + override fun toString(): String { + return "UNMAPPED_IDENTITY" + } + + override val uuid: UUID? = null + } + + /** + * [MappedIdentity] is used for keys that have an assigned UUID. Keys belonging to a mapped identity were assigned a UUID at the point + * they were generated. This UUID may refer to something outside the core of the node, for example an account. + */ + data class MappedIdentity(override val uuid: UUID) : KeyOwningIdentity() { + override fun toString(): String { + return uuid.toString() + } + } + + companion object { + fun fromUUID(uuid: UUID?): KeyOwningIdentity { + return if (uuid != null) { + MappedIdentity(uuid) + } else { + UnmappedIdentity + } + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/PublicKeyToOwningIdentityCache.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/PublicKeyToOwningIdentityCache.kt new file mode 100644 index 0000000000..e7f0e3ec3c --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/PublicKeyToOwningIdentityCache.kt @@ -0,0 +1,17 @@ +package net.corda.nodeapi.internal + +import java.security.PublicKey + +/** + * A [PublicKeyToOwningIdentityCache] maps public keys to their owners. In this case, an owner could be the node identity, or it could be + * an external identity. + */ +interface PublicKeyToOwningIdentityCache { + + /** + * Obtain the owning identity for a public key. + * + * If the key is unknown to the node, then this will return null. + */ + operator fun get(key: PublicKey): KeyOwningIdentity? +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index e086ca16ed..2457dbb4e1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -183,6 +183,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, @Suppress("LeakingThis") val networkParametersStorage = makeNetworkParametersStorage() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() + val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory) @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { @@ -818,7 +819,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService) + return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService, pkToIdCache) } open fun stop() { diff --git a/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt index 5f3014455d..e8cc7385d1 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt @@ -2,13 +2,14 @@ package net.corda.node.services.keys import net.corda.core.crypto.* import net.corda.core.crypto.internal.AliasPrivateKey -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.NamedCacheFactory import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.KeyOwningIdentity import net.corda.nodeapi.internal.cryptoservice.SignOnlyCryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX @@ -29,8 +30,11 @@ import kotlin.collections.LinkedHashSet * * This class needs database transactions to be in-flight during method calls and init. */ -class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService, - private val database: CordaPersistence, private val cryptoService: SignOnlyCryptoService) : SingletonSerializeAsToken(), KeyManagementServiceInternal { +class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, + override val identityService: PersistentIdentityService, + private val database: CordaPersistence, + private val cryptoService: SignOnlyCryptoService, + private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { @Entity @Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( @@ -93,33 +97,16 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identity identityService.stripNotOurKeys(candidateKeys) } - // Unlike initial keys, freshkey() is related confidential keys and it utilises platform's software key generation - // thus, without using [cryptoService]). - override fun freshKey(): PublicKey { + override fun freshKeyInternal(externalId: UUID?): PublicKey { val keyPair = generateKeyPair() database.transaction { keysMap[keyPair.public] = keyPair.private + pkToIdCache[keyPair.public] = KeyOwningIdentity.fromUUID(externalId) } return keyPair.public } - override fun freshKey(externalId: UUID): PublicKey { - val newKey = freshKey() - database.transaction { session.persist(PublicKeyHashToExternalId(externalId, newKey)) } - return newKey - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { - return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey)) - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate { - val newKeyWithCert = freshKeyAndCert(identity, revocationEnabled) - database.transaction { session.persist(PublicKeyHashToExternalId(externalId, newKeyWithCert.owningKey)) } - return newKeyWithCert - } - - private fun getSigner(publicKey: PublicKey): ContentSigner { + override fun getSigner(publicKey: PublicKey): ContentSigner { val signingPublicKey = getSigningPublicKey(publicKey) return if (signingPublicKey in originalKeysMap) { cryptoService.getSigner(originalKeysMap[signingPublicKey]!!) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index 81a83af33f..c5f38f0b8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -1,13 +1,12 @@ package net.corda.node.services.keys import net.corda.core.crypto.* -import net.corda.core.identity.PartyAndCertificate +import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.internal.ThreadBox import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.crypto.internal.AliasPrivateKey -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey @@ -27,7 +26,7 @@ import javax.annotation.concurrent.ThreadSafe * etc. */ @ThreadSafe -class E2ETestKeyManagementService(val identityService: IdentityService, private val cryptoService: CryptoService? = null) : SingletonSerializeAsToken(), KeyManagementServiceInternal { +class E2ETestKeyManagementService(override val identityService: IdentityService, private val cryptoService: CryptoService? = null) : SingletonSerializeAsToken(), KeyManagementServiceInternal { private class InnerState { val keys = HashMap() } @@ -53,7 +52,10 @@ class E2ETestKeyManagementService(val identityService: IdentityService, private } } - override fun freshKey(): PublicKey { + override fun freshKeyInternal(externalId: UUID?): PublicKey { + if (externalId != null) { + throw UnsupportedOperationException("This operation is only supported by persistent key management service variants.") + } val keyPair = generateKeyPair() mutex.locked { keys[keyPair.public] = keyPair.private @@ -61,19 +63,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService, private return keyPair.public } - override fun freshKey(externalId: UUID): PublicKey { - throw UnsupportedOperationException("This operation is only supported by persistent key management service variants.") - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { - return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey)) - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate { - throw UnsupportedOperationException("This operation is only supported by persistent key management service variants.") - } - - private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + override fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { return mutex.locked { diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KeyManagementServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/keys/KeyManagementServiceInternal.kt index 01d4e46c28..5f799b359b 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KeyManagementServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KeyManagementServiceInternal.kt @@ -1,32 +1,39 @@ package net.corda.node.services.keys -import net.corda.core.crypto.toStringShort +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService -import org.hibernate.annotations.Type +import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PublicKey import java.util.* -import javax.persistence.* interface KeyManagementServiceInternal : KeyManagementService { + + val identityService: IdentityService + fun start(initialKeyPairs: Set) + + fun freshKeyInternal(externalId: UUID?): PublicKey + + fun getSigner(publicKey: PublicKey): ContentSigner + + // Unlike initial keys, freshkey() is related confidential keys and it utilises platform's software key generation + // thus, without using [cryptoService]). + override fun freshKey(): PublicKey { + return freshKeyInternal(null) + } + + override fun freshKey(externalId: UUID): PublicKey { + return freshKeyInternal(externalId) + } + + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { + return freshCertificate(identityService, freshKeyInternal(null), identity, getSigner(identity.owningKey)) + } + + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate { + return freshCertificate(identityService, freshKeyInternal(externalId), identity, getSigner(identity.owningKey)) + } } -@Entity -@Table(name = "pk_hash_to_ext_id_map", indexes = [Index(name = "pk_hash_to_xid_idx", columnList = "public_key_hash")]) -class PublicKeyHashToExternalId( - @Id - @GeneratedValue - @Column(name = "id", unique = true, nullable = false) - val key: Long?, - - @Column(name = "external_id", nullable = false) - @Type(type = "uuid-char") - val externalId: UUID, - - @Column(name = "public_key_hash", nullable = false) - val publicKeyHash: String -) { - constructor(accountId: UUID, publicKey: PublicKey) - : this(null, accountId, publicKey.toStringShort()) -} diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 9095df45be..8742a0c068 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -1,13 +1,14 @@ package net.corda.node.services.keys import net.corda.core.crypto.* -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.toSet import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.KeyOwningIdentity import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY @@ -26,8 +27,10 @@ import javax.persistence.* * This class needs database transactions to be in-flight during method calls and init. */ @Deprecated("Superseded by net.corda.node.services.keys.BasicHSMKeyManagementService") -class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService, - private val database: CordaPersistence) : SingletonSerializeAsToken(), KeyManagementServiceInternal { +class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, + override val identityService: PersistentIdentityService, + private val database: CordaPersistence, + private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { @Entity @Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( @@ -76,31 +79,16 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi identityService.stripNotOurKeys(candidateKeys) } - override fun freshKey(): PublicKey { + override fun freshKeyInternal(externalId: UUID?): PublicKey { val keyPair = generateKeyPair() database.transaction { keysMap[keyPair.public] = keyPair.private + pkToIdCache[keyPair.public] = KeyOwningIdentity.fromUUID(externalId) } return keyPair.public } - override fun freshKey(externalId: UUID): PublicKey { - val newKey = freshKey() - database.transaction { session.persist(PublicKeyHashToExternalId(externalId, newKey)) } - return newKey - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { - return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey)) - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate { - val newKeyWithCert = freshKeyAndCert(identity, revocationEnabled) - database.transaction { session.persist(PublicKeyHashToExternalId(externalId, newKeyWithCert.owningKey)) } - return newKeyWithCert - } - - private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + override fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) //It looks for the PublicKey in the (potentially) CompositeKey that is ours, and then returns the associated PrivateKey to use in signing private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyHashToExternalId.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyHashToExternalId.kt new file mode 100644 index 0000000000..2a052464cc --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyHashToExternalId.kt @@ -0,0 +1,26 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.toStringShort +import org.hibernate.annotations.Type +import java.security.PublicKey +import java.util.* +import javax.persistence.* + +@Entity +@Table(name = "pk_hash_to_ext_id_map", indexes = [Index(name = "pk_hash_to_xid_idx", columnList = "public_key_hash")]) +class PublicKeyHashToExternalId( + @Id + @GeneratedValue + @Column(name = "id", unique = true, nullable = false) + val key: Long?, + + @Column(name = "external_id", nullable = false) + @Type(type = "uuid-char") + val externalId: UUID, + + @Column(name = "public_key_hash", nullable = false) + val publicKeyHash: String +) { + constructor(accountId: UUID, publicKey: PublicKey) + : this(null, accountId, publicKey.toStringShort()) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt new file mode 100644 index 0000000000..f6c06c8517 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt @@ -0,0 +1,89 @@ +package net.corda.node.services.persistence + +import com.github.benmanes.caffeine.cache.Caffeine +import net.corda.core.crypto.toStringShort +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.nodeapi.internal.KeyOwningIdentity +import net.corda.nodeapi.internal.persistence.CordaPersistence +import java.security.PublicKey +import java.util.* + +/** + * The [PublicKeyToOwningIdentityCacheImpl] provides a caching layer over the pk_hash_to_external_id table. Gets will attempt to read an + * external identity from the database if it is not present in memory, while sets will write external identity UUIDs to this database table. + */ +class PublicKeyToOwningIdentityCacheImpl(private val database: CordaPersistence, + cacheFactory: NamedCacheFactory) : WritablePublicKeyToOwningIdentityCache { + companion object { + val log = contextLogger() + } + + private val cache = cacheFactory.buildNamed(Caffeine.newBuilder(), "PublicKeyToOwningIdentityCache_cache") + + private fun isKeyBelongingToNode(key: PublicKey): Boolean { + return database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) + val queryRoot = criteriaQuery.from(BasicHSMKeyManagementService.PersistentKey::class.java) + criteriaQuery.select(criteriaBuilder.count(queryRoot)) + criteriaQuery.where( + criteriaBuilder.equal(queryRoot.get(BasicHSMKeyManagementService.PersistentKey::publicKeyHash.name), key.toStringShort()) + ) + val query = session.createQuery(criteriaQuery) + query.uniqueResult() > 0 + } + } + + /** + * Return the owning identity associated with a given key. + * + * This method caches the result of a database lookup to prevent multiple database accesses for the same key. This assumes that once a + * key is generated, the UUID assigned to it is never changed. + */ + override operator fun get(key: PublicKey): KeyOwningIdentity? { + return cache.asMap().computeIfAbsent(key) { + database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(UUID::class.java) + val queryRoot = criteriaQuery.from(PublicKeyHashToExternalId::class.java) + criteriaQuery.select(queryRoot.get(PublicKeyHashToExternalId::externalId.name)) + criteriaQuery.where( + criteriaBuilder.equal(queryRoot.get(PublicKeyHashToExternalId::publicKeyHash.name), key.toStringShort()) + ) + val query = session.createQuery(criteriaQuery) + val uuid = query.uniqueResult() + if (uuid != null || isKeyBelongingToNode(key)) { + val signingEntity = KeyOwningIdentity.fromUUID(uuid) + log.debug { "Database lookup for public key ${key.toStringShort()}, found signing entity $signingEntity" } + signingEntity + } else { + log.debug { "Attempted to find owning identity for public key ${key.toStringShort()}, but key is unknown to node" } + null + } + } + } + } + + /** + * Associate a key with a given [KeyOwningIdentity]. + * + * This will write to the pk_hash_to_external_id table if the key belongs to an external id. If a key is created within a transaction + * that is rolled back in the future, the cache may contain stale entries. However, that key should be missing from the + * KeyManagementService in that case, and so querying it from this cache should not occur (as the key is inaccessible). + * + * The same key should not be written twice. + */ + override operator fun set(key: PublicKey, value: KeyOwningIdentity) { + when (value) { + is KeyOwningIdentity.MappedIdentity -> { + database.transaction { session.persist(PublicKeyHashToExternalId(value.uuid, key)) } + } + is KeyOwningIdentity.UnmappedIdentity -> { + } + } + cache.asMap()[key] = value + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/WritablePublicKeyToOwningIdentityCache.kt b/node/src/main/kotlin/net/corda/node/services/persistence/WritablePublicKeyToOwningIdentityCache.kt new file mode 100644 index 0000000000..778163c45a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/WritablePublicKeyToOwningIdentityCache.kt @@ -0,0 +1,16 @@ +package net.corda.node.services.persistence + +import net.corda.nodeapi.internal.KeyOwningIdentity +import net.corda.nodeapi.internal.PublicKeyToOwningIdentityCache +import java.security.PublicKey + +/** + * Internal only version of a [PublicKeyToOwningIdentityCache] that allows writing to the cache and underlying database table + */ +interface WritablePublicKeyToOwningIdentityCache : PublicKeyToOwningIdentityCache { + + /** + * Assign a public key to an owning identity. + */ + operator fun set(key: PublicKey, value: KeyOwningIdentity) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 371541ebbc..a41bcbcde7 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -15,11 +15,11 @@ import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.PersistentKeyManagementService -import net.corda.node.services.keys.PublicKeyHashToExternalId import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.VaultSchemaV1 diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index de223b680b..4653a4d9b4 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -59,6 +59,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "RaftUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize) name == "BasicHSMKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) name == "NodeParametersStorage_networkParametersByHash" -> caffeine.maximumSize(defaultCacheSize) + name == "PublicKeyToOwningIdentityCache_cache" -> caffeine.maximumSize(defaultCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index fd79d49b61..2a3c641af8 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -33,6 +33,7 @@ import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.internal.MockKeyManagementService +import net.corda.testing.node.internal.MockPublicKeyToOwningIdentityCache import net.corda.testing.node.internal.network.NetworkMapServer import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -102,7 +103,7 @@ class NetworkMapUpdaterTest { server.networkParameters.serialize().hash, ourNodeInfo, networkParameters, - MockKeyManagementService(makeTestIdentityService(), ourKeyPair), + MockKeyManagementService(makeTestIdentityService(), ourKeyPair, pkToIdCache = MockPublicKeyToOwningIdentityCache()), NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters)) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 8a02e9e86f..8ac3ad07d9 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -2,17 +2,18 @@ package net.corda.node.services.network import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs +import net.corda.core.internal.NODE_INFO_DIRECTORY import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.size import net.corda.core.node.services.KeyManagementService -import net.corda.core.internal.NODE_INFO_DIRECTORY import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.internal.MockKeyManagementService +import net.corda.testing.node.internal.MockPublicKeyToOwningIdentityCache import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -49,7 +50,7 @@ class NodeInfoWatcherTest { fun start() { nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) val identityService = makeTestIdentityService() - keyManagementService = MockKeyManagementService(identityService) + keyManagementService = MockKeyManagementService(identityService, pkToIdCache = MockPublicKeyToOwningIdentityCache()) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) nodeInfoPath = tempFolder.root.toPath() / NODE_INFO_DIRECTORY } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt new file mode 100644 index 0000000000..1226a3f36e --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt @@ -0,0 +1,121 @@ +package net.corda.node.services.persistence + +import junit.framework.TestCase.assertEquals +import net.corda.core.crypto.generateKeyPair +import net.corda.core.node.services.KeyManagementService +import net.corda.core.utilities.getOrThrow +import net.corda.nodeapi.internal.KeyOwningIdentity +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.node.MockServices +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.security.PublicKey +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class PublicKeyToOwningIdentityCacheImplTest { + + private lateinit var database: CordaPersistence + private lateinit var testCache: PublicKeyToOwningIdentityCacheImpl + private lateinit var keyManagementService: KeyManagementService + private val testKeys = mutableListOf>() + private val alice = TestIdentity(ALICE_NAME, 20) + private lateinit var executor: ExecutorService + + @Before + fun setUp() { + val databaseAndServices = MockServices.makeTestDatabaseAndPersistentServices( + listOf(), + alice, + testNetworkParameters(), + emptySet(), + emptySet() + ) + database = databaseAndServices.first + testCache = PublicKeyToOwningIdentityCacheImpl(database, TestingNamedCacheFactory()) + keyManagementService = databaseAndServices.second.keyManagementService + createTestKeys() + executor = Executors.newFixedThreadPool(2) + } + + @After + fun tearDown() { + database.close() + executor.shutdown() + } + + private fun createTestKeys() { + val duplicatedUUID = UUID.randomUUID() + val uuids = listOf(UUID.randomUUID(), UUID.randomUUID(), null, null, duplicatedUUID, duplicatedUUID) + uuids.forEach { + val key = if (it != null) { + keyManagementService.freshKey(it) + } else { + keyManagementService.freshKey() + } + testKeys.add(Pair(KeyOwningIdentity.fromUUID(it), key)) + } + } + + private fun performTestRun() { + for ((keyOwningIdentity, key) in testKeys) { + assertEquals(keyOwningIdentity, testCache[key]) + } + } + + @Test + fun `cache returns right key for each UUID`() { + performTestRun() + } + + @Test + fun `querying for key twice does not go to database the second time`() { + performTestRun() + + withoutDatabaseAccess { + performTestRun() + } + } + + @Test + fun `entries can be fetched if cache invalidated`() { + testCache = PublicKeyToOwningIdentityCacheImpl(database, TestingNamedCacheFactory(sizeOverride = 0)) + + performTestRun() + } + + @Test + fun `cache access is thread safe`() { + val executor = Executors.newFixedThreadPool(2) + val f1 = executor.submit { performTestRun() } + val f2 = executor.submit { performTestRun() } + f2.getOrThrow() + f1.getOrThrow() + } + + private fun createAndAddKeys() { + keyManagementService.freshKey(UUID.randomUUID()) + } + + @Test + fun `can set multiple keys across threads`() { + val executor = Executors.newFixedThreadPool(2) + val f1 = executor.submit { repeat(5) { createAndAddKeys() } } + val f2 = executor.submit { repeat(5) { createAndAddKeys() } } + f2.getOrThrow() + f1.getOrThrow() + } + + @Test + fun `requesting a key unknown to the node returns null`() { + val keys = generateKeyPair() + assertEquals(null, testCache[keys.public]) + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index bda6cbe7c5..0b6c28e37d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -21,16 +21,17 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo -import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.node.internal.ServicesForResolutionImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.* import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.PersistentKeyManagementService +import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService +import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.contextTransaction @@ -75,7 +76,11 @@ open class MockServices private constructor( private val initialNetworkParameters: NetworkParameters, private val initialIdentity: TestIdentity, private val moreKeys: Array, - override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *arrayOf(initialIdentity.keyPair) + moreKeys) + override val keyManagementService: KeyManagementService = MockKeyManagementService( + identityService, + *arrayOf(initialIdentity.keyPair) + moreKeys, + pkToIdCache = MockPublicKeyToOwningIdentityCache() + ) ) : ServiceHub { companion object { @@ -120,7 +125,11 @@ open class MockServices private constructor( val dataSourceProps = makeTestDataSourceProperties() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas()) - val keyManagementService = MockKeyManagementService(identityService, *arrayOf(initialIdentity.keyPair) + moreKeys) + val keyManagementService = MockKeyManagementService( + identityService, + *arrayOf(initialIdentity.keyPair) + moreKeys, + pkToIdCache = MockPublicKeyToOwningIdentityCache() + ) val mockService = database.transaction { makeMockMockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys.toSet(), keyManagementService, schemaService, database) } @@ -163,7 +172,8 @@ open class MockServices private constructor( // Create a persistent key management service and add the key pair which was created for the TestIdentity. // We only add the keypair for the initial identity and any other keys which this node may control. Note: We don't add the keys // for the other identities. - val keyManagementService = PersistentKeyManagementService(TestingNamedCacheFactory(), identityService, persistence) + val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(persistence, TestingNamedCacheFactory()) + val keyManagementService = PersistentKeyManagementService(TestingNamedCacheFactory(), identityService, persistence, pkToIdCache) persistence.transaction { keyManagementService.start(moreKeys + initialIdentity.keyPair) } val mockService = persistence.transaction { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 3d0c786651..4881576fea 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -36,7 +36,6 @@ import net.corda.node.services.config.* import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.MessagingService import net.corda.node.services.persistence.NodeAttachmentService @@ -46,6 +45,7 @@ import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.User +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -373,7 +373,7 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), } override fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal { - return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService) + return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService, pkToIdCache) } override fun startShell() { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockKeyManagementService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockKeyManagementService.kt index de84cd4ce3..0537412073 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockKeyManagementService.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockKeyManagementService.kt @@ -1,63 +1,46 @@ package net.corda.testing.node.internal import net.corda.core.crypto.* -import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.node.services.keys.freshCertificate +import net.corda.node.services.keys.KeyManagementServiceInternal +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache +import net.corda.nodeapi.internal.KeyOwningIdentity import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.util.* -import java.util.concurrent.ConcurrentHashMap /** * A class which provides an implementation of [KeyManagementService] which is used in [MockServices] * * @property identityService The [IdentityService] which contains the given identities. */ -class MockKeyManagementService(val identityService: IdentityService, - vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { +class MockKeyManagementService(override val identityService: IdentityService, + vararg initialKeys: KeyPair, + private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) override val keys: Set get() = keyStore.keys private val nextKeys = LinkedList() - val keysById: MutableMap> = ConcurrentHashMap() - - override fun freshKey(): PublicKey { + override fun freshKeyInternal(externalId: UUID?): PublicKey { val k = nextKeys.poll() ?: generateKeyPair() keyStore[k.public] = k.private + pkToIdCache[k.public] = KeyOwningIdentity.fromUUID(externalId) return k.public } - private fun mapKeyToId(publicKey: PublicKey, externalId: UUID) { - val keysForId = keysById.getOrPut(externalId) { emptySet() } - keysById[externalId] = keysForId + publicKey - } - - override fun freshKey(externalId: UUID): PublicKey { - val key = freshKey() - mapKeyToId(key, externalId) - return key - } - - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate { - val keyAndCert = freshKeyAndCert(identity, revocationEnabled) - mapKeyToId(keyAndCert.owningKey, externalId) - return keyAndCert - } - override fun filterMyKeys(candidateKeys: Iterable): Iterable = candidateKeys.filter { it in this.keys } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { - return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey)) - } + override fun getSigner(publicKey: PublicKey): ContentSigner = net.corda.node.services.keys.getSigner(getSigningKeyPair(publicKey)) - private fun getSigner(publicKey: PublicKey): ContentSigner = net.corda.node.services.keys.getSigner(getSigningKeyPair(publicKey)) + override fun start(initialKeyPairs: Set) { + initialKeyPairs.forEach { keyStore[it.public] = it.private } + } private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockPublicKeyToOwningIdentityCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockPublicKeyToOwningIdentityCache.kt new file mode 100644 index 0000000000..c8acf07faf --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockPublicKeyToOwningIdentityCache.kt @@ -0,0 +1,23 @@ +package net.corda.testing.node.internal + +import net.corda.core.internal.toSynchronised +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache +import net.corda.nodeapi.internal.KeyOwningIdentity +import java.security.PublicKey + +/** + * A mock implementation of [WritablePublicKeyToOwningIdentityCache] that stores all key mappings in memory. Used in testing scenarios that do not + * require database access. + */ +class MockPublicKeyToOwningIdentityCache : WritablePublicKeyToOwningIdentityCache { + + private val cache: MutableMap = mutableMapOf().toSynchronised() + + override fun get(key: PublicKey): KeyOwningIdentity? { + return cache[key] + } + + override fun set(key: PublicKey, value: KeyOwningIdentity) { + cache[key] = value + } +} \ No newline at end of file