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 deleted file mode 100644 index 8742a0c068..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ /dev/null @@ -1,112 +0,0 @@ -package net.corda.node.services.keys - -import net.corda.core.crypto.* -import net.corda.core.internal.NamedCacheFactory -import net.corda.core.internal.toSet -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.MAX_HASH_HEX_SIZE -import net.corda.node.services.identity.PersistentIdentityService -import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache -import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.nodeapi.internal.KeyOwningIdentity -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX -import org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY -import org.bouncycastle.operator.ContentSigner -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey -import java.util.* -import javax.persistence.* - -/** - * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. - * - * This is not the long-term implementation. See the list of items in the above class. - * - * This class needs database transactions to be in-flight during method calls and init. - */ -@Deprecated("Superseded by net.corda.node.services.keys.BasicHSMKeyManagementService") -class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, - override val identityService: PersistentIdentityService, - private val database: CordaPersistence, - private val pkToIdCache: WritablePublicKeyToOwningIdentityCache) : SingletonSerializeAsToken(), KeyManagementServiceInternal { - @Entity - @Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") - class PersistentKey( - @Id - @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false) - var publicKeyHash: String, - - @Lob - @Column(name = "public_key", nullable = false) - var publicKey: ByteArray = EMPTY_BYTE_ARRAY, - @Lob - @Column(name = "private_key", nullable = false) - var privateKey: ByteArray = EMPTY_BYTE_ARRAY - ) { - constructor(publicKey: PublicKey, privateKey: PrivateKey) - : this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded) - } - - private companion object { - fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { - return AppendOnlyPersistentMap( - cacheFactory = cacheFactory, - name = "PersistentKeyManagementService_keys", - toPersistentEntityKey = { it.toStringShort() }, - fromPersistentEntity = { - Pair(Crypto.decodePublicKey(it.publicKey), - Crypto.decodePrivateKey(it.privateKey)) - }, - toPersistentEntity = { key: PublicKey, value: PrivateKey -> - PersistentKey(key, value) - }, - persistentEntityClass = PersistentKey::class.java - ) - } - } - - private val keysMap = createKeyMap(cacheFactory) - - override fun start(initialKeyPairs: Set) { - initialKeyPairs.forEach { keysMap.addWithDuplicatesAllowed(it.public, it.private) } - } - - override val keys: Set get() = database.transaction { keysMap.allPersisted.use { it.map { it.first }.toSet() } } - - override fun filterMyKeys(candidateKeys: Iterable): Iterable = database.transaction { - identityService.stripNotOurKeys(candidateKeys) - } - - override fun freshKeyInternal(externalId: UUID?): PublicKey { - val keyPair = generateKeyPair() - database.transaction { - keysMap[keyPair.public] = keyPair.private - pkToIdCache[keyPair.public] = KeyOwningIdentity.fromUUID(externalId) - } - return keyPair.public - } - - override fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) - - //It looks for the PublicKey in the (potentially) CompositeKey that is ours, and then returns the associated PrivateKey to use in signing - private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { - return database.transaction { - val pk = publicKey.keys.first { keysMap[it] != null } //TODO here for us to re-write this using an actual query if publicKey.keys.size > 1 - KeyPair(pk, keysMap[pk]!!) - } - } - - override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { - val keyPair = getSigningKeyPair(publicKey) - return keyPair.sign(bytes) - } - - // TODO: A full KeyManagementService implementation needs to record activity to the Audit Service and to limit - // signing to appropriately authorised contexts and initiating users. - override fun sign(signableData: SignableData, publicKey: PublicKey): TransactionSignature { - val keyPair = getSigningKeyPair(publicKey) - return keyPair.sign(signableData) - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt index f6c06c8517..71dec1850e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt @@ -3,8 +3,10 @@ package net.corda.node.services.persistence import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.crypto.toStringShort import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.hash import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.nodeapi.internal.KeyOwningIdentity import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -23,7 +25,28 @@ class PublicKeyToOwningIdentityCacheImpl(private val database: CordaPersistence, private val cache = cacheFactory.buildNamed(Caffeine.newBuilder(), "PublicKeyToOwningIdentityCache_cache") - private fun isKeyBelongingToNode(key: PublicKey): Boolean { + /** + * Establish whether a public key is one of the node's identity keys, by looking in the node's identity database table. + */ + private fun isKeyIdentityKey(key: PublicKey): Boolean { + return database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) + val queryRoot = criteriaQuery.from(PersistentIdentityService.PersistentIdentity::class.java) + criteriaQuery.select(criteriaBuilder.count(queryRoot)) + criteriaQuery.where( + criteriaBuilder.equal(queryRoot.get(PersistentIdentityService.PersistentIdentity::publicKeyHash.name), key.hash.toString()) + ) + val query = session.createQuery(criteriaQuery) + query.uniqueResult() > 0 + } + } + + /** + * Check to see if the key belongs to one of the key pairs in the node_our_key_pairs table. These keys may relate to confidential + * identities. + */ + private fun isKeyPartOfNodeKeyPairs(key: PublicKey): Boolean { return database.transaction { val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) @@ -55,7 +78,7 @@ class PublicKeyToOwningIdentityCacheImpl(private val database: CordaPersistence, ) val query = session.createQuery(criteriaQuery) val uuid = query.uniqueResult() - if (uuid != null || isKeyBelongingToNode(key)) { + if (uuid != null || isKeyPartOfNodeKeyPairs(key) || isKeyIdentityKey(key)) { val signingEntity = KeyOwningIdentity.fromUUID(uuid) log.debug { "Database lookup for public key ${key.toStringShort()}, found signing entity $signingEntity" } signingEntity 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 a41bcbcde7..32c18e2224 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 @@ -14,7 +14,6 @@ import net.corda.node.services.api.SchemaService.SchemaOptions 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.messaging.P2PMessageDeduplicator import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionStorage @@ -38,7 +37,6 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java, DBTransactionStorage.DBTransaction::class.java, BasicHSMKeyManagementService.PersistentKey::class.java, - PersistentKeyManagementService.PersistentKey::class.java, NodeSchedulerService.PersistentScheduledState::class.java, NodeAttachmentService.DBAttachment::class.java, P2PMessageDeduplicator.ProcessedMessage::class.java, diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt index 1226a3f36e..c7484136b5 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImplTest.kt @@ -118,4 +118,10 @@ class PublicKeyToOwningIdentityCacheImplTest { val keys = generateKeyPair() assertEquals(null, testCache[keys.public]) } + + @Test + fun `can request initial identity key`() { + val key = alice.publicKey + assertEquals(KeyOwningIdentity.UnmappedIdentity, testCache[key]) + } } \ 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 87f7973646..0d2a6c8078 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 @@ -6,6 +6,7 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.StateRef import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.CordaX500Name @@ -26,7 +27,7 @@ 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.keys.BasicHSMKeyManagementService import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -173,8 +174,23 @@ open class MockServices private constructor( // 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 keyManagementService = PersistentKeyManagementService(TestingNamedCacheFactory(), identityService, persistence, pkToIdCache) - persistence.transaction { keyManagementService.start(moreKeys + initialIdentity.keyPair) } + val aliasKeyMap = mutableMapOf() + val aliasedMoreKeys = moreKeys.mapIndexed { index, keyPair -> + val alias = "Extra key $index" + aliasKeyMap[alias] = keyPair + KeyPair(keyPair.public, AliasPrivateKey(alias)) + }.toSet() + val identityAlias = "${initialIdentity.name} private key" + aliasKeyMap[identityAlias] = initialIdentity.keyPair + val aliasedIdentityKey = KeyPair(initialIdentity.publicKey, AliasPrivateKey(identityAlias)) + val keyManagementService = BasicHSMKeyManagementService( + TestingNamedCacheFactory(), + identityService, + persistence, + MockCryptoService(aliasKeyMap), + pkToIdCache + ) + persistence.transaction { keyManagementService.start(aliasedMoreKeys + aliasedIdentityKey) } val mockService = persistence.transaction { makeMockMockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys, keyManagementService, schemaService, persistence) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt new file mode 100644 index 0000000000..dd7b4aece5 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockCryptoService.kt @@ -0,0 +1,68 @@ +package net.corda.testing.node.internal + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.internal.cordaBouncyCastleProvider +import net.corda.core.crypto.newSecureRandom +import net.corda.core.crypto.sha256 +import net.corda.nodeapi.internal.crypto.ContentSignerBuilder +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException +import org.bouncycastle.operator.ContentSigner +import java.security.KeyPair +import java.security.PublicKey +import java.security.Signature + +class MockCryptoService(initialKeyPairs: Map) : CryptoService { + + private val aliasToKey: MutableMap = mutableMapOf() + + init { + initialKeyPairs.forEach { + aliasToKey[it.key] = it.value + } + } + + override fun containsKey(alias: String): Boolean { + return aliasToKey.containsKey(alias) + } + + override fun getPublicKey(alias: String): PublicKey? { + return aliasToKey[alias]?.public + } + + override fun sign(alias: String, data: ByteArray, signAlgorithm: String?): ByteArray { + try { + return when(signAlgorithm) { + null -> Crypto.doSign(aliasToKey[alias]!!.private, data) + else -> signWithAlgorithm(alias, data, signAlgorithm) + } + } catch (e: Exception) { + throw CryptoServiceException("Cannot sign using the key with alias $alias. SHA256 of data to be signed: ${data.sha256()}", e) + } + } + + private fun signWithAlgorithm(alias: String, data: ByteArray, signAlgorithm: String): ByteArray { + val privateKey = aliasToKey[alias]!!.private + val signature = Signature.getInstance(signAlgorithm, cordaBouncyCastleProvider) + signature.initSign(privateKey, newSecureRandom()) + signature.update(data) + return signature.sign() + } + + override fun getSigner(alias: String): ContentSigner { + try { + val privateKey = aliasToKey[alias]!!.private + val signatureScheme = Crypto.findSignatureScheme(privateKey) + return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom()) + } catch (e: Exception) { + throw CryptoServiceException("Cannot get Signer for key with alias $alias", e) + } + } + + override fun generateKeyPair(alias: String, scheme: SignatureScheme): PublicKey { + val keyPair = Crypto.generateKeyPair(scheme) + aliasToKey[alias] = keyPair + return keyPair.public + } +} \ No newline at end of file