From 1c2c3d3fed24a8f6e2488e474bdff5540cd00e43 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Mon, 16 Sep 2019 13:14:12 +0100 Subject: [PATCH] Identity service refactor for confidential-identities and accounts (#5434) * Removed IdentityServiceInternal as it is no longer used. * Removed externalIdForPublicKey API from KMS and added it to IdentityService. Added a registerKeyToExternalId API on IdentityService. * Fix remaining compile errors. * Removed "registerKeyToParty" and in its place added a new registerKey method which takes a PublicKey, Party and optionally a UUID. Added a cache to the "PersistentIdentityService" to store other node's public keys. Added the cache and new hibernate entity to all teh places where one needs to add them. New keys created by teh node now automatically get associated entries in the KEY -> PARTY map and optionally the KEy -> EXT ID map. Added a test. * Removed old comments and TODOs. * Fixed broken test. Added comments/explanations for what's going on in IdentityService. Updated kdocs. * First try at Implementing publicKeysForExternalId. * Fixed broken test. * Added migration. Amended existing persistent identity service migration to handle new migration. Addressed some review comments. * Fixed broken test - whoops! * Implemented mock identity service methods. * Added back exception when remapping a key to a different party. * Fixed compile errors. Fixed broken tests. * Use set instead of first entry in ourNames. --- .../flows/CollectSignaturesFlowTests.kt | 1 - .../core/node/services/IdentityService.kt | 33 +++-- .../node/services/KeyManagementService.kt | 9 -- .../net/corda/node/internal/AbstractNode.kt | 6 +- .../migration/MigrationNamedCacheFactory.kt | 1 + .../migration/PersistentIdentityMigration.kt | 7 +- .../PersistentIdentityMigrationNewTable.kt | 58 ++++---- .../node/migration/VaultStateMigration.kt | 1 + .../services/api/IdentityServiceInternal.kt | 84 ----------- .../identity/InMemoryIdentityService.kt | 52 +++++-- .../identity/PersistentIdentityService.kt | 130 ++++++++++++++++-- .../keys/BasicHSMKeyManagementService.kt | 24 ++-- .../keys/E2ETestKeyManagementService.kt | 4 - .../net/corda/node/services/keys/KMSUtils.kt | 7 +- .../node/services/schema/NodeSchemaService.kt | 1 + .../corda/node/utilities/NodeNamedCache.kt | 1 + .../migration/node-core.changelog-master.xml | 3 + .../node-core.changelog-v15-table.xml | 19 +++ .../PersistentIdentityServiceTests.kt | 24 ++-- .../services/network/NetworkMapUpdaterTest.kt | 2 +- .../services/network/NodeInfoWatcherTest.kt | 2 +- .../HibernateColumnConverterTests.kt | 5 +- .../services/vault/ExternalIdMappingTest.kt | 84 +++++++++-- .../net/corda/testing/node/MockServices.kt | 23 ++-- .../node/internal/InternalMockNetwork.kt | 2 +- .../node/internal/MockKeyManagementService.kt | 18 +-- 26 files changed, 378 insertions(+), 223 deletions(-) delete mode 100644 node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v15-table.xml diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt index d6c82fcde6..4306c6f003 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.* import net.corda.core.node.services.IdentityService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* import net.corda.testing.internal.matchers.flow.willReturn diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index cf6a886a12..c74c50fdec 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import co.paralleluniverse.fibers.Suspendable import net.corda.core.CordaException import net.corda.core.DoNotImplement import net.corda.core.contracts.PartyAndReference @@ -10,6 +11,7 @@ import net.corda.core.utilities.contextLogger import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.* +import java.util.* /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus @@ -69,8 +71,6 @@ interface IdentityService { * @param owningKey The [PublicKey] to determine well known identity for. * @return the party and certificate, or null if unknown. */ - @Deprecated("This method has been deprecated in favour of using a new way to generate and use confidential identities. See the new " + - "confidential identities repository.") fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate? /** @@ -153,15 +153,32 @@ interface IdentityService { /** * Registers a mapping in the database between the provided [PublicKey] and [Party] if one does not already exist. If an entry - * exists for the supplied [PublicKey] but the associated [Party] does not match the one supplied to the method then an exception will - * be thrown. + * exists for the supplied [PublicKey] but the associated [Party] does not match the one supplied to the method then a warning will be + * logged and the operation will not be carried out as a key can only ever be registered to one [Party]. * - * @param key The public key that will be registered to the supplied [Party] - * @param party The party that the supplied public key will be registered to - * @throws IllegalArgumentException if the public key is already registered to a party that does not match the supplied party + * This method also optionally adds a mapping from [PublicKey] to external ID if one is provided. Lastly, the [PublicKey] is + * also stored (as well as the [PublicKey] hash). + * + * @param publicKey The public publicKey that will be registered to the supplied [Party] + * @param party The party that the supplied public publicKey will be registered to + * @param externalId The [UUID] that the supplied public key can be optionally registered to + * @throws IllegalArgumentException if the public publicKey is already registered to a party that does not match the supplied party */ @Throws(IllegalArgumentException::class) - fun registerKeyToParty(key: PublicKey, party: Party) + fun registerKey(publicKey: PublicKey, party: Party, externalId: UUID? = null) + + /** + * This method allows lookups of [PublicKey]s to an associated "external ID" / [UUID]. Providing a [PublicKey] that is unknown by the node + * or is not mapped to an external ID will return null. Otherwise, if the [PublicKey] has been mapped to an external ID, then the [UUID] + * for that external ID will be returned. The method looks up keys generated by the node as well as keys generated on other nodes and + * registered with the [IdentityService]. + * + * @param publicKey the [PublicKey] used to perform the lookup to external ID + */ + @Suspendable + fun externalIdForPublicKey(publicKey: PublicKey): UUID? + + fun publicKeysForExternalId(externalId: UUID): Iterable } class UnknownAnonymousPartyException(message: String) : CordaException(message) diff --git a/core/src/main/kotlin/net/corda/core/node/services/KeyManagementService.kt b/core/src/main/kotlin/net/corda/core/node/services/KeyManagementService.kt index 8b75e31b67..9f81abf51a 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/KeyManagementService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/KeyManagementService.kt @@ -90,13 +90,4 @@ interface KeyManagementService { */ @Suspendable fun sign(signableData: SignableData, publicKey: PublicKey): TransactionSignature - - /** - * This method allows lookups of [PublicKey]s to an associated "external ID" / [UUID]. Providing a [PublicKey] that is unknown by the node - * or is not mapped to an external ID will return null. Otherwise, if the [PublicKey] has been mapped to an external ID, then the [UUID] - * for that external ID will be returned. - * @param publicKey the [PublicKey] used to perform the lookup to external ID - */ - @Suspendable - fun externalIdForPublicKey(publicKey: PublicKey): UUID? } \ 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 9da8c93e3e..242eecc32d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -304,7 +304,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, startDatabase() val (identity, identityKeyPair) = obtainIdentity() val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] - identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) + identityService.start(trustRoot, listOf(identity.certificate, nodeCa), pkToIdCache = pkToIdCache) return database.use { it.transaction { val (_, nodeInfoAndSigned) = updateNodeInfo(identity, identityKeyPair, publish = false) @@ -359,7 +359,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, X509Utilities.validateCertPath(trustRoot, identity.certPath) val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] - identityService.start(trustRoot, listOf(identity.certificate, nodeCa), netParams.notaries.map { it.identity }) + identityService.start(trustRoot, listOf(identity.certificate, nodeCa), netParams.notaries.map { it.identity }, pkToIdCache) val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction { updateNodeInfo(identity, identityKeyPair, publish = true) @@ -842,7 +842,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, pkToIdCache) + return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService) } open fun stop() { diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt index c321aeff1c..70bb911106 100644 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt +++ b/node/src/main/kotlin/net/corda/node/migration/MigrationNamedCacheFactory.kt @@ -30,6 +30,7 @@ class MigrationNamedCacheFactory(private val metricRegistry: MetricRegistry?, "PersistentIdentityService_keyToPartyAndCert" -> caffeine.maximumSize(defaultCacheSize) "PersistentIdentityService_nameToKey" -> caffeine.maximumSize(defaultCacheSize) "PersistentIdentityService_keyToName" -> caffeine.maximumSize(defaultCacheSize) + "PersistentIdentityService_hashToKey" -> caffeine.maximumSize(defaultCacheSize) "BasicHSMKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(defaultCacheSize) "NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(defaultCacheSize) diff --git a/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigration.kt b/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigration.kt index 3fe4871d70..93db88b4ca 100644 --- a/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigration.kt @@ -45,7 +45,12 @@ class PersistentIdentityMigration : CustomSqlChange { val oldPkHash = resultSet.getString(1) val identityBytes = resultSet.getBytes(2) val partyAndCertificate = PartyAndCertificate(X509CertificateFactory().delegate.generateCertPath(identityBytes.inputStream())) - generatedStatements.addAll(MigrationData(oldPkHash, partyAndCertificate).let { listOf(updateHashToIdentityRow(it, dataSource), updateNameToHashRow(it, dataSource)) }) + generatedStatements.addAll(MigrationData(oldPkHash, partyAndCertificate).let { + listOf( + updateHashToIdentityRow(it, dataSource), + updateNameToHashRow(it, dataSource) + ) + }) } return generatedStatements.toTypedArray() } diff --git a/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigrationNewTable.kt b/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigrationNewTable.kt index 6992ee3278..6cf3d6056b 100644 --- a/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigrationNewTable.kt +++ b/node/src/main/kotlin/net/corda/node/migration/PersistentIdentityMigrationNewTable.kt @@ -4,10 +4,7 @@ import liquibase.database.Database import liquibase.database.jvm.JdbcConnection import liquibase.exception.ValidationErrors import liquibase.resource.ResourceAccessor -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SignatureScheme import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger @@ -16,18 +13,8 @@ import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA -import net.corda.nodeapi.internal.DEV_ROOT_CA -import net.corda.nodeapi.internal.createDevNodeCa -import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair -import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory -import net.corda.nodeapi.internal.crypto.X509Utilities -import java.math.BigInteger -import java.security.KeyPair import java.security.PublicKey -import java.security.cert.X509Certificate -import java.util.* /** * Migration that reads data from the [PersistentIdentityCert] table, extracts the parameters required to insert into the [PersistentIdentity] table. @@ -41,42 +28,54 @@ class PersistentIdentityMigrationNewTable : CordaMigration() { logger.info("Migrating persistent identities with certificates table into persistent table with no certificate data.") if (database == null) { - logger.error("Cannot migrate persistent states: Liquibase failed to provide a suitable database connection") - throw PersistentIdentitiesMigrationException("Cannot migrate persistent states as liquibase failed to provide a suitable database connection") + logger.error("Cannot migrate persistent identities: Liquibase failed to provide a suitable database connection") + throw PersistentIdentitiesMigrationException("Cannot migrate persistent identities as liquibase failed to provide a suitable database connection") } initialiseNodeServices(database, setOf(PersistentIdentitiesMigrationSchemaBuilder.getMappedSchema())) val connection = database.connection as JdbcConnection - val keyPartiesMap = extractKeyParties(connection) + val hashToKeyAndName = extractKeyAndName(connection) - keyPartiesMap.forEach { + hashToKeyAndName.forEach { insertEntry(connection, it) } } - private fun extractKeyParties(connection: JdbcConnection): Map { - val keyParties = mutableMapOf() + private fun extractKeyAndName(connection: JdbcConnection): Map> { + val rows = mutableMapOf>() connection.createStatement().use { val rs = it.executeQuery("SELECT pk_hash, identity_value FROM node_identities WHERE pk_hash IS NOT NULL") while (rs.next()) { - val key = rs.getString(1) - val partyBytes = rs.getBytes(2) - val name = PartyAndCertificate(X509CertificateFactory().delegate.generateCertPath(partyBytes.inputStream())).party.name - keyParties.put(key, name) + val publicKeyHash = rs.getString(1) + val certificateBytes = rs.getBytes(2) + // Deserialise certificate. + val certPath = X509CertificateFactory().delegate.generateCertPath(certificateBytes.inputStream()) + val partyAndCertificate = PartyAndCertificate(certPath) + // Record name and public key. + val publicKey = partyAndCertificate.certificate.publicKey + val name = partyAndCertificate.name + rows[publicKeyHash] = Pair(name, publicKey) } rs.close() } - return keyParties + return rows } - private fun insertEntry(connection: JdbcConnection, entry: Map.Entry) { - val pk = entry.key - val name = entry.value.toString() + private fun insertEntry(connection: JdbcConnection, entry: Map.Entry>) { + val publicKeyHash = entry.key + val (name, publicKey) = entry.value connection.prepareStatement("INSERT INTO node_identities_no_cert (pk_hash, name) VALUES (?,?)").use { - it.setString(1, pk) - it.setString(2, name) + it.setString(1, publicKeyHash) + it.setString(2, name.toString()) it.executeUpdate() } + if (name !in identityService.ourNames) { + connection.prepareStatement("INSERT INTO node_hash_to_key (pk_hash, public_key) VALUES (?,?)").use { + it.setString(1, publicKeyHash) + it.setBytes(2, publicKey.encoded) + it.executeUpdate() + } + } } override fun setUp() { @@ -111,6 +110,7 @@ object PersistentIdentitiesMigrationSchemaBuilder { PersistentIdentityService.PersistentPublicKeyHashToCertificate::class.java, PersistentIdentityService.PersistentPartyToPublicKeyHash::class.java, PersistentIdentityService.PersistentPublicKeyHashToParty::class.java, + PersistentIdentityService.PersistentHashToPublicKey::class.java, BasicHSMKeyManagementService.PersistentKey::class.java, NodeAttachmentService.DBAttachment::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java diff --git a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt index b8215fb14b..b300bc019a 100644 --- a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt @@ -129,6 +129,7 @@ object VaultMigrationSchemaV1 : MappedSchema(schemaFamily = VaultMigrationSchema PersistentIdentityService.PersistentPublicKeyHashToCertificate::class.java, PersistentIdentityService.PersistentPartyToPublicKeyHash::class.java, PersistentIdentityService.PersistentPublicKeyHashToParty::class.java, + PersistentIdentityService.PersistentHashToPublicKey::class.java, BasicHSMKeyManagementService.PersistentKey::class.java, NodeAttachmentService.DBAttachment::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java diff --git a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt deleted file mode 100644 index 070e5b0ed1..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.corda.node.services.api - -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.CertRole -import net.corda.core.node.services.IdentityService -import net.corda.core.utilities.contextLogger -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.x509Certificates -import java.security.InvalidAlgorithmParameterException -import java.security.cert.CertPathValidatorException -import java.security.cert.CertificateExpiredException -import java.security.cert.CertificateNotYetValidException -import java.security.cert.TrustAnchor - -interface IdentityServiceInternal : IdentityService { - - private companion object { - val log = contextLogger() - } - - /** This method exists so it can be mocked with doNothing, rather than having to make up a possibly invalid return value. */ - fun justVerifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean = false) { - verifyAndRegisterIdentity(identity, isNewRandomIdentity) - } - - /** - * Verify and then store an identity. - * - * @param identity a party and the certificate path linking them to the network trust root. - * @param isNewRandomIdentity true if the identity will not have been registered before (e.g. because it is randomly generated by ourselves). - * @return the issuing entity, if known. - * @throws IllegalArgumentException if the certificate path is invalid. - */ - @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) - fun verifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean): PartyAndCertificate? - - // We can imagine this being a query over a lucene index in future. - // - // Kostas says: When exactMatch = false, we can easily use the Jaro-Winkler distance metric as it is best suited for short - // strings such as entity/company names, and to detect small typos. We can also apply it for city - // or any keyword related search in lists of records (not raw text - for raw text we need indexing) - // and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0). - /** Check if [x500name] matches the [query]. */ - fun x500Matches(query: String, exactMatch: Boolean, x500name: CordaX500Name): Boolean { - val components = listOfNotNull(x500name.commonName, x500name.organisationUnit, x500name.organisation, x500name.locality, x500name.state, x500name.country) - return components.any { (exactMatch && it == query) - || (!exactMatch && it.contains(query, ignoreCase = true)) } - } - - /** - * Verifies that an identity is valid. - * - * @param trustAnchor The trust anchor that will verify the identity's validity - * @param identity The identity to verify - * @param isNewRandomIdentity true if the identity will not have been registered before (e.g. because it is randomly generated by ourselves). - */ - @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) - fun verifyAndRegisterIdentity(trustAnchor: TrustAnchor, identity: PartyAndCertificate, isNewRandomIdentity: Boolean = false): PartyAndCertificate? { - // Validate the chain first, before we do anything clever with it - val identityCertChain = identity.certPath.x509Certificates - try { - identity.verify(trustAnchor) - } catch (e: CertPathValidatorException) { - log.warn("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.") - log.warn("Certificate path :") - identityCertChain.reversed().forEachIndexed { index, certificate -> - val space = (0 until index).joinToString("") { " " } - log.warn("$space${certificate.subjectX500Principal}") - } - throw e - } - // Ensure we record the first identity of the same name, first - val wellKnownCert = identityCertChain.single { CertRole.extract(it)?.isWellKnown ?: false } - if (wellKnownCert != identity.certificate && !isNewRandomIdentity) { - val idx = identityCertChain.lastIndexOf(wellKnownCert) - val firstPath = X509Utilities.buildCertPath(identityCertChain.slice(idx until identityCertChain.size)) - verifyAndRegisterIdentity(trustAnchor, PartyAndCertificate(firstPath)) - } - return registerIdentity(identity, isNewRandomIdentity) - } - - fun registerIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean = false): PartyAndCertificate? -} diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index a545deb2d0..080e4e8c98 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -1,21 +1,27 @@ package net.corda.node.services.identity +import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.x500Matches import net.corda.core.internal.CertRole +import net.corda.core.internal.hash import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.* +import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.ArrayList +import kotlin.collections.LinkedHashSet /** * Simple identity service which caches parties and provides functionality for efficient lookup. @@ -23,8 +29,10 @@ import javax.annotation.concurrent.ThreadSafe * @param identities initial set of identities for the service, typically only used for unit tests. */ @ThreadSafe -class InMemoryIdentityService(identities: List = emptyList(), - override val trustRoot: X509Certificate) : SingletonSerializeAsToken(), IdentityService { +class InMemoryIdentityService( + identities: List = emptyList(), + override val trustRoot: X509Certificate +) : SingletonSerializeAsToken(), IdentityService { companion object { private val log = contextLogger() } @@ -34,14 +42,16 @@ class InMemoryIdentityService(identities: List = emptyList( */ override val caCertStore: CertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(setOf(trustRoot))) override val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null) + private val keyToExternalId = ConcurrentHashMap() private val keyToPartyAndCerts = ConcurrentHashMap() private val nameToKey = ConcurrentHashMap() - private val keyToName = ConcurrentHashMap() + private val keyToName = ConcurrentHashMap() + private val hashToKey = ConcurrentHashMap() init { keyToPartyAndCerts.putAll(identities.associateBy { it.owningKey }) nameToKey.putAll(identities.associateBy { it.name }.mapValues { it.value.owningKey }) - keyToName.putAll(identities.associateBy{ it.owningKey }.mapValues { it.value.party.name }) + keyToName.putAll(identities.associateBy{ it.owningKey.toStringShort() }.mapValues { it.value.party.name }) } @@ -81,7 +91,7 @@ class InMemoryIdentityService(identities: List = emptyList( keyToPartyAndCerts[identity.owningKey] = identity // Always keep the first party we registered, as that's the well known identity nameToKey.computeIfAbsent(identity.name) {identity.owningKey} - keyToName.putIfAbsent(identity.owningKey, identity.name) + keyToName.putIfAbsent(identity.owningKey.toStringShort(), identity.name) return keyToPartyAndCerts[identityCertChain[1].publicKey] } @@ -108,10 +118,34 @@ class InMemoryIdentityService(identities: List = emptyList( return results } - override fun registerKeyToParty(key: PublicKey, party: Party) { - if (keyToName[key] == null) { - keyToName.putIfAbsent(key, party.name) + override fun registerKey(publicKey: PublicKey, party: Party, externalId: UUID?) { + val publicKeyHash = publicKey.toStringShort() + val existingEntry = keyToName[publicKeyHash] + if (existingEntry == null) { + registerKeyToParty(publicKey, party) + hashToKey[publicKeyHash] = publicKey + if (externalId != null) { + registerKeyToExternalId(publicKey, externalId) + } + } else { + if (party.name != existingEntry) { + } } - throw IllegalArgumentException("An entry for the public key: $key already exists.") + } + + override fun externalIdForPublicKey(publicKey: PublicKey): UUID? { + return keyToExternalId[publicKey.toStringShort()] + } + + fun registerKeyToExternalId(key: PublicKey, externalId: UUID) { + keyToExternalId[key.toStringShort()] = externalId + } + + fun registerKeyToParty(publicKey: PublicKey, party: Party) { + keyToName[publicKey.toStringShort()] = party.name + } + + override fun publicKeysForExternalId(externalId: UUID): Iterable { + throw NotImplementedError("This method is not implemented in the InMemoryIdentityService at it requires access to CordaPersistence.") } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index d6c06c7d95..4fab6894b7 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -1,5 +1,6 @@ package net.corda.node.services.identity +import net.corda.core.crypto.Crypto import net.corda.core.crypto.toStringShort import net.corda.core.identity.* import net.corda.core.internal.CertRole @@ -12,22 +13,29 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.node.services.persistence.PublicKeyHashToExternalId +import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.KeyOwningIdentity import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import org.apache.commons.lang3.ArrayUtils import org.hibernate.annotations.Type import org.hibernate.internal.util.collections.ArrayHelper.EMPTY_BYTE_ARRAY import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.* +import java.util.* import javax.annotation.concurrent.ThreadSafe import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id -import javax.persistence.Lob +import kotlin.IllegalStateException +import kotlin.collections.HashSet import kotlin.streams.toList /** @@ -43,6 +51,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri const val HASH_TO_IDENTITY_TABLE_NAME = "${NODE_DATABASE_PREFIX}identities" const val NAME_TO_HASH_TABLE_NAME = "${NODE_DATABASE_PREFIX}named_identities" const val KEY_TO_NAME_TABLE_NAME = "${NODE_DATABASE_PREFIX}identities_no_cert" + const val HASH_TO_KEY_TABLE_NAME = "${NODE_DATABASE_PREFIX}hash_to_key" const val PK_HASH_COLUMN_NAME = "pk_hash" const val IDENTITY_COLUMN_NAME = "identity_value" const val NAME_COLUMN_NAME = "name" @@ -80,7 +89,6 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri ) } - fun createKeyToX500Map(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( cacheFactory = cacheFactory, @@ -98,6 +106,23 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri persistentEntityClass = PersistentPublicKeyHashToParty::class.java) } + fun createHashToKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + cacheFactory = cacheFactory, + name = "PersistentIdentityService_hashToKey", + toPersistentEntityKey = { it }, + fromPersistentEntity = { + Pair( + it.publicKeyHash, + Crypto.decodePublicKey(it.publicKey) + ) + }, + toPersistentEntity = { key: String, value: PublicKey -> + PersistentHashToPublicKey(key, value.encoded) + }, + persistentEntityClass = PersistentHashToPublicKey::class.java) + } + private fun mapToKey(party: PartyAndCertificate) = party.owningKey.toStringShort() } @@ -135,6 +160,18 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri var name: String = "" ) + @Entity + @javax.persistence.Table(name = HASH_TO_KEY_TABLE_NAME) + class PersistentHashToPublicKey( + @Id + @Column(name = PK_HASH_COLUMN_NAME, length = MAX_HASH_HEX_SIZE, nullable = false) + var publicKeyHash: String = "", + + @Type(type = "corda-blob") + @Column(name = "public_key", nullable = false) + var publicKey: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY + ) + private lateinit var _caCertStore: CertStore override val caCertStore: CertStore get() = _caCertStore @@ -150,14 +187,23 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri // CordaPersistence is not a c'tor parameter to work around the cyclic dependency lateinit var database: CordaPersistence + private lateinit var _pkToIdCache: WritablePublicKeyToOwningIdentityCache + private val keyToPartyAndCert = createKeyToPartyAndCertMap(cacheFactory) private val nameToKey = createX500ToKeyMap(cacheFactory) private val keyToName = createKeyToX500Map(cacheFactory) + private val hashToKey = createHashToKeyMap(cacheFactory) - fun start(trustRoot: X509Certificate, caCertificates: List = emptyList(), notaryIdentities: List = emptyList()) { + fun start( + trustRoot: X509Certificate, + caCertificates: List = emptyList(), + notaryIdentities: List = emptyList(), + pkToIdCache: WritablePublicKeyToOwningIdentityCache + ) { _trustRoot = trustRoot _trustAnchor = TrustAnchor(trustRoot, null) _caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificates.toSet() + trustRoot)) + _pkToIdCache = pkToIdCache notaryIdentityCache.addAll(notaryIdentities) } @@ -216,9 +262,9 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri val identityCertChain = identity.certPath.x509Certificates val key = mapToKey(identity) return database.transaction { - keyToPartyAndCert.addWithDuplicatesAllowed(key, identity, false) - nameToKey.addWithDuplicatesAllowed(identity.name, key, false) - keyToName.addWithDuplicatesAllowed(key, identity.name, false) + keyToPartyAndCert.addWithDuplicatesAllowed(key, identity, false) + nameToKey.addWithDuplicatesAllowed(identity.name, key, false) + keyToName.addWithDuplicatesAllowed(key, identity.name, false) val parentId = identityCertChain[1].publicKey.toStringShort() keyToPartyAndCert[parentId] } @@ -260,9 +306,12 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri val legalIdentity = super.wellKnownPartyFromAnonymous(party) if (legalIdentity == null) { // If there is no entry in the legal keyToPartyAndCert table then the party must be a confidential identity so we perform - // a lookup in the keyToName table. If an entry for that public key exists, then we attempt + // a lookup in the keyToName table. If an entry for that public key exists, then we attempt look up the associated node's + // PartyAndCertificate. val name = keyToName[party.owningKey.toStringShort()] if (name != null) { + // This should never return null as this node would not be able to communicate with the node providing a confidential + // identity unless its NodeInfo/PartyAndCertificate were available. wellKnownPartyFromX500Name(name) } else { null @@ -293,18 +342,71 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri return keys.filter { (@Suppress("DEPRECATION") certificateFromKey(it))?.name in ourNames } } - override fun registerKeyToParty(key: PublicKey, party: Party) { + override fun registerKey(publicKey: PublicKey, party: Party, externalId: UUID?) { return database.transaction { - val existingEntryForKey = keyToName[key.toStringShort()] + val publicKeyHash = publicKey.toStringShort() + // EVERY key should be mapped to a Party in the "keyToName" table. Therefore if there is already a record in that table for the + // specified key then it's either our key which has been stored prior or another node's key which we have previously mapped. + val existingEntryForKey = keyToName[publicKeyHash] if (existingEntryForKey == null) { - log.info("Linking: ${key.hash} to ${party.name}") - keyToName[key.toStringShort()] = party.name + // Update the three tables as necessary. We definitely store the public key and map it to a party and we optionally update + // the public key to external ID mapping table. This block will only ever be reached when registering keys generated on + // other because when a node generates its own keys "registerKeyToParty" is automatically called by KeyManagementService.freshKey. + registerKeyToParty(publicKey, party) + hashToKey[publicKeyHash] = publicKey + if (externalId != null) { + registerKeyToExternalId(publicKey, externalId) + } } else { - log.info("An existing entry for ${key.hash} already exists.") - if (party.name != keyToName[key.toStringShort()]) { - throw IllegalArgumentException("The public key ${key.hash} is already assigned to a different party than the supplied .") + log.info("An existing entry for $publicKeyHash already exists.") + if (party.name != existingEntryForKey) { + throw IllegalStateException("The public publicKey $publicKeyHash is already assigned to a different party than the " + + "supplied party.") } } } } + + // Internal function used by the KMS to register a public key to a Corda Party. + fun registerKeyToParty(publicKey: PublicKey, party: Party) { + return database.transaction { + log.info("Linking: ${publicKey.hash} to ${party.name}") + keyToName[publicKey.toStringShort()] = party.name + } + } + + // Internal function used by the KMS to register a public key to an external ID. + fun registerKeyToExternalId(publicKey: PublicKey, externalId: UUID) { + _pkToIdCache[publicKey] = KeyOwningIdentity.fromUUID(externalId) + } + + override fun externalIdForPublicKey(publicKey: PublicKey): UUID? { + return _pkToIdCache[publicKey]?.uuid + } + + private fun publicKeysForExternalId(externalId: UUID, table: Class<*>): List { + return database.transaction { + val query = session.createQuery( + """ + select a.publicKey + from ${table.name} a, ${PublicKeyHashToExternalId::class.java.name} b + where b.externalId = :uuid + and b.publicKeyHash = a.publicKeyHash + """, + ByteArray::class.java + ) + query.setParameter("uuid", externalId) + query.resultList.map { Crypto.decodePublicKey(it) } + } + } + + override fun publicKeysForExternalId(externalId: UUID): Iterable { + // If the externalId was created by this node then we'll find the keys in the KMS, otherwise they'll be in the IdentityService. + val keys = publicKeysForExternalId(externalId, BasicHSMKeyManagementService.PersistentKey::class.java) + return if (keys.isEmpty()) { + publicKeysForExternalId(externalId, PersistentHashToPublicKey::class.java) + } else { + keys + } + } } \ No newline at end of file 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 ae856eeaa0..7f1f0bde01 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 @@ -30,11 +30,12 @@ import kotlin.collections.LinkedHashSet * * This class needs database transactions to be in-flight during method calls and init. */ -class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, - override val identityService: PersistentIdentityService, - private val database: CordaPersistence, - private val cryptoService: SignOnlyCryptoService, - private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { +class BasicHSMKeyManagementService( + cacheFactory: NamedCacheFactory, + override val identityService: PersistentIdentityService, + private val database: CordaPersistence, + private val cryptoService: SignOnlyCryptoService +) : SingletonSerializeAsToken(), KeyManagementServiceInternal { @Entity @Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") @@ -103,7 +104,14 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val keyPair = generateKeyPair() database.transaction { keysMap[keyPair.public] = keyPair.private - pkToIdCache[keyPair.public] = KeyOwningIdentity.fromUUID(externalId) + // Register the key to our identity. + val ourIdentity = identityService.wellKnownPartyFromX500Name(identityService.ourNames.first()) + ?: throw IllegalStateException("Could not lookup node Identity.") + // No checks performed here as entries for the new key couldn't have existed before in the maps. + identityService.registerKeyToParty(keyPair.public, ourIdentity) + if (externalId != null) { + identityService.registerKeyToExternalId(keyPair.public, externalId) + } } return keyPair.public } @@ -157,8 +165,4 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, keyPair.sign(signableData) } } - - override fun externalIdForPublicKey(publicKey: PublicKey): UUID? { - return pkToIdCache[publicKey]?.uuid - } } 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 d65845b6ce..a850d82d5c 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 @@ -88,8 +88,4 @@ class E2ETestKeyManagementService(override val identityService: IdentityService, val keyPair = getSigningKeyPair(publicKey) return keyPair.sign(signableData) } - - override fun externalIdForPublicKey(publicKey: PublicKey): UUID? { - throw UnsupportedOperationException("This operation is only supported by persistent key management service variants.") - } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index f88dc1c7de..d2aaa2b3c9 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -5,7 +5,6 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.CertRole import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days -import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.X509Utilities @@ -45,11 +44,7 @@ fun freshCertificate(identityService: IdentityService, window) val ourCertPath = X509Utilities.buildCertPath(ourCertificate, issuer.certPath.x509Certificates) val anonymisedIdentity = PartyAndCertificate(ourCertPath) - if (identityService is IdentityServiceInternal) { - identityService.justVerifyAndRegisterIdentity(anonymisedIdentity, true) - } else { - identityService.verifyAndRegisterIdentity(anonymisedIdentity) - } + identityService.verifyAndRegisterIdentity(anonymisedIdentity) return anonymisedIdentity } 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 675f294a07..34343e8680 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 @@ -43,6 +43,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentPublicKeyHashToCertificate::class.java, PersistentIdentityService.PersistentPartyToPublicKeyHash::class.java, PersistentIdentityService.PersistentPublicKeyHashToParty::class.java, + PersistentIdentityService.PersistentHashToPublicKey::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java, PublicKeyHashToExternalId::class.java 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 54f9c59caf..4a514e0172 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -48,6 +48,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "PersistentIdentityService_keyToPartyAndCert" -> caffeine.maximumSize(defaultCacheSize) name == "PersistentIdentityService_nameToKey" -> caffeine.maximumSize(defaultCacheSize) name == "PersistentIdentityService_keyToName" -> caffeine.maximumSize(defaultCacheSize) + name == "PersistentIdentityService_hashToKey" -> caffeine.maximumSize(defaultCacheSize) name == "PersistentNetworkMap_nodesByKey" -> caffeine.maximumSize(defaultCacheSize) name == "PersistentNetworkMap_idByLegalName" -> caffeine.maximumSize(defaultCacheSize) name == "PersistentKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index 58769fd865..d829ff3272 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -23,8 +23,11 @@ vault-schema.changelog-v9.xml), as that will use the current hibernate mappings, and those require all DB columns to be created. --> + + + diff --git a/node/src/main/resources/migration/node-core.changelog-v15-table.xml b/node/src/main/resources/migration/node-core.changelog-v15-table.xml new file mode 100644 index 0000000000..524301deb0 --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v15-table.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 3be105ef8d..c364bf96ee 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -7,6 +7,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.UnknownAnonymousPartyException +import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -45,12 +46,14 @@ class PersistentIdentityServiceTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + private val cacheFactory = TestingNamedCacheFactory() private lateinit var database: CordaPersistence private lateinit var identityService: PersistentIdentityService @Before fun setup() { - identityService = PersistentIdentityService(TestingNamedCacheFactory()) + val cacheFactory = TestingNamedCacheFactory() + identityService = PersistentIdentityService(cacheFactory = cacheFactory) database = configureDatabase( makeTestDataSourceProperties(), DatabaseConfig(), @@ -59,7 +62,7 @@ class PersistentIdentityServiceTests { ) identityService.database = database identityService.ourNames = setOf(ALICE_NAME) - identityService.start(DEV_ROOT_CA.certificate) + identityService.start(DEV_ROOT_CA.certificate, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) } @After @@ -224,7 +227,7 @@ class PersistentIdentityServiceTests { // Create new identity service mounted onto same DB val newPersistentIdentityService = PersistentIdentityService(TestingNamedCacheFactory()).also { it.database = database - it.start(DEV_ROOT_CA.certificate) + it.start(DEV_ROOT_CA.certificate, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) } newPersistentIdentityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) @@ -250,23 +253,22 @@ class PersistentIdentityServiceTests { fun `register duplicate confidential identities`(){ val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) - identityService.registerKeyToParty(anonymousAlice.owningKey, alice.party) + identityService.registerKey(anonymousAlice.owningKey, alice.party) // If an existing entry is found matching the party then the method call is idempotent assertDoesNotThrow { - identityService.registerKeyToParty(anonymousAlice.owningKey, alice.party) + identityService.registerKey(anonymousAlice.owningKey, alice.party) } } @Test fun `register incorrect party to public key `(){ + database.transaction { identityService.verifyAndRegisterIdentity(ALICE_IDENTITY) } val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) - - identityService.registerKeyToParty(anonymousAlice.owningKey, alice.party) - - assertThrows { - identityService.registerKeyToParty(anonymousAlice.owningKey, bob.party) - } + identityService.registerKey(anonymousAlice.owningKey, alice.party) + // Should have no side effect but logs a warning that we tried to overwrite an existing mapping. + assertFailsWith { identityService.registerKey(anonymousAlice.owningKey, bob.party) } + assertEquals(ALICE, identityService.wellKnownPartyFromAnonymous(AnonymousParty(anonymousAlice.owningKey))) } @Test 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 40dfa91c40..3842c044bf 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 @@ -103,7 +103,7 @@ class NetworkMapUpdaterTest { server.networkParameters.serialize().hash, ourNodeInfo, networkParameters, - MockKeyManagementService(makeTestIdentityService(), ourKeyPair, pkToIdCache = MockPublicKeyToOwningIdentityCache()), + MockKeyManagementService(makeTestIdentityService(), ourKeyPair), 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 8ac3ad07d9..dd387c5a5c 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 @@ -50,7 +50,7 @@ class NodeInfoWatcherTest { fun start() { nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) val identityService = makeTestIdentityService() - keyManagementService = MockKeyManagementService(identityService, pkToIdCache = MockPublicKeyToOwningIdentityCache()) + keyManagementService = MockKeyManagementService(identityService) 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/HibernateColumnConverterTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt index ac4d289f31..175fab84e3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt @@ -57,10 +57,11 @@ class HibernateColumnConverterTests { val ref = OpaqueBytes.of(0x01) // Create parallel set of key and identity services so that the values are not cached, forcing the node caches to do a lookup. - val identityService = PersistentIdentityService(TestingNamedCacheFactory()) + val cacheFactory = TestingNamedCacheFactory() + val identityService = PersistentIdentityService(cacheFactory) val originalIdentityService: PersistentIdentityService = services.identityService as PersistentIdentityService identityService.database = originalIdentityService.database - identityService.start(originalIdentityService.trustRoot) + identityService.start(originalIdentityService.trustRoot, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) val keyService = E2ETestKeyManagementService(identityService) keyService.start(setOf(myself.keyPair)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt index bd1e8271fc..f5fa968e3c 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt @@ -1,11 +1,11 @@ package net.corda.node.services.vault +import net.corda.core.crypto.Crypto import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.builder import net.corda.core.transactions.TransactionBuilder import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.common.internal.testNetworkParameters @@ -19,6 +19,8 @@ import org.junit.Rule import org.junit.Test import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith class ExternalIdMappingTest { @@ -34,6 +36,9 @@ class ExternalIdMappingTest { private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) private val notary = TestIdentity(CordaX500Name("NotaryService", "London", "GB"), 1337L) + private val alice = TestIdentity.fresh("ALICE") + private val bob = TestIdentity.fresh("BOB") + lateinit var services: MockServices lateinit var database: CordaPersistence @@ -43,7 +48,7 @@ class ExternalIdMappingTest { cordappPackages = cordapps, initialIdentity = myself, networkParameters = testNetworkParameters(minimumPlatformVersion = 4), - moreIdentities = setOf(notary.identity), + moreIdentities = setOf(notary.identity, alice.identity, bob.identity), moreKeys = emptySet() ) services = mockServices @@ -72,17 +77,13 @@ class ExternalIdMappingTest { val dummyStateTwo = createDummyState(listOf(AnonymousParty(keyTwo.owningKey))) // This query should return two states! val result = database.transaction { - val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(id)) } - val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) - vaultService.queryBy(queryCriteria).states + vaultService.queryBy(QueryCriteria.VaultQueryCriteria(externalIds = listOf(id))).states } assertEquals(setOf(dummyStateOne, dummyStateTwo), result.map { it.state.data }.toSet()) // This query should return two states! val resultTwo = database.transaction { - val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.equal(id) } - val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) - vaultService.queryBy(queryCriteria).states + vaultService.queryBy(QueryCriteria.VaultQueryCriteria(externalIds = listOf(id))).states } assertEquals(setOf(dummyStateOne, dummyStateTwo), resultTwo.map { it.state.data }.toSet()) } @@ -140,11 +141,72 @@ class ExternalIdMappingTest { val dummyState = createDummyState(listOf(AnonymousParty(keyOne.owningKey), AnonymousParty(keyTwo.owningKey))) // This query should return one state! val result = database.transaction { - val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(idOne, idTwo)) } - val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) - vaultService.queryBy(queryCriteria).states + vaultService.queryBy(QueryCriteria.VaultQueryCriteria(externalIds = listOf(idOne, idTwo))).states } assertEquals(dummyState, result.single().state.data) } + @Test + fun `roger uber keys test`() { + // IDs. + val id = UUID.randomUUID() + val idTwo = UUID.randomUUID() + val idThree = UUID.randomUUID() + val idFour = UUID.randomUUID() + + // Keys. + val A = services.keyManagementService.freshKey(id) // Automatically calls registerKeyToParty and registerKeyToExternalId + val B = services.keyManagementService.freshKey(id) // Automatically calls registerKeyToParty and registerKeyToExternalId + val C = services.keyManagementService.freshKey(idTwo) // Automatically calls registerKeyToParty and registerKeyToExternalId + val D = services.keyManagementService.freshKey() // Automatically calls registerKeyToParty and registerKeyToExternalId + val E = Crypto.generateKeyPair().public + val F = Crypto.generateKeyPair().public + val G = Crypto.generateKeyPair().public + + // Check we can lookup the Party and external ID (if there is one). + assertEquals(id, services.identityService.externalIdForPublicKey(A)) + assertEquals(id, services.identityService.externalIdForPublicKey(B)) + assertEquals(idTwo, services.identityService.externalIdForPublicKey(C)) + assertEquals(null, services.identityService.externalIdForPublicKey(D)) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(A))) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(B))) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(C))) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(D))) + + // Register some keys generated on another node. + services.identityService.registerKey(E, alice.party, idThree) + services.identityService.registerKey(F, alice.party, idFour) + services.identityService.registerKey(G, bob.party) + + // Try to override existing mappings. + services.identityService.registerKey(A, myself.party) // Idempotent call. + assertFailsWith { services.identityService.registerKey(A, alice.party) } + services.identityService.registerKey(B, myself.party, UUID.randomUUID()) // Idempotent call. + assertFailsWith { services.identityService.registerKey(B, alice.party) } + assertFailsWith { services.identityService.registerKey(C, bob.party, UUID.randomUUID()) } + + // Check the above calls didn't change anything. + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(A))) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(B))) + assertEquals(myself.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(C))) + + assertEquals(id, services.identityService.externalIdForPublicKey(A)) + assertEquals(id, services.identityService.externalIdForPublicKey(B)) + assertEquals(idTwo, services.identityService.externalIdForPublicKey(C)) + assertEquals(idThree, services.identityService.externalIdForPublicKey(E)) + assertEquals(idFour, services.identityService.externalIdForPublicKey(F)) + + // ALICE and BOB PartyAndCertificates need to be present in the nameToCert table. + assertEquals(alice.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(E))) + assertEquals(alice.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(F))) + assertEquals(bob.party, services.identityService.wellKnownPartyFromAnonymous(AnonymousParty(G))) + + // Check we can look up keys by external ID. + assertEquals(setOf(A, B), services.identityService.publicKeysForExternalId(id).toSet()) + assertEquals(C, services.identityService.publicKeysForExternalId(idTwo).single()) + assertEquals(E, services.identityService.publicKeysForExternalId(idThree).single()) + assertEquals(F, services.identityService.publicKeysForExternalId(idFour).single()) + assertEquals(emptyList(), services.identityService.publicKeysForExternalId(UUID.randomUUID())) + } + } \ 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 044c851fc4..27cb01c617 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 @@ -79,8 +79,7 @@ open class MockServices private constructor( private val moreKeys: Array, override val keyManagementService: KeyManagementService = MockKeyManagementService( identityService, - *arrayOf(initialIdentity.keyPair) + moreKeys, - pkToIdCache = MockPublicKeyToOwningIdentityCache() + *arrayOf(initialIdentity.keyPair) + moreKeys ) ) : ServiceHub { @@ -128,8 +127,7 @@ open class MockServices private constructor( val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas()) val keyManagementService = MockKeyManagementService( identityService, - *arrayOf(initialIdentity.keyPair) + moreKeys, - pkToIdCache = MockPublicKeyToOwningIdentityCache() + *arrayOf(initialIdentity.keyPair) + moreKeys ) val mockService = database.transaction { makeMockMockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys.toSet(), keyManagementService, schemaService, database) @@ -160,20 +158,28 @@ open class MockServices private constructor( val dataSourceProps = makeTestDataSourceProperties() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val identityService = PersistentIdentityService(TestingNamedCacheFactory()) - val persistence = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas()) + val persistence = configureDatabase( + hikariProperties = dataSourceProps, + databaseConfig = DatabaseConfig(), + wellKnownPartyFromX500Name = identityService::wellKnownPartyFromX500Name, + wellKnownPartyFromAnonymous = identityService::wellKnownPartyFromAnonymous, + schemaService = schemaService, + internalSchemas = schemaService.internalSchemas() + ) + + val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(persistence, TestingNamedCacheFactory()) // Create a persistent identity service and add all the supplied identities. identityService.apply { ourNames = setOf(initialIdentity.name) database = persistence - start(DEV_ROOT_CA.certificate) + start(DEV_ROOT_CA.certificate, pkToIdCache = pkToIdCache) persistence.transaction { identityService.loadIdentities(moreIdentities + initialIdentity.identity) } } // 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 pkToIdCache = PublicKeyToOwningIdentityCacheImpl(persistence, TestingNamedCacheFactory()) val aliasKeyMap = mutableMapOf() val aliasedMoreKeys = moreKeys.mapIndexed { index, keyPair -> val alias = "Extra key $index" @@ -187,8 +193,7 @@ open class MockServices private constructor( TestingNamedCacheFactory(), identityService, persistence, - MockCryptoService(aliasKeyMap), - pkToIdCache + MockCryptoService(aliasKeyMap) ) persistence.transaction { keyManagementService.start(aliasedMoreKeys + aliasedIdentityKey) } 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 752ab7fdd6..30d9e24902 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 @@ -373,7 +373,7 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), } override fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal { - return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService, pkToIdCache) + return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService) } 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 629d1135ee..17aa014ca7 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 @@ -4,6 +4,8 @@ import net.corda.core.crypto.* 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.identity.InMemoryIdentityService +import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.KeyManagementServiceInternal import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.nodeapi.internal.KeyOwningIdentity @@ -18,10 +20,10 @@ import java.util.* * * @property identityService The [IdentityService] which contains the given identities. */ -class MockKeyManagementService(override val identityService: IdentityService, - vararg initialKeys: KeyPair, - private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { - +class MockKeyManagementService( + override val identityService: IdentityService, + vararg initialKeys: KeyPair +) : SingletonSerializeAsToken(), KeyManagementServiceInternal { private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) @@ -32,7 +34,9 @@ class MockKeyManagementService(override val identityService: IdentityService, override fun freshKeyInternal(externalId: UUID?): PublicKey { val k = nextKeys.poll() ?: generateKeyPair() keyStore[k.public] = k.private - pkToIdCache[k.public] = KeyOwningIdentity.fromUUID(externalId) + if (externalId != null) { + (identityService as InMemoryIdentityService).registerKeyToExternalId(k.public, externalId) + } return k.public } @@ -59,8 +63,4 @@ class MockKeyManagementService(override val identityService: IdentityService, val keyPair = getSigningKeyPair(publicKey) return keyPair.sign(signableData) } - - override fun externalIdForPublicKey(publicKey: PublicKey): UUID? { - return pkToIdCache[publicKey]?.uuid - } } \ No newline at end of file