From 060bbb0a9d7a4a35b61de91009330d04286b4ad9 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Thu, 14 Feb 2019 15:18:37 +0000 Subject: [PATCH] CORDA-2563: Assign external IDs when creating new keys (#4727) * First pass at fixing 2563. * In memory KMS now maps keys to IDs. * CreateDatabaseAndMockServices now creates a persistent key management service and a can take a persistent identity service, so now the external id mapping works for mock services. * * Created a helper for mock services which allows the creation of a mock services with persistent identity management service key management service and vault. * MockNode now uses persistent key management service - not sure why it didn't do before? * * MockNode now uses BasicHSMKeyManagementService * Updated api-current file * Little fix required after rebase to master. * Fixed broken test. * Added informative error messages to UnsupportedOperationExceptions thrown by E2ETestKeyManagementService. * Removed redundant private constructor for mock services from api-current.txt. * Addressed Rick's comments. --- .ci/api-current.txt | 2 +- .../node/services/KeyManagementService.kt | 27 +++- .../keys/BasicHSMKeyManagementService.kt | 33 ++--- .../keys/E2ETestKeyManagementService.kt | 8 ++ .../keys/KeyManagementServiceInternal.kt | 24 ++++ .../keys/PersistentKeyManagementService.kt | 13 ++ .../node/services/schema/NodeSchemaService.kt | 5 +- .../HibernateColumnConverterTests.kt | 85 +++++++----- .../services/vault/ExternalIdMappingTest.kt | 52 ++----- .../net/corda/testing/node/MockServices.kt | 130 ++++++++++++++---- .../node/internal/InternalMockNetwork.kt | 4 +- .../node/internal/MockKeyManagementService.kt | 20 +++ 12 files changed, 273 insertions(+), 130 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 154bd993fe..029ab9bb22 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -7745,7 +7745,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService, java.security.KeyPair, java.security.KeyPair...) - public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService, java.security.KeyPair, java.security.KeyPair[], int, kotlin.jvm.internal.DefaultConstructorMarker) + public (net.corda.node.cordapp.CordappLoader, net.corda.core.node.services.IdentityService, net.corda.core.node.NetworkParameters, net.corda.testing.core.TestIdentity, java.security.KeyPair[], net.corda.core.node.services.KeyManagementService, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.testing.core.TestIdentity, net.corda.core.node.NetworkParameters, net.corda.testing.core.TestIdentity...) public (net.corda.testing.core.TestIdentity, net.corda.testing.core.TestIdentity...) public final void addMockCordapp(String) 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 14ba9b21c0..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 @@ -7,10 +7,12 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignableData import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.ServiceHub import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.security.cert.X509Certificate +import java.util.* /** * The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example, @@ -31,8 +33,15 @@ interface KeyManagementService { fun freshKey(): PublicKey /** - * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding - * [X509Certificate] and adds it to the identity service. + * Generates a new random [KeyPair] and adds it to the internal key storage. Associates the public key to an external ID. Returns the + * public key part of the pair. + */ + @Suspendable + fun freshKey(externalId: UUID): PublicKey + + /** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding [X509Certificate] and adds it + * to the identity service. Associates the public key to an external ID. Returns the public part of the pair. * * @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for. * @param revocationEnabled whether to check revocation status of certificates in the certificate path. @@ -41,6 +50,18 @@ interface KeyManagementService { @Suspendable fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate + /** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding + * [X509Certificate] and adds it to the identity service. + * + * @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for. + * @param revocationEnabled whether to check revocation status of certificates in the certificate path. + * @param externalId ID to associate the newly created [PublicKey] with. + * @return X.509 certificate and path to the trust root. + */ + @Suspendable + fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean, externalId: UUID): PartyAndCertificate + /** * Filter some keys down to the set that this node owns (has private keys for). * @@ -69,4 +90,4 @@ interface KeyManagementService { */ @Suspendable fun sign(signableData: SignableData, publicKey: PublicKey): TransactionSignature -} +} \ 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 449fcfcd58..97621bdf5a 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 @@ -49,26 +49,7 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identity : this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded) } - @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()) - } - - companion object { + private companion object { fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( cacheFactory = cacheFactory, @@ -116,10 +97,22 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identity 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 { val signingPublicKey = getSigningPublicKey(publicKey) return if (signingPublicKey in originalKeysMap) { 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 c33d28e642..49eb995eac 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 @@ -61,10 +61,18 @@ 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)) private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { 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 87eca7bb67..01d4e46c28 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,8 +1,32 @@ package net.corda.node.services.keys +import net.corda.core.crypto.toStringShort import net.corda.core.node.services.KeyManagementService +import org.hibernate.annotations.Type import java.security.KeyPair +import java.security.PublicKey +import java.util.* +import javax.persistence.* interface KeyManagementServiceInternal : KeyManagementService { fun start(initialKeyPairs: Set) } + +@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 74a022fadb..c35582f6a8 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 @@ -14,6 +14,7 @@ import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.util.* import javax.persistence.* /** @@ -82,10 +83,22 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi 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)) //It looks for the PublicKey in the (potentially) CompositeKey that is ours, and then returns the associated PrivateKey to use in signing 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 460e4b2e24..371541ebbc 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,6 +15,7 @@ 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 @@ -45,7 +46,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java, - BasicHSMKeyManagementService.PublicKeyHashToExternalId::class.java + PublicKeyHashToExternalId::class.java )) { override val migrationResource = "node-core.changelog-master" } @@ -97,3 +98,5 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() schemaOptions.keys.map { schema -> crossReferencesToOtherMappedSchema(schema) }.flatMap { it.toList() } } + + 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 b4e3c1bcd2..2374bc4f27 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 @@ -2,7 +2,10 @@ package net.corda.node.services.persistence import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.services.KeyManagementService +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -11,52 +14,56 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.AbstractCashFlow import net.corda.finance.issuedBy +import net.corda.node.migration.VaultStateMigrationTest.Companion.bankOfCorda import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.E2ETestKeyManagementService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockServices import net.corda.testing.node.StartedMockNode import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.TestStartedNode import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import java.util.* import kotlin.test.assertEquals class HibernateColumnConverterTests { - private lateinit var mockNet: MockNetwork - private lateinit var bankOfCordaNode: StartedMockNode - private lateinit var bankOfCorda: Party - private lateinit var notary: Party - class TestCashIssueFlow(private val amount: Amount, - private val issuerBankPartyRef: OpaqueBytes, - private val notary: Party) : AbstractCashFlow(tracker()) { - @Suspendable - override fun call(): AbstractCashFlow.Result { - val builder = TransactionBuilder(notary) - val issuer = ourIdentity.ref(issuerBankPartyRef) - val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary) - val tx = serviceHub.signInitialTransaction(builder, signers) - serviceHub.recordTransactions(tx) - return Result(tx, ourIdentity) - } - } - + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val cordapps = listOf("net.corda.finance") + + private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) + private val notary = TestIdentity(CordaX500Name("NotaryService", "London", "GB"), 1337L) + + lateinit var services: MockServices + lateinit var database: CordaPersistence + @Before - fun start() { - mockNet = MockNetwork(MockNetworkParameters(servicePeerAllocationStrategy = RoundRobin(), cordappsForAllNodes = FINANCE_CORDAPPS)) - bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) - bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) - notary = mockNet.defaultNotaryIdentity - } - - @After - fun cleanUp() { - mockNet.stopNodes() + fun setUp() { + val (db, mockServices) = MockServices.makeTestDatabaseAndPersistentServices( + cordappPackages = cordapps, + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4), + moreIdentities = setOf(notary.identity), + moreKeys = emptySet() + ) + services = mockServices + database = db } // AbstractPartyToX500NameAsStringConverter could cause circular flush of Hibernate session because it is invoked during flush, and a @@ -68,20 +75,24 @@ class HibernateColumnConverterTests { // 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 originalIdentityService: PersistentIdentityService = bankOfCordaNode.services.identityService as PersistentIdentityService + val originalIdentityService: PersistentIdentityService = services.identityService as PersistentIdentityService identityService.database = originalIdentityService.database identityService.start(originalIdentityService.trustRoot) val keyService = E2ETestKeyManagementService(identityService) - keyService.start((bankOfCordaNode.services.keyManagementService as E2ETestKeyManagementService).keyPairs) + keyService.start(setOf(myself.keyPair)) // New identity for a notary (doesn't matter that it's for Bank Of Corda... since not going to use it as an actual notary etc). - val newKeyAndCert = keyService.freshKeyAndCert(bankOfCordaNode.info.legalIdentitiesAndCerts[0], false) - val randomNotary = Party(BOC_NAME, newKeyAndCert.owningKey) + val newKeyAndCert = keyService.freshKeyAndCert(services.myInfo.legalIdentitiesAndCerts[0], false) + val randomNotary = Party(myself.name, newKeyAndCert.owningKey) - val future = bankOfCordaNode.startFlow(TestCashIssueFlow(expected, ref, randomNotary)) - mockNet.runNetwork() - val issueTx = future.getOrThrow().stx - val output = issueTx.tx.outputsOfType().single() - assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) + val ourIdentity = services.myInfo.legalIdentities.first() + val builder = TransactionBuilder(notary.party) + val issuer = services.myInfo.legalIdentities.first().ref(ref) + val signers = Cash().generateIssue(builder, expected.issuedBy(issuer), ourIdentity, randomNotary) + val tx: SignedTransaction = services.signInitialTransaction(builder, signers) + services.recordTransactions(tx) + + val output = tx.tx.outputsOfType().single() + assertEquals(expected.`issued by`(ourIdentity.ref(ref)), output.amount) } } \ No newline at end of file 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 ec74b820b9..f8fc950025 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,8 +1,5 @@ package net.corda.node.services.vault -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -10,15 +7,12 @@ 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.node.services.api.IdentityServiceInternal -import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Rule @@ -45,41 +39,17 @@ class ExternalIdMappingTest { @Before fun setUp() { - val (db, mockServices) = MockServices.makeTestDatabaseAndMockServices( + val (db, mockServices) = MockServices.makeTestDatabaseAndPersistentServices( cordappPackages = cordapps, - identityService = mock().also { - doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey) - doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party) - doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name) - }, initialIdentity = myself, - networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + networkParameters = testNetworkParameters(minimumPlatformVersion = 4), + moreIdentities = setOf(notary.identity), + moreKeys = emptySet() ) services = mockServices database = db } - private fun freshKeyForExternalId(externalId: UUID): AnonymousParty { - val anonymousParty = freshKey() - database.transaction { - services.withEntityManager { - val mapping = BasicHSMKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey) - persist(mapping) - flush() - } - } - return anonymousParty - } - - private fun freshKey(): AnonymousParty { - val key = services.keyManagementService.freshKey() - val anonymousParty = AnonymousParty(key) - // Add behaviour to the mock identity management service for dealing with the new key. - // It won't be able to resolve it as it's just an anonymous key that is not linked to an identity. - services.identityService.also { doReturn(null).whenever(it).wellKnownPartyFromAnonymous(anonymousParty) } - return anonymousParty - } - private fun createDummyState(participants: List): DummyState { val tx = TransactionBuilder(notary = notary.party).apply { addOutputState(DummyState(1, participants), DummyContract.PROGRAM_ID) @@ -95,11 +65,11 @@ class ExternalIdMappingTest { val vaultService = services.vaultService // Create new external ID and two keys mapped to it. val id = UUID.randomUUID() - val keyOne = freshKeyForExternalId(id) - val keyTwo = freshKeyForExternalId(id) + val keyOne = services.keyManagementService.freshKeyAndCert(myself.identity, false, id) + val keyTwo = services.keyManagementService.freshKeyAndCert(myself.identity, false, id) // Create states with a public key assigned to the new external ID. - val dummyStateOne = createDummyState(listOf(keyOne)) - val dummyStateTwo = createDummyState(listOf(keyTwo)) + val dummyStateOne = createDummyState(listOf(AnonymousParty(keyOne.owningKey))) + 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)) } @@ -122,11 +92,11 @@ class ExternalIdMappingTest { val vaultService = services.vaultService // Create new external ID. val idOne = UUID.randomUUID() - val keyOne = freshKeyForExternalId(idOne) + val keyOne = services.keyManagementService.freshKeyAndCert(myself.identity, false, idOne) val idTwo = UUID.randomUUID() - val keyTwo = freshKeyForExternalId(idTwo) + val keyTwo = services.keyManagementService.freshKeyAndCert(myself.identity, false, idTwo) // Create state with a public key assigned to the new external ID. - val dummyState = createDummyState(listOf(keyOne, keyTwo)) + 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)) } 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 57aeeabcc8..89be93f1d4 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 @@ -26,6 +26,8 @@ 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.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService @@ -36,6 +38,7 @@ import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.MockCordappProvider +import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase import net.corda.testing.node.internal.* import net.corda.testing.services.MockAttachmentStorage @@ -71,7 +74,8 @@ open class MockServices private constructor( override val identityService: IdentityService, private val initialNetworkParameters: NetworkParameters, private val initialIdentity: TestIdentity, - private val moreKeys: Array + private val moreKeys: Array, + override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *arrayOf(initialIdentity.keyPair) + moreKeys) ) : ServiceHub { companion object { @@ -116,32 +120,91 @@ 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 mockService = database.transaction { - object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys) { - override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(networkParameters) - override val vaultService: VaultService = makeVaultService(schemaService, database, cordappLoader) - override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { - ServiceHubInternal.recordTransactions(statesToRecord, txs, - validatedTransactions as WritableTransactionStorage, - mockStateMachineRecordedTransactionMappingStorage, - vaultService as VaultServiceInternal, - database) - } - - override fun jdbcSession(): Connection = database.createSession() - - override fun withEntityManager(block: EntityManager.() -> T): T { - return block(contextTransaction.restrictedEntityManager) - } - - override fun withEntityManager(block: Consumer) { - return block.accept(contextTransaction.restrictedEntityManager) - } - } + makeMockMockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys.toSet(), keyManagementService, schemaService, database) } return Pair(database, mockService) } + /** + * Makes database and persistent services appropriate for unit tests which require persistence across the vault, identity service + * and key managment service. + * + * @param cordappPackages A [List] of cordapp packages to scan for any cordapp code, e.g. contract verification code, flows and services. + * @param initialIdentity The first (typically sole) identity the services will represent. + * @param moreKeys A list of additional [KeyPair] instances to be used by [MockServices]. + * @param moreIdentities A list of additional [KeyPair] instances to be used by [MockServices]. + * @return A pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. + */ + @JvmStatic + @JvmOverloads + fun makeTestDatabaseAndPersistentServices( + cordappPackages: List, + initialIdentity: TestIdentity, + networkParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN), + moreKeys: Set, + moreIdentities: Set + ): Pair { + val cordappLoader = cordappLoaderForPackages(cordappPackages) + val dataSourceProps = makeTestDataSourceProperties() + val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) + val identityService = PersistentIdentityService(TestingNamedCacheFactory()) + val persistence = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas()) + + // Create a persistent identity service and add all the supplied identities. + identityService.apply { + ourNames = setOf(initialIdentity.name) + database = persistence + start(DEV_ROOT_CA.certificate) + 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 keyManagementService = PersistentKeyManagementService(TestingNamedCacheFactory(), identityService, persistence) + persistence.transaction { keyManagementService.start(moreKeys + initialIdentity.keyPair) } + + val mockService = persistence.transaction { + makeMockMockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys, keyManagementService, schemaService, persistence) + } + return Pair(persistence, mockService) + } + + private fun makeMockMockServices( + cordappLoader: CordappLoader, + identityService: IdentityService, + networkParameters: NetworkParameters, + initialIdentity: TestIdentity, + moreKeys: Set, + keyManagementService: KeyManagementService, + schemaService: SchemaService, + persistence: CordaPersistence + ): MockServices { + return object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys.toTypedArray(), keyManagementService) { + override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(networkParameters) + override val vaultService: VaultService = makeVaultService(schemaService, persistence, cordappLoader) + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { + ServiceHubInternal.recordTransactions(statesToRecord, txs, + validatedTransactions as WritableTransactionStorage, + mockStateMachineRecordedTransactionMappingStorage, + vaultService as VaultServiceInternal, + persistence) + } + + override fun jdbcSession(): Connection = persistence.createSession() + + override fun withEntityManager(block: EntityManager.() -> T): T { + return block(contextTransaction.restrictedEntityManager) + } + + override fun withEntityManager(block: Consumer) { + return block.accept(contextTransaction.restrictedEntityManager) + } + } + } + // Because Kotlin is dumb and makes not publicly visible objects public, thus changing the public API. private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() @@ -175,8 +238,18 @@ open class MockServices private constructor( } private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters, - initialIdentity: TestIdentity, moreKeys: Array) - : this(cordappLoader, MockTransactionStorage(), identityService, networkParameters, initialIdentity, moreKeys) + initialIdentity: TestIdentity, moreKeys: Array, keyManagementService: KeyManagementService) + : this(cordappLoader, MockTransactionStorage(), identityService, networkParameters, initialIdentity, moreKeys, keyManagementService) + + private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters, + initialIdentity: TestIdentity, moreKeys: Array) : this( + cordappLoader, + MockTransactionStorage(), + identityService, + networkParameters, + initialIdentity, + moreKeys + ) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service @@ -196,6 +269,14 @@ open class MockServices private constructor( vararg moreKeys: KeyPair) : this(cordappLoaderForPackages(cordappPackages), identityService, networkParameters, initialIdentity, moreKeys) + constructor(cordappPackages: Iterable, + initialIdentity: TestIdentity, + identityService: IdentityService, + networkParameters: NetworkParameters, + vararg moreKeys: KeyPair, + keyManagementService: KeyManagementService) : + this(cordappLoaderForPackages(cordappPackages), identityService, networkParameters, initialIdentity, moreKeys, keyManagementService) + /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. @@ -298,7 +379,6 @@ open class MockServices private constructor( get() = networkParametersService.run { lookup(currentHash)!! } final override val attachments = MockAttachmentStorage() - override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *arrayOf(initialIdentity.keyPair) + moreKeys) } override val vaultService: VaultService get() = throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() 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 fff4e46a12..391a15c6d4 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 @@ -34,7 +34,7 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.* import net.corda.node.services.identity.PersistentIdentityService -import net.corda.node.services.keys.E2ETestKeyManagementService +import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal import net.corda.node.services.keys.cryptoservice.BCCryptoService import net.corda.node.services.messaging.Message @@ -373,7 +373,7 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), } override fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal { - return E2ETestKeyManagementService(identityService, cryptoService) + 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 53664ea9f1..de84cd4ce3 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 @@ -11,6 +11,7 @@ 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] @@ -25,12 +26,31 @@ class MockKeyManagementService(val identityService: IdentityService, private val nextKeys = LinkedList() + val keysById: MutableMap> = ConcurrentHashMap() + override fun freshKey(): PublicKey { val k = nextKeys.poll() ?: generateKeyPair() keyStore[k.public] = k.private 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 {